Compare commits
244 Commits
v2.0.0-bet
...
v2.1.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c5881f75c9 | ||
![]() |
c4b7429e99 | ||
![]() |
b1eced3612 | ||
![]() |
9d5b07537d | ||
![]() |
122e4141b0 | ||
![]() |
be2de4f15d | ||
![]() |
92a920021d | ||
![]() |
72000cac36 | ||
![]() |
4510902677 | ||
![]() |
c2b9d2fa7b | ||
![]() |
cd38c39908 | ||
![]() |
9016a1e6df | ||
![]() |
627254d5a7 | ||
![]() |
ff31558252 | ||
![]() |
9454978264 | ||
![]() |
e2d25a7a09 | ||
![]() |
85f824f032 | ||
![]() |
1a48910e6b | ||
![]() |
bffd5829d0 | ||
![]() |
7e12bd1bef | ||
![]() |
af0817ab74 | ||
![]() |
fbf1a051a2 | ||
![]() |
7ecf7f704a | ||
![]() |
7b7a74d821 | ||
![]() |
e4acc33519 | ||
![]() |
2fd141d914 | ||
![]() |
aad814d342 | ||
![]() |
7e21aaec17 | ||
![]() |
6d5fdfe2e2 | ||
![]() |
5942cd6cd2 | ||
![]() |
5cd17e71e2 | ||
![]() |
2f2ecaa61e | ||
![]() |
aa858a35e2 | ||
![]() |
d658150d42 | ||
![]() |
c312149b35 | ||
![]() |
18a9a3df12 | ||
![]() |
0cdd3581c9 | ||
![]() |
cae79b811f | ||
![]() |
6d953babcb | ||
![]() |
975e5f3fd0 | ||
![]() |
6cfe92bed1 | ||
![]() |
a51c0850c8 | ||
![]() |
03415456bf | ||
![]() |
0309a0fae1 | ||
![]() |
b48910bb94 | ||
![]() |
9a89786dd3 | ||
![]() |
15a5261189 | ||
![]() |
f616da3b85 | ||
![]() |
0e9cf016ec | ||
![]() |
4481f12e32 | ||
![]() |
66efaedcbb | ||
![]() |
771c1fab92 | ||
![]() |
8d6e7ed477 | ||
![]() |
5d80511b9b | ||
![]() |
90f90dc9b4 | ||
![]() |
a58e8498aa | ||
![]() |
ca355d5855 | ||
![]() |
826322b610 | ||
![]() |
80ff5677ea | ||
![]() |
62c417cd51 | ||
![]() |
f27f25aa03 | ||
![]() |
285a4b5aef | ||
![]() |
47a2ded30d | ||
![]() |
5b502b1e1a | ||
![]() |
aff56077a8 | ||
![]() |
6e371ac5ac | ||
![]() |
1b69b89d2d | ||
![]() |
5a20c8e512 | ||
![]() |
4ca1503beb | ||
![]() |
567a7eb7f3 | ||
![]() |
20f27fe32f | ||
![]() |
1a50d6bb86 | ||
![]() |
0b16c2db03 | ||
![]() |
76ac888386 | ||
![]() |
7cfa05d7f2 | ||
![]() |
e3496d0485 | ||
![]() |
d2c33c0074 | ||
![]() |
9c5caecafa | ||
![]() |
64651d5a84 | ||
![]() |
4493236879 | ||
![]() |
ce643942ea | ||
![]() |
46d216b02f | ||
![]() |
133d43ae30 | ||
![]() |
27155cb7e3 | ||
![]() |
b55c413774 | ||
![]() |
69be86e16c | ||
![]() |
65f6b0881e | ||
![]() |
c2bede40c7 | ||
![]() |
33c2398de9 | ||
![]() |
64cfc43891 | ||
![]() |
6575c69409 | ||
![]() |
e3f4e0b775 | ||
![]() |
5be89bfda5 | ||
![]() |
e1b573adeb | ||
![]() |
0913c7aa9e | ||
![]() |
5297626816 | ||
![]() |
c075642d78 | ||
![]() |
253f1a44c1 | ||
![]() |
26837c8871 | ||
![]() |
c732d31edd | ||
![]() |
c2595f28fb | ||
![]() |
58656a63cf | ||
![]() |
f4ce178cfa | ||
![]() |
2c1cfc64d5 | ||
![]() |
4b84d93cee | ||
![]() |
0bfa347595 | ||
![]() |
c7a07b59fd | ||
![]() |
c7c737d8c9 | ||
![]() |
1126e668d1 | ||
![]() |
b71a94ebca | ||
![]() |
815137af19 | ||
![]() |
3c4fbfec54 | ||
![]() |
b3a8eb7e06 | ||
![]() |
1e5c64ff73 | ||
![]() |
a320bfa425 | ||
![]() |
1c4dfc3c6e | ||
![]() |
92ee4d33c3 | ||
![]() |
01c2f01d3f | ||
![]() |
b83e6a5d5b | ||
![]() |
16281b38e0 | ||
![]() |
ca11e116b6 | ||
![]() |
1f32e4a642 | ||
![]() |
4c01089de0 | ||
![]() |
47a416dd9b | ||
![]() |
1ab1bbdd70 | ||
![]() |
1e05cb168c | ||
![]() |
d0383c1edf | ||
![]() |
c8ee35692c | ||
![]() |
3e062a8021 | ||
![]() |
0eb17e7102 | ||
![]() |
a14796cf90 | ||
![]() |
84b1c1ce6c | ||
![]() |
27e0b65c2d | ||
![]() |
ef35576174 | ||
![]() |
804faf726b | ||
![]() |
a94e5d2e47 | ||
![]() |
54d817d656 | ||
![]() |
00aa6c1f6a | ||
![]() |
ed34815393 | ||
![]() |
ceb15716c3 | ||
![]() |
bc97f540de | ||
![]() |
4ae67c79e0 | ||
![]() |
be857e989b | ||
![]() |
232402197c | ||
![]() |
12cbe84166 | ||
![]() |
b7b0be141c | ||
![]() |
8f9f4d9b71 | ||
![]() |
b97b9b7a28 | ||
![]() |
cf23732da3 | ||
![]() |
4e6b1d54e8 | ||
![]() |
fec8f90b78 | ||
![]() |
bedd4dafc4 | ||
![]() |
0d92648d62 | ||
![]() |
07989bc2fa | ||
![]() |
09ff2cb9a3 | ||
![]() |
6aa2e60037 | ||
![]() |
9f3ef532b8 | ||
![]() |
654621b438 | ||
![]() |
f8cfa9e02d | ||
![]() |
7476aa5897 | ||
![]() |
1d00a59706 | ||
![]() |
82d4e5e456 | ||
![]() |
07b6a36aab | ||
![]() |
0ceb6cc143 | ||
![]() |
864a155ee8 | ||
![]() |
15e2e575ee | ||
![]() |
253c524131 | ||
![]() |
48c02b8554 | ||
![]() |
d8fef22c9a | ||
![]() |
d82103e02d | ||
![]() |
9e5b22931e | ||
![]() |
30cd361edb | ||
![]() |
48da0071bd | ||
![]() |
32037e3645 | ||
![]() |
4279038a62 | ||
![]() |
c8afe7503b | ||
![]() |
4dfae98250 | ||
![]() |
b44dca49f5 | ||
![]() |
3d5892a841 | ||
![]() |
365e06891a | ||
![]() |
0f436481b8 | ||
![]() |
a5510be0bf | ||
![]() |
45a3c95a66 | ||
![]() |
94cb13ce9d | ||
![]() |
fecc431b10 | ||
![]() |
c88a975bf0 | ||
![]() |
4ed26ab934 | ||
![]() |
76a8da20bf | ||
![]() |
9f05747681 | ||
![]() |
f0bb9bb953 | ||
![]() |
72c6ac09ac | ||
![]() |
07c38d6353 | ||
![]() |
dd1cafbc77 | ||
![]() |
77e30d7844 | ||
![]() |
1c4f772a14 | ||
![]() |
bfc3148e4f | ||
![]() |
85db76c86e | ||
![]() |
f6c0d97172 | ||
![]() |
67f846513b | ||
![]() |
ad4cf843dd | ||
![]() |
af79b1504f | ||
![]() |
caff72967e | ||
![]() |
4abd21bbca | ||
![]() |
a2770a1bff | ||
![]() |
30e2d15321 | ||
![]() |
ba7e8b8627 | ||
![]() |
58850e97ff | ||
![]() |
b35acabc56 | ||
![]() |
ef907f1bc4 | ||
![]() |
c25128917b | ||
![]() |
9db6e64666 | ||
![]() |
f51a29a2e1 | ||
![]() |
b90025a2d0 | ||
![]() |
03b5ae02e1 | ||
![]() |
70627b992a | ||
![]() |
e830cd9baa | ||
![]() |
05fb10390e | ||
![]() |
d2c802c9da | ||
![]() |
741c165a5b | ||
![]() |
53d3ede184 | ||
![]() |
a41d2f831d | ||
![]() |
e30b7d0fa4 | ||
![]() |
7222741863 | ||
![]() |
49cea717af | ||
![]() |
49a7b62155 | ||
![]() |
3abc88985a | ||
![]() |
764b007d2c | ||
![]() |
a5c0d3dfae | ||
![]() |
386070923f | ||
![]() |
e79015d877 | ||
![]() |
b574396751 | ||
![]() |
91400070a7 | ||
![]() |
23d02085e8 | ||
![]() |
a9459dca89 | ||
![]() |
bd08cd1983 | ||
![]() |
3ae91495c1 | ||
![]() |
7f9fa46271 | ||
![]() |
f3b02d9922 | ||
![]() |
f2be582299 | ||
![]() |
b2eb403f9b | ||
![]() |
7e6a5927d7 | ||
![]() |
2c4a3650c4 | ||
![]() |
5b125d4513 | ||
![]() |
66167aeb55 |
1
.env
@@ -1,2 +1 @@
|
||||
COMPOSE_PROJECT_NAME=paperless
|
||||
export PROMPT="(pipenv-projectname)$P$G"
|
||||
|
40
.github/workflows/ci.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
documentation:
|
||||
name: "Build Documentation"
|
||||
name: "Build & Deploy Documentation"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
@@ -77,6 +77,14 @@ jobs:
|
||||
name: Make documentation
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs build --config-file ./mkdocs.yml
|
||||
-
|
||||
name: Deploy documentation
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
|
||||
git config --global user.name "${{ github.actor }}"
|
||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history
|
||||
-
|
||||
name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -85,25 +93,6 @@ jobs:
|
||||
path: site/
|
||||
retention-days: 7
|
||||
|
||||
documentation-deploy:
|
||||
name: "Deploy Documentation"
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- documentation
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CUSTOM_DOMAIN: docs.paperless-ngx.com
|
||||
CONFIG_FILE: mkdocs.yml
|
||||
EXTRA_PACKAGES: build-base
|
||||
|
||||
tests-backend:
|
||||
name: "Backend Tests (Python ${{ matrix.python-version }})"
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -120,8 +109,8 @@ jobs:
|
||||
-
|
||||
name: Start containers
|
||||
run: |
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml up --detach
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
@@ -176,8 +165,8 @@ jobs:
|
||||
name: Stop containers
|
||||
if: always()
|
||||
run: |
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml logs
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml down
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
||||
|
||||
install-frontend-depedendencies:
|
||||
name: "Install Frontend Dependendencies"
|
||||
@@ -437,6 +426,7 @@ jobs:
|
||||
name: "Build Release"
|
||||
needs:
|
||||
- build-docker-image
|
||||
- documentation
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
@@ -642,7 +632,7 @@ jobs:
|
||||
git push origin ${{ needs.publish-release.outputs.version }}-changelog
|
||||
-
|
||||
name: Create Pull Request
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
|
25
.github/workflows/cleanup-tags.yml
vendored
@@ -19,9 +19,13 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
cleanup-images:
|
||||
name: Cleanup Image Tags for paperless-ngx
|
||||
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
||||
if: github.repository_owner == 'paperless-ngx'
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
|
||||
@@ -29,12 +33,12 @@ jobs:
|
||||
-
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
is_org: "true"
|
||||
package_name: "paperless-ngx"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
scheme: "branch"
|
||||
repo_name: "paperless-ngx"
|
||||
match_regex: "feature-"
|
||||
@@ -49,18 +53,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- primary-name: "paperless-ngx"
|
||||
- primary-name: "paperless-ngx/builder/cache/app"
|
||||
# TODO: Remove the above and replace with the below
|
||||
# - primary-name: "builder/qpdf"
|
||||
# - primary-name: "builder/cache/qpdf"
|
||||
# - primary-name: "builder/pikepdf"
|
||||
# - primary-name: "builder/cache/pikepdf"
|
||||
# - primary-name: "builder/jbig2enc"
|
||||
# - primary-name: "builder/cache/jbig2enc"
|
||||
# - primary-name: "builder/psycopg2"
|
||||
# - primary-name: "builder/cache/psycopg2"
|
||||
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
|
||||
@@ -68,7 +61,7 @@ jobs:
|
||||
-
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.3.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.4.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
|
33
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '2 */12 * * *'
|
||||
push:
|
||||
paths: [
|
||||
'src/locale/**',
|
||||
'src-ui/src/locale/**'
|
||||
]
|
||||
branches: [ dev ]
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
name: Crowdin Sync
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v1
|
||||
with:
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
crowdin_branch_name: 'dev'
|
||||
localization_branch_name: l10n_dev
|
||||
pull_request_labels: 'skip-changelog, translation'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
30
.github/workflows/project-actions.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Project Automations
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
pull_request_target: #_target allows access to secrets
|
||||
types:
|
||||
- opened
|
||||
@@ -16,25 +12,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
todo: Todo
|
||||
done: Done
|
||||
in_progress: In Progress
|
||||
|
||||
jobs:
|
||||
issue_opened_or_reopened:
|
||||
name: issue_opened_or_reopened
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||
steps:
|
||||
- name: Add issue to project and set status to ${{ env.todo }}
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.2.1
|
||||
with:
|
||||
gh_token: ${{ secrets.GH_TOKEN }}
|
||||
organization: paperless-ngx
|
||||
project_id: 2
|
||||
resource_node_id: ${{ github.event.issue.node_id }}
|
||||
status_value: ${{ env.todo }} # Target status
|
||||
pr_opened_or_reopened:
|
||||
name: pr_opened_or_reopened
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -43,14 +21,6 @@ jobs:
|
||||
pull-requests: write
|
||||
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||
steps:
|
||||
- name: Add PR to project and set status to "Needs Review"
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.2.1
|
||||
with:
|
||||
gh_token: ${{ secrets.GH_TOKEN }}
|
||||
organization: paperless-ngx
|
||||
project_id: 2
|
||||
resource_node_id: ${{ github.event.pull_request.node_id }}
|
||||
status_value: "Needs Review" # Target status
|
||||
- name: Label PR with release-drafter
|
||||
uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
|
14
.github/workflows/repo-maintenance.yml
vendored
@@ -33,10 +33,11 @@ jobs:
|
||||
name: 'Lock Old Threads'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '30'
|
||||
pr-inactive-days: '30'
|
||||
discussion-inactive-days: '30'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
This issue has been automatically locked since there
|
||||
@@ -46,13 +47,21 @@ jobs:
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new discussion or issue for related concerns.
|
||||
discussion-comment: >
|
||||
This discussion has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new discussion for related concerns.
|
||||
close-answered-discussions:
|
||||
name: 'Close Answered Discussions'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const query = `query($owner:String!, $name:String!) {
|
||||
repository(owner:$owner, name:$name){
|
||||
discussions(first:100, answered:true, states:[OPEN]) {
|
||||
@@ -96,4 +105,5 @@ jobs:
|
||||
}
|
||||
await github.graphql(closeDiscussionMutation, closeVariables)
|
||||
|
||||
await sleep(1000)
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "index.md",
|
||||
"files": ["index.md", "administration.md"],
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
|
2
Pipfile
@@ -20,7 +20,7 @@ djangorestframework = "~=3.14"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
celery = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
channels = "~=4.0"
|
||||
channels-redis = "*"
|
||||
concurrent-log-handler = "*"
|
||||
|
375
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f39e8ff3dbd6a7ad290d3fcad1efaf9252ffb498353527cb53126cf73e76824c"
|
||||
"sha256": "d7ef8db734997cda7c11971f2ddb66bf1918f4232b0956a9bf604c41763ce461"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
|
||||
"sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"
|
||||
"sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f",
|
||||
"sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.0.0"
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
@@ -156,20 +156,19 @@
|
||||
"redis"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:30b75ac60fb081c2d9f8881382c148ed7c9052031a75a1e8743ff4b4b071f184",
|
||||
"sha256:6b65d8dd5db499dd6190c45aa6398e171b99592f2af62c312f7391587feb5458"
|
||||
"sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9",
|
||||
"sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==5.3.5"
|
||||
"version": "==5.3.6"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
"sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1",
|
||||
"sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.7.22"
|
||||
"version": "==2023.11.17"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -385,32 +384,32 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf",
|
||||
"sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84",
|
||||
"sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e",
|
||||
"sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8",
|
||||
"sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7",
|
||||
"sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1",
|
||||
"sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88",
|
||||
"sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86",
|
||||
"sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179",
|
||||
"sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81",
|
||||
"sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20",
|
||||
"sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548",
|
||||
"sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d",
|
||||
"sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d",
|
||||
"sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5",
|
||||
"sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1",
|
||||
"sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147",
|
||||
"sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936",
|
||||
"sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797",
|
||||
"sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696",
|
||||
"sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72",
|
||||
"sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da",
|
||||
"sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"
|
||||
"sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960",
|
||||
"sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a",
|
||||
"sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc",
|
||||
"sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a",
|
||||
"sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf",
|
||||
"sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1",
|
||||
"sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39",
|
||||
"sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406",
|
||||
"sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a",
|
||||
"sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a",
|
||||
"sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c",
|
||||
"sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be",
|
||||
"sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15",
|
||||
"sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2",
|
||||
"sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d",
|
||||
"sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157",
|
||||
"sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003",
|
||||
"sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248",
|
||||
"sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a",
|
||||
"sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec",
|
||||
"sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309",
|
||||
"sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7",
|
||||
"sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==41.0.5"
|
||||
"version": "==41.0.7"
|
||||
},
|
||||
"dateparser": {
|
||||
"hashes": [
|
||||
@@ -541,11 +540,11 @@
|
||||
},
|
||||
"exceptiongroup": {
|
||||
"hashes": [
|
||||
"sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
|
||||
"sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
|
||||
"sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
|
||||
"sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==1.1.3"
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
@@ -567,12 +566,12 @@
|
||||
},
|
||||
"gotenberg-client": {
|
||||
"hashes": [
|
||||
"sha256:4508ecb913ef2d553dd2ceb78e32cee001000ba08c910ba1f9ace38350d1589e",
|
||||
"sha256:7a3f8a02caee768391373b3610c6ec25a853cccf391ed6b5d5a1292c3ed15e7e"
|
||||
"sha256:69e9dd5264b75ed0ba1f9eebebdc750b13d190710fd82ca0670d161c249155c9",
|
||||
"sha256:dd0f49d3d4e01399949f39ac5024a5512566c8ded6ee457a336a5f77ce4c1a25"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.3.0"
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@@ -754,11 +753,11 @@
|
||||
"http2"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a",
|
||||
"sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"
|
||||
"sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8",
|
||||
"sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.25.1"
|
||||
"version": "==0.25.2"
|
||||
},
|
||||
"humanize": {
|
||||
"hashes": [
|
||||
@@ -778,11 +777,11 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.4"
|
||||
"version": "==3.6"
|
||||
},
|
||||
"imap-tools": {
|
||||
"hashes": [
|
||||
@@ -794,9 +793,9 @@
|
||||
},
|
||||
"img2pdf": {
|
||||
"hashes": [
|
||||
"sha256:ae6c19731bde2551356c178bf356ca118ac32a232c737a14b423f8039df3c24b"
|
||||
"sha256:73847e47242f4b5bd113c70049e03e03212936c2727cd2a8bf564229a67d0b95"
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"inotify-simple": {
|
||||
"hashes": [
|
||||
@@ -824,11 +823,11 @@
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:1491df826cfc5178c80f3e89dd6dfba68e484ef334db81070eb5cb8094b31167",
|
||||
"sha256:6cd5c5d5ef77538434b8f81f3e265c414269418645dbb47dbf130a8a05c3e357"
|
||||
"sha256:0bb2e278644d11dea6272c17974a3dbb9688a949f3bb60aeb5b791329c44fadc",
|
||||
"sha256:63bb093fc9bb80cfb3a0972336a5cec1fa7ac5f9ef7e8237c6bf8dda9469313e"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==5.3.3"
|
||||
"version": "==5.3.4"
|
||||
},
|
||||
"langdetect": {
|
||||
"hashes": [
|
||||
@@ -1081,12 +1080,12 @@
|
||||
},
|
||||
"ocrmypdf": {
|
||||
"hashes": [
|
||||
"sha256:0ead16bb9842ac206d26a8c17d9acb76e9105e5fccecfc4cf5ced9dcc53f6d0a",
|
||||
"sha256:e63015b8308be4f95e881f55a4cf5108696b748e0fd0e95de18d587a72f3b750"
|
||||
"sha256:63803b21f80925170cc96dec11833a6cb740be377e50adfaa210b2c26e64b67a",
|
||||
"sha256:9385f582fa8b9eb4d89a27dfd80a29cffdf53694c570f9f0e87db4af8cfe3b15"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==15.4.2"
|
||||
"version": "==15.4.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
@@ -1123,41 +1122,41 @@
|
||||
},
|
||||
"pikepdf": {
|
||||
"hashes": [
|
||||
"sha256:07f6b4dfc492bd13cc18d9b6d5b0fcd57d82b1b7bffdc811e5de95722a6c9480",
|
||||
"sha256:0b03167b53f465fbe1d658d5e3440fe1627bf21f0fe612bd3810d397634a2c1b",
|
||||
"sha256:0c8b52f210513d34db8cea1960009bf8404c4d2f4f94a5b3e31d140fe33ddcb1",
|
||||
"sha256:123c1e68c042fc1e51a6b8d0294a3bef1c091fcdc427abe71c9436fc1f9b51b2",
|
||||
"sha256:261b0c107c659d9072879cb5837c38657966c8701c52d1a344b1176319d9a69a",
|
||||
"sha256:26f40f9f9f113d85b0b6ac33a9539ca1c1cbf5a77c040e7c859cb71037c4c71d",
|
||||
"sha256:39fcb93fb644f2e5fbb73142688fef701463f9c4d9a6af2d42c4ac9eca4e6f02",
|
||||
"sha256:474eaa781216c467ee7e5b698a43f7d42012e277b16dd5b4ee6822955ad9c8a6",
|
||||
"sha256:4b67956b6faceccd9da1d5a513bc3de8c5e2ee36e07cd8e4d24d639223ca6bbc",
|
||||
"sha256:507e36c40568ff068e5209c34251168e19177fcba20ec8d83b5ae5340d20b0eb",
|
||||
"sha256:54c0f76558da5c0f4779e31726fd5fa4ad6ec910ec319cd956bf638b5520f690",
|
||||
"sha256:60a21302e03978a7f4c1fb6ef108eab4ac4f91aa0f7740f8636ead5ac9008b91",
|
||||
"sha256:631b8f6b38bfe81977e0092f2e9a6c0bd5dd93510b809a277295c200a95f04b2",
|
||||
"sha256:642525270546eef175e85bde4469eeaee767989f476ae3c129cb359b0a49159b",
|
||||
"sha256:6a9dd1afa246c9e8c3f3acc96bb27b75666a87c16ffd9231f44b9379af52df48",
|
||||
"sha256:6be2cee62932e77c2906e9594c538bf8092a72b54cabd0c995672506740a7afc",
|
||||
"sha256:77d5f1b2b19a0a9f3aaf4114f12083e499cad5a94c19175d1e4fd19140e28143",
|
||||
"sha256:7ca2f630f8ff2f4e9218c541fad320719bdc7f8ed9495e7d9f1b99075e7b382c",
|
||||
"sha256:7f933fb675fa6de32f5d77963869a59da6a5536eaafc80dd1a0783457861c1dd",
|
||||
"sha256:7fe08b60a13503b1e9481a84213c0db4e0c27effacb4740c2b4379c76820871f",
|
||||
"sha256:80bbe725be2163b1301b355472d45f2857f12ff7bf259d39a282cf99eb094d5c",
|
||||
"sha256:81261e533d513472084a46d044027c37c76561cf707106a86ecfb0b12d124ef2",
|
||||
"sha256:b6b341c309cb0d992d10af3f4983ea69ce9d6f37274c474bf450fd0d8b4595fe",
|
||||
"sha256:b7ab92e9f9e94c9bb264cdc622847fb3ece1b2c50ee99c6a6d35975cf3ec2bbe",
|
||||
"sha256:b9e2bd27d57620c0f3a3801df0101f19ce53f0d9447a1a881940aeb773df5e87",
|
||||
"sha256:c3a7f1fa5c5f806bdc01bb0b7c2ca6ac1f57b3a6fd1c15139a2b4afde3f40284",
|
||||
"sha256:c729ce4d55485beaf1b526b55478ab15254eea704ecb1e40c9c9941c7448c297",
|
||||
"sha256:cfc58963da2b83568d068ab5b93f90ea009c9ecf8e3e9e6aad26eaa6e7bf5905",
|
||||
"sha256:d2af13e16b694e7913fe07386931bbf23964f74a6c0470136f9fb0fb6cac7b9b",
|
||||
"sha256:e5ac975a421f308ca50fead5043474225275203d60a67153020cfc28e35cdfea",
|
||||
"sha256:fa31e8ad1356e0469a6d80e388897fd5032f9044d412eb9cf6a24442449acc3f",
|
||||
"sha256:ff55fee1061a5914c882e1120e8df591a0af6973db045305fc0432ef860f10c3"
|
||||
"sha256:08070185661db9dfbaec014094fec9e323d8ab882dc458a07383f55e08bfdd22",
|
||||
"sha256:0f118446960213c78b21fe106890028dcaff739369bceda9008719b43670e290",
|
||||
"sha256:1e982dbf2d25df72975e2d336ca6a5a8565add156bfdba328beb67b324a1689e",
|
||||
"sha256:2771cba79c62b3f9d93cffa684a92d60e6050f727d4f52220d8180eaf967c4bf",
|
||||
"sha256:2a320bfa4caa73d00d90b04433305b3ff7a6c63bdf36f55af16725066fe46b49",
|
||||
"sha256:3748c4d67b40c26fa777d1f3eefcab4bcca101395ad54502020653024d23c37d",
|
||||
"sha256:3956b2abda0ef86b37b5d49066c3adfd29d056ed96b1ef93bacbd269c78c5e13",
|
||||
"sha256:3b719a40f1b08da1ff6fd87b3847d4ce033f8ffd81a13dca8c3c947a080c071d",
|
||||
"sha256:3ff178684ed43de28ca3e7dcf9da5082ebf49bcd042e9308ffaa0162275da1a4",
|
||||
"sha256:42360df95f08164162af465c493d1e612d3c0ff6d23dc17be073380b05db715c",
|
||||
"sha256:5075a2a741ba23212e963d9ea3fc7a4f9394535fab11fdc7ea4641d61cab0287",
|
||||
"sha256:67276e1731dc8b2f58663ff62b4728f9e0046c8217768fe0649428315cfe8253",
|
||||
"sha256:69d69a93d07027e351996ef8232f26bba762d415206414e0ae3814be0aee0f7a",
|
||||
"sha256:7075c79fa5d85b0acf1536a1849bcbb4e661b33f476c7c6d81255df247b2ae1f",
|
||||
"sha256:7bd88db0b68da1d04dd5a1ed0897e37ef1968317a887781b1e26c1649bbd11e3",
|
||||
"sha256:80275466282495c672264801b50c7188f86590779f3ada8148336e3e84fd06c0",
|
||||
"sha256:897cac14f9e3db97d4effea50190e6c59e87d4147da98ff6108d254dcc1949d9",
|
||||
"sha256:8a1ac9e17d890b39f717b040eaebc79325ff23278756805220138f054599f9f0",
|
||||
"sha256:8e687681f26533fee69904403fdf4fc2a1f0e47480b5bfbba64139c07d72c9d2",
|
||||
"sha256:9430eb3439c0e9cb1b477115d5a34905f5c6be55422d66fcba55cedbf711ae89",
|
||||
"sha256:94737db4beb7391ca78627e461050c5dd3a7cb7526fd6bdd0a0db94780e8b103",
|
||||
"sha256:94998ff1d15b0eaec76494266e2e61d80573c5ee4ac0860a9af4a02e5107fe17",
|
||||
"sha256:a9da8d1db2d7e17411afef351ea9ccddbff36ce748b16003d4f065250c941436",
|
||||
"sha256:c11acef5c211af2559e1753ed96b3b4f30e7a9e26acad4b2cbd55ea3b25e3154",
|
||||
"sha256:c631c6fd16b44a2636988d8be7202f1ffd3932f505e8b9044a6b88ef62411871",
|
||||
"sha256:d0678ed952a85f4b3b23d5ee94445e55a05aec0b47de37843625eba1f9b0fbb3",
|
||||
"sha256:d7899f43a198d184c3d4787390a67c654a8c600c6c844f29b557b4f236d11619",
|
||||
"sha256:dc8f0116237d2f07fd8a8763f2c59f2fa50f445471f0c619914075ec411920e2",
|
||||
"sha256:dd5ad4cb6244ac885512f5385f6fc055e2bfc174636189fc1408f138458380c8",
|
||||
"sha256:e93d706b40cebc3b2fdaa9e814b4008ed4c659c6df03ee6a4ed46683aa21ab24",
|
||||
"sha256:ece366b8e68be47a7252c27f73dd9666df6871fb6e302a7bfd00a5cb293743f2",
|
||||
"sha256:f3781b8dc868e3d452ce36348b26735d7a2fc0fe6a4503f72411e5bc9341a7fa"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==8.7.0"
|
||||
"version": "==8.7.1"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
@@ -1245,11 +1244,11 @@
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:99ba3dfb23d5b5af89712f89e60a5f3d9b8b67a9482ca377c5771d0e9047a34b",
|
||||
"sha256:a371c06bb1d66cd499fecd708e50c0b6ae00acba9822ba33c586e2f16d1b739e"
|
||||
"sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0",
|
||||
"sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==3.0.40"
|
||||
"version": "==3.0.41"
|
||||
},
|
||||
"psycopg2": {
|
||||
"hashes": [
|
||||
@@ -1280,11 +1279,11 @@
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
|
||||
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
|
||||
"sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c",
|
||||
"sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.16.1"
|
||||
"version": "==2.17.2"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@@ -1613,11 +1612,11 @@
|
||||
},
|
||||
"rich": {
|
||||
"hashes": [
|
||||
"sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245",
|
||||
"sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"
|
||||
"sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa",
|
||||
"sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==13.6.0"
|
||||
"version": "==13.7.0"
|
||||
},
|
||||
"scikit-learn": {
|
||||
"hashes": [
|
||||
@@ -1850,7 +1849,7 @@
|
||||
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
|
||||
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"markers": "python_version < '3.10'",
|
||||
"version": "==4.8.0"
|
||||
},
|
||||
"tzdata": {
|
||||
@@ -2040,10 +2039,10 @@
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:9a929bd8380f6cd9571a968a9c8f4353ca58d7cd812a4822bba831f8d685b223",
|
||||
"sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8"
|
||||
"sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02",
|
||||
"sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"
|
||||
],
|
||||
"version": "==0.2.9"
|
||||
"version": "==0.2.12"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
@@ -2301,11 +2300,11 @@
|
||||
"develop": {
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
|
||||
"sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"
|
||||
"sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f",
|
||||
"sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.0.0"
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
@@ -2372,11 +2371,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
"sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1",
|
||||
"sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.7.22"
|
||||
"version": "==2023.11.17"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -2672,11 +2671,11 @@
|
||||
},
|
||||
"exceptiongroup": {
|
||||
"hashes": [
|
||||
"sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
|
||||
"sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
|
||||
"sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
|
||||
"sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==1.1.3"
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"execnet": {
|
||||
"hashes": [
|
||||
@@ -2708,7 +2707,6 @@
|
||||
"sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
|
||||
"sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.13.1"
|
||||
},
|
||||
@@ -2740,11 +2738,11 @@
|
||||
"http2"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a",
|
||||
"sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"
|
||||
"sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8",
|
||||
"sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.25.1"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.25.2"
|
||||
},
|
||||
"hyperlink": {
|
||||
"hashes": [
|
||||
@@ -2755,19 +2753,19 @@
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75",
|
||||
"sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"
|
||||
"sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d",
|
||||
"sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.5.31"
|
||||
"version": "==2.5.33"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.4"
|
||||
"version": "==3.6"
|
||||
},
|
||||
"imagehash": {
|
||||
"hashes": [
|
||||
@@ -2779,11 +2777,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb",
|
||||
"sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"
|
||||
"sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7",
|
||||
"sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"
|
||||
],
|
||||
"markers": "python_version < '3.10'",
|
||||
"version": "==6.8.0"
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"incremental": {
|
||||
"hashes": [
|
||||
@@ -2900,28 +2898,28 @@
|
||||
},
|
||||
"mkdocs-glightbox": {
|
||||
"hashes": [
|
||||
"sha256:8f894435b4f75231164e5d9fb023c01e922e6769e74a121e822c4914f310a41d",
|
||||
"sha256:96aaf98216f83c0d0fad2e42a8d805cfa6329d6ab25b54265012ccb2154010d8"
|
||||
"sha256:096c2753cf4f46f548b02070a2ff5dd8b823a431ce17873a62dcef304cf3364c",
|
||||
"sha256:f572256cca17c912da50a045129026566a79b8c6477e1170258ccc0ac5b162da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.4"
|
||||
"version": "==0.3.5"
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:8b20f6851bddeef37dced903893cd176cf13a21a482e97705a103c45f06ce9b9",
|
||||
"sha256:f0c101453e8bc12b040e8b64ca39a405d950d8402609b1378cc2b98976e74b5f"
|
||||
"sha256:6ed0fbf4682491766f0ec1acc955db6901c2fd424c7ab343964ef51b819741f5",
|
||||
"sha256:ca8b9cd2b3be53e858e5a1a45ac9668bd78d95d77a30288bb5ebc1a31db6184c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==9.4.8"
|
||||
"version": "==9.5.2"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc",
|
||||
"sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd"
|
||||
"sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443",
|
||||
"sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.3"
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -2997,11 +2995,11 @@
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
|
||||
"sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
|
||||
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
|
||||
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.11.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
@@ -3065,11 +3063,11 @@
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
|
||||
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
|
||||
"sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380",
|
||||
"sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.11.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -3081,12 +3079,12 @@
|
||||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32",
|
||||
"sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"
|
||||
"sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376",
|
||||
"sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.5.0"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.6.0"
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
@@ -3113,19 +3111,19 @@
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
|
||||
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
|
||||
"sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c",
|
||||
"sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.16.1"
|
||||
"version": "==2.17.2"
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:bc46f11749ecd4d6b71cf62396104b4a200bad3498cb0f5dad1b8502fe461a35",
|
||||
"sha256:cfc28d6a09d19448bcbf8eee3ce098c7d17ff99f7bd3069db4819af181212037"
|
||||
"sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b",
|
||||
"sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.4"
|
||||
"version": "==10.5"
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
@@ -3163,30 +3161,30 @@
|
||||
},
|
||||
"pytest-env": {
|
||||
"hashes": [
|
||||
"sha256:1efb8acce1f6431196150f3b30673443ff05a6fabff64539a9495cd2248adf9e",
|
||||
"sha256:2b71b37c6810f28bec790a7b373c777af87352b3a359b3de0edb9d24df5cf8b3"
|
||||
"sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc",
|
||||
"sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.1.1"
|
||||
"version": "==1.1.3"
|
||||
},
|
||||
"pytest-httpx": {
|
||||
"hashes": [
|
||||
"sha256:b489c5a7bb847551943eaee601bc35053b35dc4f5961c944305120f14a1d770a",
|
||||
"sha256:ca372b94c569c0aca2f06240f6f78cc223dfbc3ab97b5700d4e14c9a73eab17a"
|
||||
"sha256:24f6f53d507ab483bea8f89b975a1a111fb613ccab4d86e570be8991776e8bcc",
|
||||
"sha256:a33c4e8df415cc1232b3664869b6a8b8061c4c223335aca0b237cefbc01ba0eb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.26.0"
|
||||
"version": "==0.27.0"
|
||||
},
|
||||
"pytest-rerunfailures": {
|
||||
"hashes": [
|
||||
"sha256:784f462fa87fe9bdf781d0027d856b47a4bfe6c12af108f6bd887057a917b48e",
|
||||
"sha256:9a1afd04e21b8177faf08a9bbbf44de7a0fe3fc29f8ddbe83b9684bd5f8f92a9"
|
||||
"sha256:34919cb3fcb1f8e5d4b940aa75ccdea9661bade925091873b7c6fa5548333069",
|
||||
"sha256:e132dbe420bc476f544b96e7036edd0a69707574209b6677263c950d19b09199"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==12.0"
|
||||
"version": "==13.0"
|
||||
},
|
||||
"pytest-sugar": {
|
||||
"hashes": [
|
||||
@@ -3198,19 +3196,18 @@
|
||||
},
|
||||
"pytest-xdist": {
|
||||
"hashes": [
|
||||
"sha256:3a94a931dd9e268e0b871a877d09fe2efb6175c2c23d60d56a6001359002b832",
|
||||
"sha256:e513118bf787677a427e025606f55e95937565e06dfaac8d87f55301e57ae607"
|
||||
"sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a",
|
||||
"sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.4.0"
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.2"
|
||||
},
|
||||
@@ -3298,6 +3295,7 @@
|
||||
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
||||
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==6.0.1"
|
||||
},
|
||||
"pyyaml-env-tag": {
|
||||
@@ -3412,27 +3410,27 @@
|
||||
},
|
||||
"ruff": {
|
||||
"hashes": [
|
||||
"sha256:171276c1df6c07fa0597fb946139ced1c2978f4f0b8254f201281729981f3c17",
|
||||
"sha256:17ef33cd0bb7316ca65649fc748acc1406dfa4da96a3d0cde6d52f2e866c7b39",
|
||||
"sha256:32d47fc69261c21a4c48916f16ca272bf2f273eb635d91c65d5cd548bf1f3d96",
|
||||
"sha256:5cbec0ef2ae1748fb194f420fb03fb2c25c3258c86129af7172ff8f198f125ab",
|
||||
"sha256:721f4b9d3b4161df8dc9f09aa8562e39d14e55a4dbaa451a8e55bdc9590e20f4",
|
||||
"sha256:82bfcb9927e88c1ed50f49ac6c9728dab3ea451212693fe40d08d314663e412f",
|
||||
"sha256:9b97fd6da44d6cceb188147b68db69a5741fbc736465b5cea3928fdac0bc1aeb",
|
||||
"sha256:a00a7ec893f665ed60008c70fe9eeb58d210e6b4d83ec6654a9904871f982a2a",
|
||||
"sha256:a4894dddb476597a0ba4473d72a23151b8b3b0b5f958f2cf4d3f1c572cdb7af7",
|
||||
"sha256:a8c11206b47f283cbda399a654fd0178d7a389e631f19f51da15cbe631480c5b",
|
||||
"sha256:aafb9d2b671ed934998e881e2c0f5845a4295e84e719359c71c39a5363cccc91",
|
||||
"sha256:b2c205827b3f8c13b4a432e9585750b93fd907986fe1aec62b2a02cf4401eee6",
|
||||
"sha256:bb408e3a2ad8f6881d0f2e7ad70cddb3ed9f200eb3517a91a245bbe27101d379",
|
||||
"sha256:c21fe20ee7d76206d290a76271c1af7a5096bc4c73ab9383ed2ad35f852a0087",
|
||||
"sha256:f20dc5e5905ddb407060ca27267c7174f532375c08076d1a953cf7bb016f5a24",
|
||||
"sha256:f80c73bba6bc69e4fdc73b3991db0b546ce641bdcd5b07210b8ad6f64c79f1ab",
|
||||
"sha256:fa29e67b3284b9a79b1a85ee66e293a94ac6b7bb068b307a8a373c3d343aa8ec"
|
||||
"sha256:0683b7bfbb95e6df3c7c04fe9d78f631f8e8ba4868dfc932d43d690698057e2e",
|
||||
"sha256:1ea109bdb23c2a4413f397ebd8ac32cb498bee234d4191ae1a310af760e5d287",
|
||||
"sha256:276a89bcb149b3d8c1b11d91aa81898fe698900ed553a08129b38d9d6570e717",
|
||||
"sha256:290ecab680dce94affebefe0bbca2322a6277e83d4f29234627e0f8f6b4fa9ce",
|
||||
"sha256:416dfd0bd45d1a2baa3b1b07b1b9758e7d993c256d3e51dc6e03a5e7901c7d80",
|
||||
"sha256:45b38c3f8788a65e6a2cab02e0f7adfa88872696839d9882c13b7e2f35d64c5f",
|
||||
"sha256:4af95fd1d3b001fc41325064336db36e3d27d2004cdb6d21fd617d45a172dd96",
|
||||
"sha256:69a4bed13bc1d5dabf3902522b5a2aadfebe28226c6269694283c3b0cecb45fd",
|
||||
"sha256:6b05e3b123f93bb4146a761b7a7d57af8cb7384ccb2502d29d736eaade0db519",
|
||||
"sha256:6c64cb67b2025b1ac6d58e5ffca8f7b3f7fd921f35e78198411237e4f0db8e73",
|
||||
"sha256:7f80496854fdc65b6659c271d2c26e90d4d401e6a4a31908e7e334fab4645aac",
|
||||
"sha256:8b0c2de9dd9daf5e07624c24add25c3a490dbf74b0e9bca4145c632457b3b42a",
|
||||
"sha256:90c958fe950735041f1c80d21b42184f1072cc3975d05e736e8d66fc377119ea",
|
||||
"sha256:9dcc6bb2f4df59cb5b4b40ff14be7d57012179d69c6565c1da0d1f013d29951b",
|
||||
"sha256:de02ca331f2143195a712983a57137c5ec0f10acc4aa81f7c1f86519e52b92a1",
|
||||
"sha256:df2bb4bb6bbe921f6b4f5b6fdd8d8468c940731cb9406f274ae8c5ed7a78c478",
|
||||
"sha256:dffd699d07abf54833e5f6cc50b85a6ff043715da8788c4a79bcd4ab4734d306"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.1.5"
|
||||
"version": "==0.1.7"
|
||||
},
|
||||
"scipy": {
|
||||
"hashes": [
|
||||
@@ -3474,11 +3472,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
|
||||
"sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
|
||||
"sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2",
|
||||
"sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==68.2.2"
|
||||
"version": "==69.0.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -3549,11 +3547,11 @@
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af",
|
||||
"sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"
|
||||
"sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3",
|
||||
"sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==20.24.6"
|
||||
"version": "==20.25.0"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
@@ -3585,7 +3583,6 @@
|
||||
"sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44",
|
||||
"sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
|
18
README.md
@@ -6,8 +6,11 @@
|
||||
[](https://demo.paperless-ngx.com)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png#gh-light-mode-only" width="50%" />
|
||||
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/White%20logo%20-%20no%20background.png#gh-dark-mode-only" width="50%" />
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/resources/logo/web/png/White%20logo%20-%20no%20background.png" width="50%">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
|
||||
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<!-- omit in toc -->
|
||||
@@ -32,16 +35,19 @@ A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com)
|
||||
|
||||
# Features
|
||||
|
||||

|
||||

|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png">
|
||||
<img src="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png">
|
||||
</picture>
|
||||
|
||||
A full list of [features](https://docs.paperless-ngx.com/#features) and [screenshots](https://docs.paperless-ngx.com/#screenshots) are available in the [documentation](https://docs.paperless-ngx.com/).
|
||||
|
||||
# Getting started
|
||||
|
||||
The easiest way to deploy paperless is docker-compose. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from GitHub Packages.
|
||||
The easiest way to deploy paperless is `docker compose`. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from GitHub Packages.
|
||||
|
||||
If you'd like to jump right in, you can configure a docker-compose environment with our install script:
|
||||
If you'd like to jump right in, you can configure a `docker compose` environment with our install script:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||
|
@@ -1,8 +1,6 @@
|
||||
commit_message: '[ci skip]'
|
||||
pull_request_labels: [
|
||||
"skip-changelog",
|
||||
"translation"
|
||||
]
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /src/locale/en_US/LC_MESSAGES/django.po
|
||||
translation: /src/locale/%locale_with_underscore%/LC_MESSAGES/django.po
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless testing with actual gotenberg
|
||||
# Docker Compose file for running paperless testing with actual gotenberg
|
||||
# and Tika containers for a more end to end test of the Tika related functionality
|
||||
# Can be used locally or by the CI to start the nessecary containers with the
|
||||
# correct networking for the tests
|
||||
@@ -6,7 +6,7 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:7.10
|
||||
hostname: gotenberg
|
||||
container_name: gotenberg
|
||||
network_mode: host
|
||||
@@ -17,6 +17,8 @@ services:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
- "--log-level=warn"
|
||||
- "--log-format=text"
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
hostname: tika
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# docker compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), MariaDB is used as the database server.
|
||||
@@ -23,9 +23,9 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:7.10
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), MariaDB is used as the database server.
|
||||
@@ -19,9 +19,9 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8010.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the docker container registry.
|
||||
# Docker Compose file for running paperless from the docker container registry.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
@@ -23,9 +23,9 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
@@ -77,7 +77,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:7.10
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
@@ -19,9 +19,9 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the docker container registry.
|
||||
# Docker Compose file for running paperless from the docker container registry.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
# All compose files of paperless configure paperless in the following way:
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# SQLite is used as the database. The SQLite file is stored in the data volume.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Apache Tika and Gotenberg servers are started with paperless and paperless
|
||||
@@ -23,9 +23,9 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:7.10
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -16,9 +16,9 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
@@ -5,17 +5,19 @@
|
||||
Multiple options exist for making backups of your paperless instance,
|
||||
depending on how you installed paperless.
|
||||
|
||||
Before making backups, make sure that paperless is not running.
|
||||
Before making a backup, it's probably best to make sure that paperless is not actively
|
||||
consuming documents at that time.
|
||||
|
||||
Options available to any installation of paperless:
|
||||
|
||||
- Use the [document exporter](#exporter). The document exporter exports all your documents,
|
||||
thumbnails, metadata, and database contents to a specific folder. You may import your
|
||||
documents and settings into a fresh instance of paperless again or store your
|
||||
documents in another DMS with this export.
|
||||
- The document exporter is also able to update an already existing
|
||||
export. Therefore, incremental backups with `rsync` are entirely
|
||||
possible.
|
||||
- Use the [document exporter](#exporter). The document exporter exports all your documents,
|
||||
thumbnails, metadata, and database contents to a specific folder. You may import your
|
||||
documents and settings into a fresh instance of paperless again or store your
|
||||
documents in another DMS with this export.
|
||||
|
||||
The document exporter is also able to update an already existing
|
||||
export. Therefore, incremental backups with `rsync` are entirely
|
||||
possible.
|
||||
|
||||
!!! caution
|
||||
|
||||
@@ -25,31 +27,37 @@ Options available to any installation of paperless:
|
||||
|
||||
Options available to docker installations:
|
||||
|
||||
- Backup the docker volumes. These usually reside within
|
||||
`/var/lib/docker/volumes` on the host and you need to be root in
|
||||
order to access them.
|
||||
- Backup the docker volumes. These usually reside within
|
||||
`/var/lib/docker/volumes` on the host and you need to be root in
|
||||
order to access them.
|
||||
|
||||
Paperless uses 4 volumes:
|
||||
Paperless uses 4 volumes:
|
||||
|
||||
- `paperless_media`: This is where your documents are stored.
|
||||
- `paperless_data`: This is where auxillary data is stored. This
|
||||
folder also contains the SQLite database, if you use it.
|
||||
- `paperless_pgdata`: Exists only if you use PostgreSQL and
|
||||
contains the database.
|
||||
- `paperless_dbdata`: Exists only if you use MariaDB and contains
|
||||
the database.
|
||||
- `paperless_media`: This is where your documents are stored.
|
||||
- `paperless_data`: This is where auxiliary data is stored. This
|
||||
folder also contains the SQLite database, if you use it.
|
||||
- `paperless_pgdata`: Exists only if you use PostgreSQL and
|
||||
contains the database.
|
||||
- `paperless_dbdata`: Exists only if you use MariaDB and contains
|
||||
the database.
|
||||
|
||||
Options available to bare-metal and non-docker installations:
|
||||
|
||||
- Backup the entire paperless folder. This ensures that if your
|
||||
paperless instance crashes at some point or your disk fails, you can
|
||||
simply copy the folder back into place and it works.
|
||||
- Backup the entire paperless folder. This ensures that if your
|
||||
paperless instance crashes at some point or your disk fails, you can
|
||||
simply copy the folder back into place and it works.
|
||||
|
||||
When using PostgreSQL or MariaDB, you'll also have to backup the
|
||||
database.
|
||||
When using PostgreSQL or MariaDB, you'll also have to backup the
|
||||
database.
|
||||
|
||||
### Restoring {#migrating-restoring}
|
||||
|
||||
If you've backed-up Paperless-ngx using the [document exporter](#exporter),
|
||||
restoring can simply be done with the [document importer](#importer).
|
||||
|
||||
Of course, other backup strategies require restoring any volumes, folders and database
|
||||
copies you created in the steps above.
|
||||
|
||||
## Updating Paperless {#updating}
|
||||
|
||||
### Docker Route {#docker-updating}
|
||||
@@ -63,7 +71,7 @@ First of all, ensure that paperless is stopped.
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker-compose down
|
||||
$ docker compose down
|
||||
```
|
||||
|
||||
After that, [make a backup](#backup).
|
||||
@@ -71,22 +79,22 @@ After that, [make a backup](#backup).
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
|
||||
```shell-session
|
||||
$ docker-compose pull
|
||||
$ docker-compose up
|
||||
$ docker compose pull
|
||||
$ docker compose up
|
||||
```
|
||||
|
||||
The docker-compose files refer to the `latest` version, which is
|
||||
The Docker Compose files refer to the `latest` version, which is
|
||||
always the latest stable release.
|
||||
|
||||
1. If you built the image yourself, do the following:
|
||||
|
||||
```shell-session
|
||||
$ git pull
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
$ docker compose build
|
||||
$ docker compose up
|
||||
```
|
||||
|
||||
Running `docker-compose up` will also apply any new database migrations.
|
||||
Running `docker compose up` will also apply any new database migrations.
|
||||
If you see everything working, press CTRL+C once to gracefully stop
|
||||
paperless. Then you can start paperless-ngx with `-d` to have it run in
|
||||
the background.
|
||||
@@ -94,7 +102,7 @@ the background.
|
||||
!!! note
|
||||
|
||||
In version 0.9.14, the update process was changed. In 0.9.13 and
|
||||
earlier, the docker-compose files specified exact versions and pull
|
||||
earlier, the Docker Compose files specified exact versions and pull
|
||||
won't automatically update to newer versions. In order to enable
|
||||
updates as described above, either get the new `docker-compose.yml`
|
||||
file from
|
||||
@@ -212,11 +220,11 @@ Paperless comes with some management commands that perform various
|
||||
maintenance tasks on your paperless instance. You can invoke these
|
||||
commands in the following way:
|
||||
|
||||
With docker-compose, while paperless is running:
|
||||
With Docker Compose, while paperless is running:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker-compose exec webserver <command> <arguments>
|
||||
$ docker compose exec webserver <command> <arguments>
|
||||
```
|
||||
|
||||
With docker, while paperless is running:
|
||||
@@ -246,7 +254,7 @@ migration to another DMS.
|
||||
If you use the document exporter within a cronjob to backup your data
|
||||
you might use the `-T` flag behind exec to suppress "The input device
|
||||
is not a TTY" errors. For example:
|
||||
`docker-compose exec -T webserver document_exporter ../export`
|
||||
`docker compose exec -T webserver document_exporter ../export`
|
||||
|
||||
```
|
||||
document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z]
|
||||
@@ -400,7 +408,7 @@ that don't match a document anymore get removed as well.
|
||||
### Managing the Automatic matching algorithm
|
||||
|
||||
The _Auto_ matching algorithm requires a trained neural network to work.
|
||||
This network needs to be updated whenever somethings in your data
|
||||
This network needs to be updated whenever something in your data
|
||||
changes. The docker image takes care of that automatically with the task
|
||||
scheduler. You can manually renew the classifier by invoking the
|
||||
following management command:
|
||||
@@ -470,19 +478,19 @@ collection for issues.
|
||||
|
||||
The issues detected by the sanity checker are as follows:
|
||||
|
||||
- Missing original files.
|
||||
- Missing archive files.
|
||||
- Inaccessible original files due to improper permissions.
|
||||
- Inaccessible archive files due to improper permissions.
|
||||
- Corrupted original documents by comparing their checksum against
|
||||
what is stored in the database.
|
||||
- Corrupted archive documents by comparing their checksum against what
|
||||
is stored in the database.
|
||||
- Missing thumbnails.
|
||||
- Inaccessible thumbnails due to improper permissions.
|
||||
- Documents without any content (warning).
|
||||
- Orphaned files in the media directory (warning). These are files
|
||||
that are not referenced by any document in paperless.
|
||||
- Missing original files.
|
||||
- Missing archive files.
|
||||
- Inaccessible original files due to improper permissions.
|
||||
- Inaccessible archive files due to improper permissions.
|
||||
- Corrupted original documents by comparing their checksum against
|
||||
what is stored in the database.
|
||||
- Corrupted archive documents by comparing their checksum against what
|
||||
is stored in the database.
|
||||
- Missing thumbnails.
|
||||
- Inaccessible thumbnails due to improper permissions.
|
||||
- Documents without any content (warning).
|
||||
- Orphaned files in the media directory (warning). These are files
|
||||
that are not referenced by any document in paperless.
|
||||
|
||||
```
|
||||
document_sanity_checker
|
||||
@@ -589,7 +597,7 @@ This tool does a fuzzy match over document content, looking for
|
||||
those which look close according to a given ratio.
|
||||
|
||||
At this time, other metadata (such as correspondent or type) is not
|
||||
take into account by the detection.
|
||||
taken into account by the detection.
|
||||
|
||||
```
|
||||
document_fuzzy_match [--ratio] [--processes N]
|
||||
|
@@ -236,8 +236,8 @@ webserver:
|
||||
|
||||
Troubleshooting:
|
||||
|
||||
- Monitor the docker-compose log
|
||||
`cd ~/paperless-ngx; docker-compose logs -f`
|
||||
- Monitor the Docker Compose log
|
||||
`cd ~/paperless-ngx; docker compose logs -f`
|
||||
- Check your script's permission e.g. in case of permission error
|
||||
`sudo chmod 755 post-consumption-example.sh`
|
||||
- Pipe your scripts's output to a log file e.g.
|
||||
@@ -510,7 +510,7 @@ existing tables) with:
|
||||
|
||||
## Barcodes {#barcodes}
|
||||
|
||||
Paperless is able to utilize barcodes for automatically preforming some tasks.
|
||||
Paperless is able to utilize barcodes for automatically performing some tasks.
|
||||
|
||||
At this time, the library utilized for detection of barcodes supports the following types:
|
||||
|
||||
@@ -566,7 +566,7 @@ collating two separate scans into one document, reordering the pages as necessar
|
||||
|
||||
Suppose you have a double-sided document with 6 pages (3 sheets of paper). First,
|
||||
put the stack into your ADF as normal, ensuring that page 1 is scanned first. Your ADF
|
||||
will now scan pages 1, 3, and 5. Then you (or your the scanner, if it supports it) upload
|
||||
will now scan pages 1, 3, and 5. Then you (or your scanner, if it supports it) upload
|
||||
the scan into the correct sub-directory of the consume folder (`double-sided` by default;
|
||||
keep in mind that Paperless will _not_ automatically create the directory for you.)
|
||||
Paperless will then process the scan and move it into an internal staging area.
|
||||
|
@@ -21,6 +21,7 @@ The API provides the following main endpoints:
|
||||
- `/api/groups/`: Full CRUD support.
|
||||
- `/api/share_links/`: Full CRUD support.
|
||||
- `/api/custom_fields/`: Full CRUD support.
|
||||
- `/api/profile/`: GET, PATCH
|
||||
|
||||
All of these endpoints except for the logging endpoint allow you to
|
||||
fetch (and edit and delete where appropriate) individual objects by
|
||||
@@ -53,7 +54,7 @@ fields:
|
||||
- `set_permissions`: Allows setting document permissions. Optional,
|
||||
write-only. See [below](#permissions).
|
||||
- `custom_fields`: Array of custom fields & values, specified as
|
||||
{ field: CUSTOM_FIELD_ID, value: VALUE }
|
||||
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
|
||||
|
||||
## Downloading documents
|
||||
|
||||
@@ -157,6 +158,10 @@ The REST api provides three different forms of authentication.
|
||||
|
||||
3. Token authentication
|
||||
|
||||
You can create (or re-create) an API token by opening the "My Profile"
|
||||
link in the user dropdown found in the web UI and clicking the circular
|
||||
arrow button.
|
||||
|
||||
Paperless also offers an endpoint to acquire authentication tokens.
|
||||
|
||||
POST a username and password as a form or json string to
|
||||
@@ -168,7 +173,7 @@ The REST api provides three different forms of authentication.
|
||||
Authorization: Token <token>
|
||||
```
|
||||
|
||||
Tokens can be managed and revoked in the paperless admin.
|
||||
Tokens can also be managed in the Django admin.
|
||||
|
||||
## Searching for documents
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.7 MiB |
@@ -1,5 +1,440 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.1.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: sort consumption templates by order by default [@shamoon](https://github.com/shamoon) ([#4956](https://github.com/paperless-ngx/paperless-ngx/pull/4956))
|
||||
- Fix: Updates gotenberg-client, including workaround for Gotenberg non-latin handling [@stumpylog](https://github.com/stumpylog) ([#4944](https://github.com/paperless-ngx/paperless-ngx/pull/4944))
|
||||
- Fix: allow text copy in pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#4938](https://github.com/paperless-ngx/paperless-ngx/pull/4938))
|
||||
- Fix: Don't allow autocomplete searches to fail on schema field matches [@stumpylog](https://github.com/stumpylog) ([#4934](https://github.com/paperless-ngx/paperless-ngx/pull/4934))
|
||||
- Fix: Convert search dates to UTC in advanced search [@bogdal](https://github.com/bogdal) ([#4891](https://github.com/paperless-ngx/paperless-ngx/pull/4891))
|
||||
- Fix: Use the attachment filename so downstream template matching works [@stumpylog](https://github.com/stumpylog) ([#4931](https://github.com/paperless-ngx/paperless-ngx/pull/4931))
|
||||
- Fix: frontend handle autocomplete failure gracefully [@shamoon](https://github.com/shamoon) ([#4903](https://github.com/paperless-ngx/paperless-ngx/pull/4903))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps-dev): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#4942](https://github.com/paperless-ngx/paperless-ngx/pull/4942))
|
||||
- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#4939](https://github.com/paperless-ngx/paperless-ngx/pull/4939))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>9 changes</summary>
|
||||
|
||||
- Fix: sort consumption templates by order by default [@shamoon](https://github.com/shamoon) ([#4956](https://github.com/paperless-ngx/paperless-ngx/pull/4956))
|
||||
- Chore: reorganize api tests [@shamoon](https://github.com/shamoon) ([#4935](https://github.com/paperless-ngx/paperless-ngx/pull/4935))
|
||||
- Chore(deps-dev): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#4942](https://github.com/paperless-ngx/paperless-ngx/pull/4942))
|
||||
- Fix: allow text copy in pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#4938](https://github.com/paperless-ngx/paperless-ngx/pull/4938))
|
||||
- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#4939](https://github.com/paperless-ngx/paperless-ngx/pull/4939))
|
||||
- Fix: Don't allow autocomplete searches to fail on schema field matches [@stumpylog](https://github.com/stumpylog) ([#4934](https://github.com/paperless-ngx/paperless-ngx/pull/4934))
|
||||
- Fix: Convert search dates to UTC in advanced search [@bogdal](https://github.com/bogdal) ([#4891](https://github.com/paperless-ngx/paperless-ngx/pull/4891))
|
||||
- Fix: Use the attachment filename so downstream template matching works [@stumpylog](https://github.com/stumpylog) ([#4931](https://github.com/paperless-ngx/paperless-ngx/pull/4931))
|
||||
- Fix: frontend handle autocomplete failure gracefully [@shamoon](https://github.com/shamoon) ([#4903](https://github.com/paperless-ngx/paperless-ngx/pull/4903))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.1.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: disable toggle for share link creation without archive version, fix auto-copy in Safari [@shamoon](https://github.com/shamoon) ([#4885](https://github.com/paperless-ngx/paperless-ngx/pull/4885))
|
||||
- Fix: storage paths link incorrect in dashboard widget [@shamoon](https://github.com/shamoon) ([#4878](https://github.com/paperless-ngx/paperless-ngx/pull/4878))
|
||||
- Fix: respect baseURI for pdfjs worker URL [@shamoon](https://github.com/shamoon) ([#4865](https://github.com/paperless-ngx/paperless-ngx/pull/4865))
|
||||
- Fix: Allow users to configure the From email for password reset [@stumpylog](https://github.com/stumpylog) ([#4867](https://github.com/paperless-ngx/paperless-ngx/pull/4867))
|
||||
- Fix: dont show move icon for file tasks badge [@shamoon](https://github.com/shamoon) ([#4860](https://github.com/paperless-ngx/paperless-ngx/pull/4860))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Simplifies how the documentation site is deployed [@stumpylog](https://github.com/stumpylog) ([#4858](https://github.com/paperless-ngx/paperless-ngx/pull/4858))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Fix: disable toggle for share link creation without archive version, fix auto-copy in Safari [@shamoon](https://github.com/shamoon) ([#4885](https://github.com/paperless-ngx/paperless-ngx/pull/4885))
|
||||
- Fix: storage paths link incorrect in dashboard widget [@shamoon](https://github.com/shamoon) ([#4878](https://github.com/paperless-ngx/paperless-ngx/pull/4878))
|
||||
- Fix: respect baseURI for pdfjs worker URL [@shamoon](https://github.com/shamoon) ([#4865](https://github.com/paperless-ngx/paperless-ngx/pull/4865))
|
||||
- Fix: Allow users to configure the From email for password reset [@stumpylog](https://github.com/stumpylog) ([#4867](https://github.com/paperless-ngx/paperless-ngx/pull/4867))
|
||||
- Fix: dont show move icon for file tasks badge [@shamoon](https://github.com/shamoon) ([#4860](https://github.com/paperless-ngx/paperless-ngx/pull/4860))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.1.0
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: implement document link custom field [@shamoon](https://github.com/shamoon) ([#4799](https://github.com/paperless-ngx/paperless-ngx/pull/4799))
|
||||
- Feature: Adds additional warnings during an import if it might fail [@stumpylog](https://github.com/stumpylog) ([#4814](https://github.com/paperless-ngx/paperless-ngx/pull/4814))
|
||||
- Feature: pngx PDF viewer with updated pdfjs [@shamoon](https://github.com/shamoon) ([#4679](https://github.com/paperless-ngx/paperless-ngx/pull/4679))
|
||||
- Enhancement: support automatically assigning custom fields via consumption templates [@shamoon](https://github.com/shamoon) ([#4727](https://github.com/paperless-ngx/paperless-ngx/pull/4727))
|
||||
- Feature: update user profile [@shamoon](https://github.com/shamoon) ([#4678](https://github.com/paperless-ngx/paperless-ngx/pull/4678))
|
||||
- Enhancement: Allow excluding mail attachments by name [@stumpylog](https://github.com/stumpylog) ([#4691](https://github.com/paperless-ngx/paperless-ngx/pull/4691))
|
||||
- Enhancement: auto-refresh logs \& tasks [@shamoon](https://github.com/shamoon) ([#4680](https://github.com/paperless-ngx/paperless-ngx/pull/4680))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: welcome widget text color [@shamoon](https://github.com/shamoon) ([#4829](https://github.com/paperless-ngx/paperless-ngx/pull/4829))
|
||||
- Fix: export consumption templates \& custom fields in exporter [@shamoon](https://github.com/shamoon) ([#4825](https://github.com/paperless-ngx/paperless-ngx/pull/4825))
|
||||
- Fix: bulk edit object permissions should use permissions object [@shamoon](https://github.com/shamoon) ([#4797](https://github.com/paperless-ngx/paperless-ngx/pull/4797))
|
||||
- Fix: empty string for consumption template field should be interpreted as [@shamoon](https://github.com/shamoon) ([#4762](https://github.com/paperless-ngx/paperless-ngx/pull/4762))
|
||||
- Fix: use default permissions for objects created via dropdown [@shamoon](https://github.com/shamoon) ([#4778](https://github.com/paperless-ngx/paperless-ngx/pull/4778))
|
||||
- Fix: Alpha layer removal could allow duplicates [@stumpylog](https://github.com/stumpylog) ([#4781](https://github.com/paperless-ngx/paperless-ngx/pull/4781))
|
||||
- Fix: update checker broke in v2.0.0 [@shamoon](https://github.com/shamoon) ([#4773](https://github.com/paperless-ngx/paperless-ngx/pull/4773))
|
||||
- Fix: only show global drag-drop when files included [@shamoon](https://github.com/shamoon) ([#4767](https://github.com/paperless-ngx/paperless-ngx/pull/4767))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Enhancement: implement document link custom field [@shamoon](https://github.com/shamoon) ([#4799](https://github.com/paperless-ngx/paperless-ngx/pull/4799))
|
||||
- Fix: export consumption templates \& custom fields in exporter [@shamoon](https://github.com/shamoon) ([#4825](https://github.com/paperless-ngx/paperless-ngx/pull/4825))
|
||||
- Documentation: Fix typos [@omahs](https://github.com/omahs) ([#4737](https://github.com/paperless-ngx/paperless-ngx/pull/4737))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Bump the actions group with 2 updates [@dependabot](https://github.com/dependabot) ([#4745](https://github.com/paperless-ngx/paperless-ngx/pull/4745))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>7 changes</summary>
|
||||
|
||||
- Bump the development group with 6 updates [@dependabot](https://github.com/dependabot) ([#4838](https://github.com/paperless-ngx/paperless-ngx/pull/4838))
|
||||
- Bump the actions group with 2 updates [@dependabot](https://github.com/dependabot) ([#4745](https://github.com/paperless-ngx/paperless-ngx/pull/4745))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4756](https://github.com/paperless-ngx/paperless-ngx/pull/4756))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4744](https://github.com/paperless-ngx/paperless-ngx/pull/4744))
|
||||
- Bump [@<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot) ([#4749](https://github.com/paperless-ngx/paperless-ngx/pull/4749))
|
||||
- Bump wait-on from 7.0.1 to 7.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#4747](https://github.com/paperless-ngx/paperless-ngx/pull/4747))
|
||||
- Bump [@<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot) ([#4748](https://github.com/paperless-ngx/paperless-ngx/pull/4748))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>20 changes</summary>
|
||||
|
||||
- Enhancement: implement document link custom field [@shamoon](https://github.com/shamoon) ([#4799](https://github.com/paperless-ngx/paperless-ngx/pull/4799))
|
||||
- Bump the development group with 6 updates [@dependabot](https://github.com/dependabot) ([#4838](https://github.com/paperless-ngx/paperless-ngx/pull/4838))
|
||||
- Fix: welcome widget text color [@shamoon](https://github.com/shamoon) ([#4829](https://github.com/paperless-ngx/paperless-ngx/pull/4829))
|
||||
- Fix: export consumption templates \& custom fields in exporter [@shamoon](https://github.com/shamoon) ([#4825](https://github.com/paperless-ngx/paperless-ngx/pull/4825))
|
||||
- Feature: Adds additional warnings during an import if it might fail [@stumpylog](https://github.com/stumpylog) ([#4814](https://github.com/paperless-ngx/paperless-ngx/pull/4814))
|
||||
- Feature: pngx PDF viewer with updated pdfjs [@shamoon](https://github.com/shamoon) ([#4679](https://github.com/paperless-ngx/paperless-ngx/pull/4679))
|
||||
- Fix: bulk edit object permissions should use permissions object [@shamoon](https://github.com/shamoon) ([#4797](https://github.com/paperless-ngx/paperless-ngx/pull/4797))
|
||||
- Enhancement: support automatically assigning custom fields via consumption templates [@shamoon](https://github.com/shamoon) ([#4727](https://github.com/paperless-ngx/paperless-ngx/pull/4727))
|
||||
- Fix: empty string for consumption template field should be interpreted as [@shamoon](https://github.com/shamoon) ([#4762](https://github.com/paperless-ngx/paperless-ngx/pull/4762))
|
||||
- Fix: use default permissions for objects created via dropdown [@shamoon](https://github.com/shamoon) ([#4778](https://github.com/paperless-ngx/paperless-ngx/pull/4778))
|
||||
- Fix: Alpha layer removal could allow duplicates [@stumpylog](https://github.com/stumpylog) ([#4781](https://github.com/paperless-ngx/paperless-ngx/pull/4781))
|
||||
- Feature: update user profile [@shamoon](https://github.com/shamoon) ([#4678](https://github.com/paperless-ngx/paperless-ngx/pull/4678))
|
||||
- Fix: update checker broke in v2.0.0 [@shamoon](https://github.com/shamoon) ([#4773](https://github.com/paperless-ngx/paperless-ngx/pull/4773))
|
||||
- Fix: only show global drag-drop when files included [@shamoon](https://github.com/shamoon) ([#4767](https://github.com/paperless-ngx/paperless-ngx/pull/4767))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4756](https://github.com/paperless-ngx/paperless-ngx/pull/4756))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4744](https://github.com/paperless-ngx/paperless-ngx/pull/4744))
|
||||
- Bump [@<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot) ([#4749](https://github.com/paperless-ngx/paperless-ngx/pull/4749))
|
||||
- Bump wait-on from 7.0.1 to 7.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#4747](https://github.com/paperless-ngx/paperless-ngx/pull/4747))
|
||||
- Bump [@<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot) ([#4748](https://github.com/paperless-ngx/paperless-ngx/pull/4748))
|
||||
- Enhancement: auto-refresh logs \& tasks [@shamoon](https://github.com/shamoon) ([#4680](https://github.com/paperless-ngx/paperless-ngx/pull/4680))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.0.1
|
||||
|
||||
### Please Note
|
||||
|
||||
Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumption templates or custom fields, we recommend users upgrade to at least v2.1.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Increase field the length for consumption template source [@stumpylog](https://github.com/stumpylog) ([#4719](https://github.com/paperless-ngx/paperless-ngx/pull/4719))
|
||||
- Fix: Set RGB color conversion strategy for PDF outputs [@stumpylog](https://github.com/stumpylog) ([#4709](https://github.com/paperless-ngx/paperless-ngx/pull/4709))
|
||||
- Fix: Add a warning about a low image DPI which may cause OCR to fail [@stumpylog](https://github.com/stumpylog) ([#4708](https://github.com/paperless-ngx/paperless-ngx/pull/4708))
|
||||
- Fix: share links for URLs containing 'api' incorrect in dropdown [@shamoon](https://github.com/shamoon) ([#4701](https://github.com/paperless-ngx/paperless-ngx/pull/4701))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: Increase field the length for consumption template source [@stumpylog](https://github.com/stumpylog) ([#4719](https://github.com/paperless-ngx/paperless-ngx/pull/4719))
|
||||
- Fix: Set RGB color conversion strategy for PDF outputs [@stumpylog](https://github.com/stumpylog) ([#4709](https://github.com/paperless-ngx/paperless-ngx/pull/4709))
|
||||
- Fix: Add a warning about a low image DPI which may cause OCR to fail [@stumpylog](https://github.com/stumpylog) ([#4708](https://github.com/paperless-ngx/paperless-ngx/pull/4708))
|
||||
- Fix: share links for URLs containing 'api' incorrect in dropdown [@shamoon](https://github.com/shamoon) ([#4701](https://github.com/paperless-ngx/paperless-ngx/pull/4701))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.0.0
|
||||
|
||||
### Please Note
|
||||
|
||||
Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumption templates or custom fields, we recommend users upgrade to at least v2.1.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Breaking: Rename the environment variable for self-signed email certificates [@stumpylog](https://github.com/stumpylog) ([#4346](https://github.com/paperless-ngx/paperless-ngx/pull/4346))
|
||||
- Breaking: Drop support for Python 3.8 [@stumpylog](https://github.com/stumpylog) ([#4156](https://github.com/paperless-ngx/paperless-ngx/pull/4156))
|
||||
- Breaking: Remove ARMv7 building of the Docker image [@stumpylog](https://github.com/stumpylog) ([#3973](https://github.com/paperless-ngx/paperless-ngx/pull/3973))
|
||||
|
||||
### Notable Changes
|
||||
|
||||
- Feature: consumption templates [@shamoon](https://github.com/shamoon) ([#4196](https://github.com/paperless-ngx/paperless-ngx/pull/4196))
|
||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||
- Enhancement: Updates the underlying image to use Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4150](https://github.com/paperless-ngx/paperless-ngx/pull/4150))
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: compact notifications [@shamoon](https://github.com/shamoon) ([#4545](https://github.com/paperless-ngx/paperless-ngx/pull/4545))
|
||||
- Chore: Backend bulk updates [@stumpylog](https://github.com/stumpylog) ([#4509](https://github.com/paperless-ngx/paperless-ngx/pull/4509))
|
||||
- Feature: Hungarian translation [@shamoon](https://github.com/shamoon) ([#4552](https://github.com/paperless-ngx/paperless-ngx/pull/4552))
|
||||
- Chore: API support for id args for documents \& objects [@shamoon](https://github.com/shamoon) ([#4519](https://github.com/paperless-ngx/paperless-ngx/pull/4519))
|
||||
- Feature: Add Bulgarian translation [@shamoon](https://github.com/shamoon) ([#4470](https://github.com/paperless-ngx/paperless-ngx/pull/4470))
|
||||
- Feature: Audit Trail [@nanokatz](https://github.com/nanokatz) ([#4425](https://github.com/paperless-ngx/paperless-ngx/pull/4425))
|
||||
- Feature: Add ahead of time compression of the static files for x86_64 [@stumpylog](https://github.com/stumpylog) ([#4390](https://github.com/paperless-ngx/paperless-ngx/pull/4390))
|
||||
- Feature: sort sidebar views [@shamoon](https://github.com/shamoon) ([#4381](https://github.com/paperless-ngx/paperless-ngx/pull/4381))
|
||||
- Feature: Switches to a new client to handle communication with Gotenberg [@stumpylog](https://github.com/stumpylog) ([#4391](https://github.com/paperless-ngx/paperless-ngx/pull/4391))
|
||||
- barcode logic: strip non-numeric characters from detected ASN string [@queaker](https://github.com/queaker) ([#4379](https://github.com/paperless-ngx/paperless-ngx/pull/4379))
|
||||
- Feature: Include more updated base tools in Docker image [@stumpylog](https://github.com/stumpylog) ([#4319](https://github.com/paperless-ngx/paperless-ngx/pull/4319))
|
||||
- CI: speed-up frontend tests on ci [@shamoon](https://github.com/shamoon) ([#4316](https://github.com/paperless-ngx/paperless-ngx/pull/4316))
|
||||
- Feature: password reset [@shamoon](https://github.com/shamoon) ([#4289](https://github.com/paperless-ngx/paperless-ngx/pull/4289))
|
||||
- Enhancement: dashboard improvements, drag-n-drop reorder dashboard views [@shamoon](https://github.com/shamoon) ([#4252](https://github.com/paperless-ngx/paperless-ngx/pull/4252))
|
||||
- Feature: Updates Django to 4.2.5 [@stumpylog](https://github.com/stumpylog) ([#4278](https://github.com/paperless-ngx/paperless-ngx/pull/4278))
|
||||
- Enhancement: settings reorganization \& improvements, separate admin section [@shamoon](https://github.com/shamoon) ([#4251](https://github.com/paperless-ngx/paperless-ngx/pull/4251))
|
||||
- Feature: consumption templates [@shamoon](https://github.com/shamoon) ([#4196](https://github.com/paperless-ngx/paperless-ngx/pull/4196))
|
||||
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
|
||||
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
|
||||
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
||||
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
|
||||
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
|
||||
- Feature: New management command for fuzzy matching document content [@stumpylog](https://github.com/stumpylog) ([#4160](https://github.com/paperless-ngx/paperless-ngx/pull/4160))
|
||||
- Enhancement: Updates the underlying image to use Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4150](https://github.com/paperless-ngx/paperless-ngx/pull/4150))
|
||||
- Enhancement: frontend better handle slow backend requests [@shamoon](https://github.com/shamoon) ([#4055](https://github.com/paperless-ngx/paperless-ngx/pull/4055))
|
||||
- Chore: update docker image \& ci testing node to v18 [@shamoon](https://github.com/shamoon) ([#4149](https://github.com/paperless-ngx/paperless-ngx/pull/4149))
|
||||
- Enhancement: Improved error notifications [@shamoon](https://github.com/shamoon) ([#4062](https://github.com/paperless-ngx/paperless-ngx/pull/4062))
|
||||
- Feature: Official support for Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4146](https://github.com/paperless-ngx/paperless-ngx/pull/4146))
|
||||
- Enhancement: Add Afrikaans, Greek \& Norwegian languages [@shamoon](https://github.com/shamoon) ([#4088](https://github.com/paperless-ngx/paperless-ngx/pull/4088))
|
||||
- Enhancement: add task id to pre/post consume script as env [@andreheuer](https://github.com/andreheuer) ([#4037](https://github.com/paperless-ngx/paperless-ngx/pull/4037))
|
||||
- Enhancement: update bootstrap to v5.3.1 for backend static pages [@shamoon](https://github.com/shamoon) ([#4060](https://github.com/paperless-ngx/paperless-ngx/pull/4060))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Add missing spaces to help string in [@joouha](https://github.com/joouha) ([#4674](https://github.com/paperless-ngx/paperless-ngx/pull/4674))
|
||||
- Fix: Typo invalidates precondition for doctype, resulting in Exception [@ArminGruner](https://github.com/ArminGruner) ([#4668](https://github.com/paperless-ngx/paperless-ngx/pull/4668))
|
||||
- Fix: Miscellaneous visual fixes in v2.0.0-beta.rc1 2 [@shamoon](https://github.com/shamoon) ([#4635](https://github.com/paperless-ngx/paperless-ngx/pull/4635))
|
||||
- Fix: Delay consumption after MODIFY inotify events [@frozenbrain](https://github.com/frozenbrain) ([#4626](https://github.com/paperless-ngx/paperless-ngx/pull/4626))
|
||||
- Documentation: Add note that trash dir must exist [@shamoon](https://github.com/shamoon) ([#4608](https://github.com/paperless-ngx/paperless-ngx/pull/4608))
|
||||
- Fix: Miscellaneous v2.0 visual fixes [@shamoon](https://github.com/shamoon) ([#4576](https://github.com/paperless-ngx/paperless-ngx/pull/4576))
|
||||
- Fix: Force UTF-8 for exporter manifests and don't allow escaping [@stumpylog](https://github.com/stumpylog) ([#4574](https://github.com/paperless-ngx/paperless-ngx/pull/4574))
|
||||
- Fix: plain text preview overflows [@shamoon](https://github.com/shamoon) ([#4555](https://github.com/paperless-ngx/paperless-ngx/pull/4555))
|
||||
- Fix: add permissions for custom fields with migration [@shamoon](https://github.com/shamoon) ([#4513](https://github.com/paperless-ngx/paperless-ngx/pull/4513))
|
||||
- Fix: visually hidden text breaks delete button wrap [@shamoon](https://github.com/shamoon) ([#4462](https://github.com/paperless-ngx/paperless-ngx/pull/4462))
|
||||
- Fix: API statistics document_file_type_counts return type [@shamoon](https://github.com/shamoon) ([#4464](https://github.com/paperless-ngx/paperless-ngx/pull/4464))
|
||||
- Fix: Always return a list for audit log check [@shamoon](https://github.com/shamoon) ([#4463](https://github.com/paperless-ngx/paperless-ngx/pull/4463))
|
||||
- Fix: Only create a Correspondent if the email matches rule filters [@stumpylog](https://github.com/stumpylog) ([#4431](https://github.com/paperless-ngx/paperless-ngx/pull/4431))
|
||||
- Fix: Combination of consume template with recursive tagging [@stumpylog](https://github.com/stumpylog) ([#4442](https://github.com/paperless-ngx/paperless-ngx/pull/4442))
|
||||
- Fix: replace drag drop \& clipboard deps with angular cdk [@shamoon](https://github.com/shamoon) ([#4362](https://github.com/paperless-ngx/paperless-ngx/pull/4362))
|
||||
- Fix: update document modified time on note creation / deletion [@shamoon](https://github.com/shamoon) ([#4374](https://github.com/paperless-ngx/paperless-ngx/pull/4374))
|
||||
- Fix: Updates to latest imap_tools which includes fix for the meta charset in HTML content [@stumpylog](https://github.com/stumpylog) ([#4355](https://github.com/paperless-ngx/paperless-ngx/pull/4355))
|
||||
- Fix: Missing creation of a folder in Docker image [@stumpylog](https://github.com/stumpylog) ([#4347](https://github.com/paperless-ngx/paperless-ngx/pull/4347))
|
||||
- Fix: Retry Tika parsing when Tika returns HTTP 500 [@stumpylog](https://github.com/stumpylog) ([#4334](https://github.com/paperless-ngx/paperless-ngx/pull/4334))
|
||||
- Fix: get highest ASN regardless of user [@shamoon](https://github.com/shamoon) ([#4326](https://github.com/paperless-ngx/paperless-ngx/pull/4326))
|
||||
- Fix: Generate secret key with C locale and increase allowed characters [@stumpylog](https://github.com/stumpylog) ([#4277](https://github.com/paperless-ngx/paperless-ngx/pull/4277))
|
||||
- Fix: long notes cause visual overflow [@shamoon](https://github.com/shamoon) ([#4287](https://github.com/paperless-ngx/paperless-ngx/pull/4287))
|
||||
- Fix: Ensures all old connections are closed in certain long lived places [@stumpylog](https://github.com/stumpylog) ([#4265](https://github.com/paperless-ngx/paperless-ngx/pull/4265))
|
||||
- CI: fix playwright browser version mismatch failures [@shamoon](https://github.com/shamoon) ([#4239](https://github.com/paperless-ngx/paperless-ngx/pull/4239))
|
||||
- Fix: Set a non-zero polling internal when inotify cannot import [@stumpylog](https://github.com/stumpylog) ([#4230](https://github.com/paperless-ngx/paperless-ngx/pull/4230))
|
||||
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
|
||||
- Documentation: Fix fuzzy matching details [@stumpylog](https://github.com/stumpylog) ([#4207](https://github.com/paperless-ngx/paperless-ngx/pull/4207))
|
||||
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
|
||||
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
|
||||
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
|
||||
- Fix: Logs the errors during thumbnail generation [@stumpylog](https://github.com/stumpylog) ([#4171](https://github.com/paperless-ngx/paperless-ngx/pull/4171))
|
||||
- Fix: remove owner details from saved_views api endpoint [@shamoon](https://github.com/shamoon) ([#4158](https://github.com/paperless-ngx/paperless-ngx/pull/4158))
|
||||
- Fix: dashboard widget card borders hidden by bkgd color [@shamoon](https://github.com/shamoon) ([#4155](https://github.com/paperless-ngx/paperless-ngx/pull/4155))
|
||||
- Fix: hide entire add user / group buttons if insufficient permissions [@shamoon](https://github.com/shamoon) ([#4133](https://github.com/paperless-ngx/paperless-ngx/pull/4133))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation: Update documentation to refer only to Docker Compose v2 command [@stumpylog](https://github.com/stumpylog) ([#4650](https://github.com/paperless-ngx/paperless-ngx/pull/4650))
|
||||
- Documentation: fix typo, add to features list [@tooomm](https://github.com/tooomm) ([#4624](https://github.com/paperless-ngx/paperless-ngx/pull/4624))
|
||||
- Documentation: Add note that trash dir must exist [@shamoon](https://github.com/shamoon) ([#4608](https://github.com/paperless-ngx/paperless-ngx/pull/4608))
|
||||
- Documentation: Structure backup sections more clearly [@quantenProjects](https://github.com/quantenProjects) ([#4559](https://github.com/paperless-ngx/paperless-ngx/pull/4559))
|
||||
- Documentation: update docs, screenshots ahead of Paperless-ngx v2.0 [@shamoon](https://github.com/shamoon) ([#4542](https://github.com/paperless-ngx/paperless-ngx/pull/4542))
|
||||
- Chore: Cleanup command arguments and standardize process count handling [@stumpylog](https://github.com/stumpylog) ([#4541](https://github.com/paperless-ngx/paperless-ngx/pull/4541))
|
||||
- Add section for SELinux troubleshooting [@nachtjasmin](https://github.com/nachtjasmin) ([#4528](https://github.com/paperless-ngx/paperless-ngx/pull/4528))
|
||||
- Documentation: clarify document_exporter includes settings [@coaxial](https://github.com/coaxial) ([#4533](https://github.com/paperless-ngx/paperless-ngx/pull/4533))
|
||||
- Change: Install script improvements [@m-GDEV](https://github.com/m-GDEV) ([#4387](https://github.com/paperless-ngx/paperless-ngx/pull/4387))
|
||||
- Fix: update document modified time on note creation / deletion [@shamoon](https://github.com/shamoon) ([#4374](https://github.com/paperless-ngx/paperless-ngx/pull/4374))
|
||||
- Fix: correct set owner API location in docs, additional test [@shamoon](https://github.com/shamoon) ([#4366](https://github.com/paperless-ngx/paperless-ngx/pull/4366))
|
||||
- Documentation: Remove old information about building the Docker image locally [@stumpylog](https://github.com/stumpylog) ([#4354](https://github.com/paperless-ngx/paperless-ngx/pull/4354))
|
||||
- Documentation enhancement: add direct links for all config vars [@shamoon](https://github.com/shamoon) ([#4237](https://github.com/paperless-ngx/paperless-ngx/pull/4237))
|
||||
- Documentation: Fix fuzzy matching details [@stumpylog](https://github.com/stumpylog) ([#4207](https://github.com/paperless-ngx/paperless-ngx/pull/4207))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Backend bulk updates [@stumpylog](https://github.com/stumpylog) ([#4509](https://github.com/paperless-ngx/paperless-ngx/pull/4509))
|
||||
- Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#4476](https://github.com/paperless-ngx/paperless-ngx/pull/4476))
|
||||
- Feature: Add Bulgarian translation [@shamoon](https://github.com/shamoon) ([#4470](https://github.com/paperless-ngx/paperless-ngx/pull/4470))
|
||||
- Chore: Stop duplicated action runs against internal PRs [@stumpylog](https://github.com/stumpylog) ([#4430](https://github.com/paperless-ngx/paperless-ngx/pull/4430))
|
||||
- CI: separate frontend deps install [@shamoon](https://github.com/shamoon) ([#4336](https://github.com/paperless-ngx/paperless-ngx/pull/4336))
|
||||
- CI: speed-up frontend tests on ci [@shamoon](https://github.com/shamoon) ([#4316](https://github.com/paperless-ngx/paperless-ngx/pull/4316))
|
||||
- Fix: Generate secret key with C locale and increase allowed characters [@stumpylog](https://github.com/stumpylog) ([#4277](https://github.com/paperless-ngx/paperless-ngx/pull/4277))
|
||||
- Bump leonsteinhaeuser/project-beta-automations from 2.1.0 to 2.2.1 [@dependabot](https://github.com/dependabot) ([#4281](https://github.com/paperless-ngx/paperless-ngx/pull/4281))
|
||||
- Chore: Updates dependabot to group more dependencies [@stumpylog](https://github.com/stumpylog) ([#4280](https://github.com/paperless-ngx/paperless-ngx/pull/4280))
|
||||
- Change: update translation string for tasks dialog [@shamoon](https://github.com/shamoon) ([#4263](https://github.com/paperless-ngx/paperless-ngx/pull/4263))
|
||||
- CI: fix playwright browser version mismatch failures [@shamoon](https://github.com/shamoon) ([#4239](https://github.com/paperless-ngx/paperless-ngx/pull/4239))
|
||||
- Bump docker/login-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4221](https://github.com/paperless-ngx/paperless-ngx/pull/4221))
|
||||
- Bump docker/setup-buildx-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4220](https://github.com/paperless-ngx/paperless-ngx/pull/4220))
|
||||
- Bump docker/setup-qemu-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4211](https://github.com/paperless-ngx/paperless-ngx/pull/4211))
|
||||
- Bump stumpylog/image-cleaner-action from 0.2.0 to 0.3.0 [@dependabot](https://github.com/dependabot) ([#4210](https://github.com/paperless-ngx/paperless-ngx/pull/4210))
|
||||
- Bump docker/metadata-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4209](https://github.com/paperless-ngx/paperless-ngx/pull/4209))
|
||||
- Bump docker/build-push-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4212](https://github.com/paperless-ngx/paperless-ngx/pull/4212))
|
||||
- Bump actions/checkout from 3 to 4 [@dependabot](https://github.com/dependabot) ([#4208](https://github.com/paperless-ngx/paperless-ngx/pull/4208))
|
||||
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>39 changes</summary>
|
||||
|
||||
- Chore: Bulk update of Python dependencies [@stumpylog](https://github.com/stumpylog) ([#4688](https://github.com/paperless-ngx/paperless-ngx/pull/4688))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4479](https://github.com/paperless-ngx/paperless-ngx/pull/4479))
|
||||
- Bump [@<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot) ([#4480](https://github.com/paperless-ngx/paperless-ngx/pull/4480))
|
||||
- Bump concurrently from 8.2.1 to 8.2.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4481](https://github.com/paperless-ngx/paperless-ngx/pull/4481))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#4478](https://github.com/paperless-ngx/paperless-ngx/pull/4478))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 14 updates [@dependabot](https://github.com/dependabot) ([#4477](https://github.com/paperless-ngx/paperless-ngx/pull/4477))
|
||||
- Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#4476](https://github.com/paperless-ngx/paperless-ngx/pull/4476))
|
||||
- Bump [@<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot](https://github.com/<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot) ([#4389](https://github.com/paperless-ngx/paperless-ngx/pull/4389))
|
||||
- Fix: replace drag drop \& clipboard deps with angular cdk [@shamoon](https://github.com/shamoon) ([#4362](https://github.com/paperless-ngx/paperless-ngx/pull/4362))
|
||||
- Bump postcss from 8.4.12 to 8.4.31 in /src/paperless_mail/templates [@dependabot](https://github.com/dependabot) ([#4318](https://github.com/paperless-ngx/paperless-ngx/pull/4318))
|
||||
- Bump [@<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot) ([#4303](https://github.com/paperless-ngx/paperless-ngx/pull/4303))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 8 updates [@dependabot](https://github.com/dependabot) ([#4302](https://github.com/paperless-ngx/paperless-ngx/pull/4302))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4283](https://github.com/paperless-ngx/paperless-ngx/pull/4283))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#4282](https://github.com/paperless-ngx/paperless-ngx/pull/4282))
|
||||
- Bump [@<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot) ([#4284](https://github.com/paperless-ngx/paperless-ngx/pull/4284))
|
||||
- Bump leonsteinhaeuser/project-beta-automations from 2.1.0 to 2.2.1 [@dependabot](https://github.com/dependabot) ([#4281](https://github.com/paperless-ngx/paperless-ngx/pull/4281))
|
||||
- Bump zone.js from 0.13.1 to 0.13.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#4223](https://github.com/paperless-ngx/paperless-ngx/pull/4223))
|
||||
- Bump [@<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot) ([#4224](https://github.com/paperless-ngx/paperless-ngx/pull/4224))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4222](https://github.com/paperless-ngx/paperless-ngx/pull/4222))
|
||||
- Bump docker/login-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4221](https://github.com/paperless-ngx/paperless-ngx/pull/4221))
|
||||
- Bump docker/setup-buildx-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4220](https://github.com/paperless-ngx/paperless-ngx/pull/4220))
|
||||
- Bump docker/setup-qemu-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4211](https://github.com/paperless-ngx/paperless-ngx/pull/4211))
|
||||
- Bump bootstrap from 5.3.1 to 5.3.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4217](https://github.com/paperless-ngx/paperless-ngx/pull/4217))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4215](https://github.com/paperless-ngx/paperless-ngx/pull/4215))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4218](https://github.com/paperless-ngx/paperless-ngx/pull/4218))
|
||||
- Bump stumpylog/image-cleaner-action from 0.2.0 to 0.3.0 [@dependabot](https://github.com/dependabot) ([#4210](https://github.com/paperless-ngx/paperless-ngx/pull/4210))
|
||||
- Bump docker/metadata-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4209](https://github.com/paperless-ngx/paperless-ngx/pull/4209))
|
||||
- Bump uuid from 9.0.0 to 9.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4216](https://github.com/paperless-ngx/paperless-ngx/pull/4216))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#4213](https://github.com/paperless-ngx/paperless-ngx/pull/4213))
|
||||
- Bump docker/build-push-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4212](https://github.com/paperless-ngx/paperless-ngx/pull/4212))
|
||||
- Bump actions/checkout from 3 to 4 [@dependabot](https://github.com/dependabot) ([#4208](https://github.com/paperless-ngx/paperless-ngx/pull/4208))
|
||||
- Chore: update docker image \& ci testing node to v18 [@shamoon](https://github.com/shamoon) ([#4149](https://github.com/paperless-ngx/paperless-ngx/pull/4149))
|
||||
- Chore: Unlock dependencies \& update them all [@stumpylog](https://github.com/stumpylog) ([#4142](https://github.com/paperless-ngx/paperless-ngx/pull/4142))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4112](https://github.com/paperless-ngx/paperless-ngx/pull/4112))
|
||||
- Bump tslib from 2.6.1 to 2.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4108](https://github.com/paperless-ngx/paperless-ngx/pull/4108))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4106](https://github.com/paperless-ngx/paperless-ngx/pull/4106))
|
||||
- Bump concurrently from 8.2.0 to 8.2.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4111](https://github.com/paperless-ngx/paperless-ngx/pull/4111))
|
||||
- Bump [@<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot) ([#4110](https://github.com/paperless-ngx/paperless-ngx/pull/4110))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#4104](https://github.com/paperless-ngx/paperless-ngx/pull/4104))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>95 changes</summary>
|
||||
|
||||
- Fix: Add missing spaces to help string in [@joouha](https://github.com/joouha) ([#4674](https://github.com/paperless-ngx/paperless-ngx/pull/4674))
|
||||
- Fix: Typo invalidates precondition for doctype, resulting in Exception [@ArminGruner](https://github.com/ArminGruner) ([#4668](https://github.com/paperless-ngx/paperless-ngx/pull/4668))
|
||||
- Fix: dark mode inconsistencies in v2.0.0 beta.rc1 [@shamoon](https://github.com/shamoon) ([#4669](https://github.com/paperless-ngx/paperless-ngx/pull/4669))
|
||||
- Fix: dashboard saved view mobile width in v.2.0.0 beta.rc1 [@shamoon](https://github.com/shamoon) ([#4660](https://github.com/paperless-ngx/paperless-ngx/pull/4660))
|
||||
- Fix: Miscellaneous visual fixes in v2.0.0-beta.rc1 2 [@shamoon](https://github.com/shamoon) ([#4635](https://github.com/paperless-ngx/paperless-ngx/pull/4635))
|
||||
- Fix: Delay consumption after MODIFY inotify events [@frozenbrain](https://github.com/frozenbrain) ([#4626](https://github.com/paperless-ngx/paperless-ngx/pull/4626))
|
||||
- Fix: Import of split-manifests can fail [@stumpylog](https://github.com/stumpylog) ([#4623](https://github.com/paperless-ngx/paperless-ngx/pull/4623))
|
||||
- Fix: sidebar views dont update after creation in v2.0.0-beta.rc1 [@shamoon](https://github.com/shamoon) ([#4619](https://github.com/paperless-ngx/paperless-ngx/pull/4619))
|
||||
- Fix: Prevent text wrap on consumption template label [@shamoon](https://github.com/shamoon) ([#4616](https://github.com/paperless-ngx/paperless-ngx/pull/4616))
|
||||
- Fix: increase width of labels in default perms settings [@shamoon](https://github.com/shamoon) ([#4612](https://github.com/paperless-ngx/paperless-ngx/pull/4612))
|
||||
- Fix: note deletion fails in v2.0.0-beta.rc1 [@shamoon](https://github.com/shamoon) ([#4602](https://github.com/paperless-ngx/paperless-ngx/pull/4602))
|
||||
- Fix: Handle override lists being None [@stumpylog](https://github.com/stumpylog) ([#4598](https://github.com/paperless-ngx/paperless-ngx/pull/4598))
|
||||
- Fix: Miscellaneous v2.0 visual fixes [@shamoon](https://github.com/shamoon) ([#4576](https://github.com/paperless-ngx/paperless-ngx/pull/4576))
|
||||
- Fix: Force UTF-8 for exporter manifests and don't allow escaping [@stumpylog](https://github.com/stumpylog) ([#4574](https://github.com/paperless-ngx/paperless-ngx/pull/4574))
|
||||
- Feature: compact notifications [@shamoon](https://github.com/shamoon) ([#4545](https://github.com/paperless-ngx/paperless-ngx/pull/4545))
|
||||
- Chore: Backend bulk updates [@stumpylog](https://github.com/stumpylog) ([#4509](https://github.com/paperless-ngx/paperless-ngx/pull/4509))
|
||||
- Fix: plain text preview overflows [@shamoon](https://github.com/shamoon) ([#4555](https://github.com/paperless-ngx/paperless-ngx/pull/4555))
|
||||
- Feature: Hungarian translation [@shamoon](https://github.com/shamoon) ([#4552](https://github.com/paperless-ngx/paperless-ngx/pull/4552))
|
||||
- Chore: Cleanup command arguments and standardize process count handling [@stumpylog](https://github.com/stumpylog) ([#4541](https://github.com/paperless-ngx/paperless-ngx/pull/4541))
|
||||
- Chore: API support for id args for documents \& objects [@shamoon](https://github.com/shamoon) ([#4519](https://github.com/paperless-ngx/paperless-ngx/pull/4519))
|
||||
- Fix: add permissions for custom fields with migration [@shamoon](https://github.com/shamoon) ([#4513](https://github.com/paperless-ngx/paperless-ngx/pull/4513))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4479](https://github.com/paperless-ngx/paperless-ngx/pull/4479))
|
||||
- Bump [@<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot) ([#4480](https://github.com/paperless-ngx/paperless-ngx/pull/4480))
|
||||
- Bump concurrently from 8.2.1 to 8.2.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4481](https://github.com/paperless-ngx/paperless-ngx/pull/4481))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#4478](https://github.com/paperless-ngx/paperless-ngx/pull/4478))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 14 updates [@dependabot](https://github.com/dependabot) ([#4477](https://github.com/paperless-ngx/paperless-ngx/pull/4477))
|
||||
- Fix: visually hidden text breaks delete button wrap [@shamoon](https://github.com/shamoon) ([#4462](https://github.com/paperless-ngx/paperless-ngx/pull/4462))
|
||||
- Fix: API statistics document_file_type_counts return type [@shamoon](https://github.com/shamoon) ([#4464](https://github.com/paperless-ngx/paperless-ngx/pull/4464))
|
||||
- Fix: Always return a list for audit log check [@shamoon](https://github.com/shamoon) ([#4463](https://github.com/paperless-ngx/paperless-ngx/pull/4463))
|
||||
- Feature: Audit Trail [@nanokatz](https://github.com/nanokatz) ([#4425](https://github.com/paperless-ngx/paperless-ngx/pull/4425))
|
||||
- Fix: Only create a Correspondent if the email matches rule filters [@stumpylog](https://github.com/stumpylog) ([#4431](https://github.com/paperless-ngx/paperless-ngx/pull/4431))
|
||||
- Fix: Combination of consume template with recursive tagging [@stumpylog](https://github.com/stumpylog) ([#4442](https://github.com/paperless-ngx/paperless-ngx/pull/4442))
|
||||
- Feature: Add ahead of time compression of the static files for x86_64 [@stumpylog](https://github.com/stumpylog) ([#4390](https://github.com/paperless-ngx/paperless-ngx/pull/4390))
|
||||
- Feature: sort sidebar views [@shamoon](https://github.com/shamoon) ([#4381](https://github.com/paperless-ngx/paperless-ngx/pull/4381))
|
||||
- Feature: Switches to a new client to handle communication with Gotenberg [@stumpylog](https://github.com/stumpylog) ([#4391](https://github.com/paperless-ngx/paperless-ngx/pull/4391))
|
||||
- barcode logic: strip non-numeric characters from detected ASN string [@queaker](https://github.com/queaker) ([#4379](https://github.com/paperless-ngx/paperless-ngx/pull/4379))
|
||||
- Bump [@<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot](https://github.com/<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot) ([#4389](https://github.com/paperless-ngx/paperless-ngx/pull/4389))
|
||||
- Fix: replace drag drop \& clipboard deps with angular cdk [@shamoon](https://github.com/shamoon) ([#4362](https://github.com/paperless-ngx/paperless-ngx/pull/4362))
|
||||
- Fix: update document modified time on note creation / deletion [@shamoon](https://github.com/shamoon) ([#4374](https://github.com/paperless-ngx/paperless-ngx/pull/4374))
|
||||
- Fix: correct set owner API location in docs, additional test [@shamoon](https://github.com/shamoon) ([#4366](https://github.com/paperless-ngx/paperless-ngx/pull/4366))
|
||||
- Fix: get highest ASN regardless of user [@shamoon](https://github.com/shamoon) ([#4326](https://github.com/paperless-ngx/paperless-ngx/pull/4326))
|
||||
- Bump postcss from 8.4.12 to 8.4.31 in /src/paperless_mail/templates [@dependabot](https://github.com/dependabot) ([#4318](https://github.com/paperless-ngx/paperless-ngx/pull/4318))
|
||||
- CI: speed-up frontend tests on ci [@shamoon](https://github.com/shamoon) ([#4316](https://github.com/paperless-ngx/paperless-ngx/pull/4316))
|
||||
- Bump [@<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot) ([#4303](https://github.com/paperless-ngx/paperless-ngx/pull/4303))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 8 updates [@dependabot](https://github.com/dependabot) ([#4302](https://github.com/paperless-ngx/paperless-ngx/pull/4302))
|
||||
- Feature: password reset [@shamoon](https://github.com/shamoon) ([#4289](https://github.com/paperless-ngx/paperless-ngx/pull/4289))
|
||||
- Enhancement: dashboard improvements, drag-n-drop reorder dashboard views [@shamoon](https://github.com/shamoon) ([#4252](https://github.com/paperless-ngx/paperless-ngx/pull/4252))
|
||||
- Fix: long notes cause visual overflow [@shamoon](https://github.com/shamoon) ([#4287](https://github.com/paperless-ngx/paperless-ngx/pull/4287))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4283](https://github.com/paperless-ngx/paperless-ngx/pull/4283))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#4282](https://github.com/paperless-ngx/paperless-ngx/pull/4282))
|
||||
- Bump [@<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot) ([#4284](https://github.com/paperless-ngx/paperless-ngx/pull/4284))
|
||||
- Fix: Ensures all old connections are closed in certain long lived places [@stumpylog](https://github.com/stumpylog) ([#4265](https://github.com/paperless-ngx/paperless-ngx/pull/4265))
|
||||
- Change: update translation string for tasks dialog [@shamoon](https://github.com/shamoon) ([#4263](https://github.com/paperless-ngx/paperless-ngx/pull/4263))
|
||||
- Enhancement: settings reorganization \& improvements, separate admin section [@shamoon](https://github.com/shamoon) ([#4251](https://github.com/paperless-ngx/paperless-ngx/pull/4251))
|
||||
- Chore: Standardizes the imports across all the files and modules [@stumpylog](https://github.com/stumpylog) ([#4248](https://github.com/paperless-ngx/paperless-ngx/pull/4248))
|
||||
- Feature: consumption templates [@shamoon](https://github.com/shamoon) ([#4196](https://github.com/paperless-ngx/paperless-ngx/pull/4196))
|
||||
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
|
||||
- Fix: Set a non-zero polling internal when inotify cannot import [@stumpylog](https://github.com/stumpylog) ([#4230](https://github.com/paperless-ngx/paperless-ngx/pull/4230))
|
||||
- Bump zone.js from 0.13.1 to 0.13.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#4223](https://github.com/paperless-ngx/paperless-ngx/pull/4223))
|
||||
- Bump [@<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot) ([#4224](https://github.com/paperless-ngx/paperless-ngx/pull/4224))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4222](https://github.com/paperless-ngx/paperless-ngx/pull/4222))
|
||||
- Bump bootstrap from 5.3.1 to 5.3.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4217](https://github.com/paperless-ngx/paperless-ngx/pull/4217))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4215](https://github.com/paperless-ngx/paperless-ngx/pull/4215))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4218](https://github.com/paperless-ngx/paperless-ngx/pull/4218))
|
||||
- Bump uuid from 9.0.0 to 9.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4216](https://github.com/paperless-ngx/paperless-ngx/pull/4216))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#4213](https://github.com/paperless-ngx/paperless-ngx/pull/4213))
|
||||
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
||||
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
|
||||
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
|
||||
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
|
||||
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
|
||||
- Fix: Logs the errors during thumbnail generation [@stumpylog](https://github.com/stumpylog) ([#4171](https://github.com/paperless-ngx/paperless-ngx/pull/4171))
|
||||
- Feature: New management command for fuzzy matching document content [@stumpylog](https://github.com/stumpylog) ([#4160](https://github.com/paperless-ngx/paperless-ngx/pull/4160))
|
||||
- Breaking: Drop support for Python 3.8 [@stumpylog](https://github.com/stumpylog) ([#4156](https://github.com/paperless-ngx/paperless-ngx/pull/4156))
|
||||
- Fix: dashboard widget card borders hidden by bkgd color [@shamoon](https://github.com/shamoon) ([#4155](https://github.com/paperless-ngx/paperless-ngx/pull/4155))
|
||||
- Enhancement: frontend better handle slow backend requests [@shamoon](https://github.com/shamoon) ([#4055](https://github.com/paperless-ngx/paperless-ngx/pull/4055))
|
||||
- Chore: Extend the live service utility for handling 503 errors [@stumpylog](https://github.com/stumpylog) ([#4143](https://github.com/paperless-ngx/paperless-ngx/pull/4143))
|
||||
- Chore: update docker image \& ci testing node to v18 [@shamoon](https://github.com/shamoon) ([#4149](https://github.com/paperless-ngx/paperless-ngx/pull/4149))
|
||||
- Fix: hide entire add user / group buttons if insufficient permissions [@shamoon](https://github.com/shamoon) ([#4133](https://github.com/paperless-ngx/paperless-ngx/pull/4133))
|
||||
- Enhancement: Improved error notifications [@shamoon](https://github.com/shamoon) ([#4062](https://github.com/paperless-ngx/paperless-ngx/pull/4062))
|
||||
- Feature: Official support for Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4146](https://github.com/paperless-ngx/paperless-ngx/pull/4146))
|
||||
- Chore: Unlock dependencies \& update them all [@stumpylog](https://github.com/stumpylog) ([#4142](https://github.com/paperless-ngx/paperless-ngx/pull/4142))
|
||||
- Change: PWA Manifest to Standalone Display [@swoga](https://github.com/swoga) ([#4129](https://github.com/paperless-ngx/paperless-ngx/pull/4129))
|
||||
- Enhancement: add --id-range for document_retagger [@kamilkosek](https://github.com/kamilkosek) ([#4080](https://github.com/paperless-ngx/paperless-ngx/pull/4080))
|
||||
- Enhancement: Add Afrikaans, Greek \& Norwegian languages [@shamoon](https://github.com/shamoon) ([#4088](https://github.com/paperless-ngx/paperless-ngx/pull/4088))
|
||||
- Enhancement: add task id to pre/post consume script as env [@andreheuer](https://github.com/andreheuer) ([#4037](https://github.com/paperless-ngx/paperless-ngx/pull/4037))
|
||||
- Enhancement: update bootstrap to v5.3.1 for backend static pages [@shamoon](https://github.com/shamoon) ([#4060](https://github.com/paperless-ngx/paperless-ngx/pull/4060))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4112](https://github.com/paperless-ngx/paperless-ngx/pull/4112))
|
||||
- Bump tslib from 2.6.1 to 2.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4108](https://github.com/paperless-ngx/paperless-ngx/pull/4108))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4106](https://github.com/paperless-ngx/paperless-ngx/pull/4106))
|
||||
- Bump concurrently from 8.2.0 to 8.2.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4111](https://github.com/paperless-ngx/paperless-ngx/pull/4111))
|
||||
- Bump [@<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot) ([#4110](https://github.com/paperless-ngx/paperless-ngx/pull/4110))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#4104](https://github.com/paperless-ngx/paperless-ngx/pull/4104))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.17.4
|
||||
|
||||
### Bug Fixes
|
||||
|
@@ -181,7 +181,7 @@ configure their endpoints, and enable the feature.
|
||||
Defaults to "<http://localhost:3000>".
|
||||
|
||||
If you run paperless on docker, you can add those services to the
|
||||
docker-compose file (see the provided
|
||||
Docker Compose file (see the provided
|
||||
[`docker-compose.sqlite-tika.yml`](https://github.com/paperless-ngx/paperless-ngx/blob/main/docker/compose/docker-compose.sqlite-tika.yml)
|
||||
file for reference).
|
||||
|
||||
@@ -221,6 +221,8 @@ directory.
|
||||
inside docker, ensure that this path is within a permanent volume
|
||||
(such as "../media/trash") so it won't get lost on upgrades.
|
||||
|
||||
Note that the directory must exist prior to using this setting.
|
||||
|
||||
Defaults to empty (i.e. really delete documents).
|
||||
|
||||
#### [`PAPERLESS_MEDIA_ROOT=<path>`](#PAPERLESS_MEDIA_ROOT) {#PAPERLESS_MEDIA_ROOT}
|
||||
@@ -702,6 +704,20 @@ but could result in missing text content.
|
||||
this value if you are certain your documents are not malicious and
|
||||
you need the text which was not OCRed
|
||||
|
||||
#### [`PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY=<RGB>`](#PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY) {#PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY}
|
||||
|
||||
: Controls the Ghostscript color conversion strategy when creating the archive file. This setting
|
||||
will only be utilized if the output is a version of PDF/A.
|
||||
|
||||
Valid options are CMYK, Gray, LeaveColorUnchanged, RGB or UseDeviceIndependentColor.
|
||||
|
||||
You can find more on the settings [here](https://ghostscript.readthedocs.io/en/latest/VectorDevices.html#color-conversion-and-management) in the Ghostscript documentation.
|
||||
|
||||
!!! warning
|
||||
|
||||
Utilizing some of the options may result in errors when creating archive
|
||||
files from PDFs.
|
||||
|
||||
#### [`PAPERLESS_OCR_USER_ARGS=<json>`](#PAPERLESS_OCR_USER_ARGS) {#PAPERLESS_OCR_USER_ARGS}
|
||||
|
||||
: OCRmyPDF offers many more options. Use this parameter to specify any
|
||||
@@ -717,7 +733,7 @@ they use underscores instead of dashes.
|
||||
Paperless has been tested to work with the OCR options provided
|
||||
above. There are many options that are incompatible with each other,
|
||||
so specifying invalid options may prevent paperless from consuming
|
||||
any documents.
|
||||
any documents. Use with caution!
|
||||
|
||||
Specify arguments as a JSON dictionary. Keep note of lower case
|
||||
booleans and double quoted parameter names and strings. Examples:
|
||||
@@ -1329,6 +1345,10 @@ password. All of these options come from their similarly-named [Django settings]
|
||||
|
||||
: Defaults to ''.
|
||||
|
||||
#### [`PAPERLESS_EMAIL_FROM=<str>`](#PAPERLESS_EMAIL_FROM) {#PAPERLESS_EMAIL_FROM}
|
||||
|
||||
: Defaults to PAPERLESS_EMAIL_HOST_USER if not set.
|
||||
|
||||
#### [`PAPERLESS_EMAIL_HOST_PASSWORD=<str>`](#PAPERLESS_EMAIL_HOST_PASSWORD) {#PAPERLESS_EMAIL_HOST_PASSWORD}
|
||||
|
||||
: Defaults to ''.
|
||||
|
@@ -9,7 +9,7 @@ following way:
|
||||
- `main` always represents the latest release and will only see
|
||||
changes when a new release is made.
|
||||
- `dev` contains the code that will be in the next release.
|
||||
- `feature-X` contain bigger changes that will be in some release, but
|
||||
- `feature-X` contains bigger changes that will be in some release, but
|
||||
not necessarily the next one.
|
||||
|
||||
When making functional changes to Paperless-ngx, _always_ make your changes
|
||||
|
@@ -83,11 +83,11 @@ has to do much less work to serve the data.
|
||||
## _How do I install paperless-ngx on Raspberry Pi?_
|
||||
|
||||
**A:** Docker images are available for arm64 hardware, so just
|
||||
follow the [docker-compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
|
||||
follow the [Docker Compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
|
||||
space compared to a bare metal installation, docker comes with close to
|
||||
zero overhead, even on Raspberry Pi.
|
||||
|
||||
If you decide to got with the bare metal route, be aware that some of
|
||||
If you decide to go with the bare metal route, be aware that some of
|
||||
the python requirements do not have precompiled packages for ARM /
|
||||
ARM64. Installation of these will require additional development
|
||||
libraries and compilation will take a long time.
|
||||
|
@@ -18,7 +18,9 @@ physical documents into a searchable online archive so you can keep, well, _less
|
||||
## Features
|
||||
|
||||
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
|
||||
- Performs **OCR** on your documents, adding selectable text to image-only documents.
|
||||
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
|
||||
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
|
||||
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
|
||||
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
|
||||
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents)[^1] and more.
|
||||
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents.
|
||||
|
@@ -25,7 +25,10 @@ necessary configuration files, pull the docker image, start paperless
|
||||
and create your user account. This script essentially performs all the
|
||||
steps described in [Docker setup](#docker_hub) automatically.
|
||||
|
||||
1. Make sure that docker and docker-compose are installed.
|
||||
1. Make sure that Docker and Docker Compose are installed.
|
||||
|
||||
!!! tip
|
||||
See the Docker installation instructions at https://docs.docker.com/engine/install/
|
||||
|
||||
2. Download and run the installation script:
|
||||
|
||||
@@ -62,19 +65,19 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
For new installations, it is recommended to use PostgreSQL as the
|
||||
database backend.
|
||||
|
||||
3. Install [Docker](https://www.docker.com/) and
|
||||
[docker-compose](https://docs.docker.com/compose/install/).
|
||||
3. Install [Docker](https://docs.docker.com/engine/install/) and
|
||||
[Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
!!! warning
|
||||
|
||||
If you want to use the included `docker-compose.*.yml` file, you
|
||||
need to have at least Docker version **17.09.0** and docker-compose
|
||||
version **1.17.0**. To check do: `docker-compose -v` or `docker -v`
|
||||
need to have at least Docker version **17.09.0** and Docker Compose
|
||||
version **v2**. To check do: `docker compose -v` or `docker -v`
|
||||
|
||||
See the [Docker installation guide](https://docs.docker.com/engine/install/) on how to install the current
|
||||
version of Docker for your operating system or Linux distribution of
|
||||
choice. To get the latest version of docker-compose, follow the
|
||||
[docker-compose installation guide](https://docs.docker.com/compose/install/linux/) if your package repository
|
||||
choice. To get the latest version of Docker Compose, follow the
|
||||
[Docker Compose installation guide](https://docs.docker.com/compose/install/linux/) if your package repository
|
||||
doesn't include it.
|
||||
|
||||
4. Modify `docker-compose.yml` to your preferences. You may want to
|
||||
@@ -165,13 +168,13 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
[`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See
|
||||
[here](configuration.md#polling).
|
||||
|
||||
6. Run `docker-compose pull`. This will pull the image.
|
||||
6. Run `docker compose pull`. This will pull the image.
|
||||
|
||||
7. To be able to login, you will need a super user. To create it,
|
||||
execute the following command:
|
||||
|
||||
```shell-session
|
||||
$ docker-compose run --rm webserver createsuperuser
|
||||
$ docker compose run --rm webserver createsuperuser
|
||||
```
|
||||
|
||||
or using docker exec from within the container:
|
||||
@@ -183,7 +186,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
This will prompt you to set a username, an optional e-mail address
|
||||
and finally a password (at least 8 characters).
|
||||
|
||||
8. Run `docker-compose up -d`. This will create and start the necessary containers.
|
||||
8. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||
|
||||
9. The default `docker-compose.yml` exports the webserver on your local
|
||||
port
|
||||
@@ -209,14 +212,14 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
root as well.
|
||||
|
||||
3. In the `docker-compose.yml` file, find the line that instructs
|
||||
docker-compose to pull the paperless image from Docker Hub:
|
||||
Docker Compose to pull the paperless image from Docker Hub:
|
||||
|
||||
```yaml
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
```
|
||||
|
||||
and replace it with a line that instructs docker-compose to build
|
||||
and replace it with a line that instructs Docker Compose to build
|
||||
the image from the current working directory instead:
|
||||
|
||||
```yaml
|
||||
@@ -226,10 +229,10 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
```
|
||||
|
||||
4. Follow steps 3 to 8 of [Docker Setup](#docker_hub). When asked to run
|
||||
`docker-compose pull` to pull the image, do
|
||||
`docker compose pull` to pull the image, do
|
||||
|
||||
```shell-session
|
||||
$ docker-compose build
|
||||
$ docker compose build
|
||||
```
|
||||
|
||||
instead to build the image.
|
||||
@@ -541,7 +544,7 @@ to
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
```
|
||||
|
||||
and then run `docker-compose up -d` which will pull the new image
|
||||
and then run `docker compose up -d` which will pull the new image
|
||||
recreate the container. That's it!
|
||||
|
||||
Users who installed with the bare-metal route should also update their
|
||||
@@ -570,7 +573,7 @@ installation. The important things to keep in mind are as follows:
|
||||
- The task scheduler of paperless, which is used to execute periodic
|
||||
tasks such as email checking and maintenance, requires a
|
||||
[redis](https://redis.io/) message broker instance. The
|
||||
docker-compose route takes care of that.
|
||||
Docker Compose route takes care of that.
|
||||
- The layout of the folder structure for your documents and data
|
||||
remains the same, so you can just plug your old docker volumes into
|
||||
paperless-ngx and expect it to find everything where it should be.
|
||||
@@ -581,7 +584,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
|
||||
```bash
|
||||
$ cd /path/to/current/paperless
|
||||
$ docker-compose down
|
||||
$ docker compose down
|
||||
```
|
||||
|
||||
2. Do a backup for two purposes: If something goes wrong, you still
|
||||
@@ -589,7 +592,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
switch back to paperless.
|
||||
|
||||
3. Download the latest release of paperless-ngx. You can either go with
|
||||
the docker-compose files from
|
||||
the Docker Compose files from
|
||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||
or clone the repository to build the image yourself (see
|
||||
[above](#docker_build)). You can
|
||||
@@ -626,7 +629,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
the search index:
|
||||
|
||||
```shell-session
|
||||
$ docker-compose run --rm webserver document_index reindex
|
||||
$ docker compose run --rm webserver document_index reindex
|
||||
```
|
||||
|
||||
This will migrate your database and create the search index. After
|
||||
@@ -635,7 +638,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
8. Start paperless-ngx.
|
||||
|
||||
```bash
|
||||
$ docker-compose up -d
|
||||
$ docker compose up -d
|
||||
```
|
||||
|
||||
This will run paperless in the background and automatically start it
|
||||
@@ -679,7 +682,7 @@ commands as well.
|
||||
8. Modify the `image:` to point to
|
||||
`ghcr.io/paperless-ngx/paperless-ngx:latest` or a specific version
|
||||
if preferred.
|
||||
9. Start the containers as before, using `docker-compose`.
|
||||
9. Start the containers as before, using `docker compose`.
|
||||
|
||||
## Moving data from SQLite to PostgreSQL or MySQL/MariaDB {#sqlite_to_psql}
|
||||
|
||||
@@ -737,7 +740,7 @@ below use PostgreSQL, but are applicable to MySQL/MariaDB with the
|
||||
|
||||
``` shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker-compose run --rm webserver /bin/bash
|
||||
$ docker compose run --rm webserver /bin/bash
|
||||
```
|
||||
|
||||
This will launch the container and initialize the PostgreSQL
|
||||
@@ -790,7 +793,7 @@ Execute this:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker-compose run --rm webserver migrate documents 0023
|
||||
$ docker compose run --rm webserver migrate documents 0023
|
||||
```
|
||||
|
||||
Or without docker:
|
||||
|
@@ -6,7 +6,7 @@ Check for the following issues:
|
||||
|
||||
- Ensure that the directory you're putting your documents in is the
|
||||
folder paperless is watching. With docker, this setting is performed
|
||||
in the `docker-compose.yml` file. Without docker, look at the
|
||||
in the `docker-compose.yml` file. Without Docker, look at the
|
||||
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
|
||||
using docker.
|
||||
|
||||
@@ -120,7 +120,7 @@ Gotenberg raises this error.
|
||||
|
||||
You can increase the timeout by configuring a command flag for Gotenberg
|
||||
(see also [here](https://gotenberg.dev/docs/modules/api#properties)). If
|
||||
using docker-compose, this is achieved by the following configuration
|
||||
using Docker Compose, this is achieved by the following configuration
|
||||
change in the `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
|
@@ -279,10 +279,11 @@ Consumption templates allow you to filter by:
|
||||
|
||||
Consumption templates can assign:
|
||||
|
||||
- Title, see [title placeholders](usage.md#title_placeholders) below
|
||||
- Title, see [title placeholders](usage.md#title-placeholders) below
|
||||
- Tags, correspondent, document types
|
||||
- Document owner
|
||||
- View and / or edit permissions to users or groups
|
||||
- Custom fields. Note that no value for the field will be set
|
||||
|
||||
### Consumption template permissions
|
||||
|
||||
@@ -342,6 +343,7 @@ The following custom field types are supported:
|
||||
- `Integer`: integer number e.g. 12
|
||||
- `Number`: float number e.g. 12.3456
|
||||
- `Monetary`: float number with exactly two decimals, e.g. 12.30
|
||||
- `Document Link`: reference(s) to other document(s), displayed as links
|
||||
|
||||
## Share Links
|
||||
|
||||
|
@@ -56,14 +56,9 @@ if ! command -v docker &> /dev/null ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
if ! command -v ${DOCKER_COMPOSE_CMD} &> /dev/null ; then
|
||||
if docker compose version &> /dev/null ; then
|
||||
DOCKER_COMPOSE_CMD="docker compose"
|
||||
else
|
||||
echo "docker-compose executable not found. Is docker-compose installed?"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v docker compose &> /dev/null ; then
|
||||
echo "docker compose executable not found. Is docker compose installed?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
|
||||
@@ -382,16 +377,16 @@ if [ "$l1" -eq "$l2" ] ; then
|
||||
fi
|
||||
|
||||
|
||||
${DOCKER_COMPOSE_CMD} pull
|
||||
docker compose pull
|
||||
|
||||
if [ "$DATABASE_BACKEND" == "postgres" ] || [ "$DATABASE_BACKEND" == "mariadb" ] ; then
|
||||
echo "Starting DB first for initilzation"
|
||||
${DOCKER_COMPOSE_CMD} up --detach db
|
||||
echo "Starting DB first for initialization"
|
||||
docker compose up --detach db
|
||||
# hopefully enough time for even the slower systems
|
||||
sleep 15
|
||||
${DOCKER_COMPOSE_CMD} stop
|
||||
docker compose stop
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
|
||||
docker compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
|
||||
|
||||
${DOCKER_COMPOSE_CMD} up --detach
|
||||
docker compose up --detach
|
||||
|
@@ -67,4 +67,5 @@ extra:
|
||||
- icon: material/chat
|
||||
link: https://matrix.to/#/#paperless:matrix.org
|
||||
plugins:
|
||||
- search
|
||||
- glightbox
|
||||
|
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
"projects/**/*",
|
||||
"/src/app/components/common/pdf-viewer/**"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
|
@@ -65,7 +65,7 @@
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "pdf.worker.min.js",
|
||||
"glob": "{pdf.worker.min.js,pdf.min.js}",
|
||||
"input": "node_modules/pdfjs-dist/build/",
|
||||
"output": "/assets/js/"
|
||||
}
|
||||
@@ -75,7 +75,8 @@
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ng2-pdf-viewer"
|
||||
"pdfjs-dist",
|
||||
"pdfjs-dist/web/pdf_viewer"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
@@ -109,7 +110,7 @@
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
"maximumError": "30kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@@ -79,7 +79,7 @@ test('should show a mobile preview', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 400, height: 1000 })
|
||||
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||
await page.waitForSelector('pdf-viewer')
|
||||
await page.waitForSelector('pngx-pdf-viewer')
|
||||
})
|
||||
|
||||
test('should show a list of notes', async ({ page }) => {
|
||||
|
@@ -7,6 +7,7 @@ module.exports = {
|
||||
'abstract-name-filter-service',
|
||||
'abstract-paperless-service',
|
||||
],
|
||||
coveragePathIgnorePatterns: ['/src/app/components/common/pdf-viewer/*'],
|
||||
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)': '<rootDir>/src/$1',
|
||||
|
588
src-ui/package-lock.json
generated
@@ -27,11 +27,11 @@
|
||||
"bootstrap": "^5.3.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^10.0.0",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^16.0.1",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^13.0.6",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
@@ -47,20 +47,20 @@
|
||||
"@angular-eslint/template-parser": "16.2.0",
|
||||
"@angular/cli": "~16.2.9",
|
||||
"@angular/compiler-cli": "~16.2.3",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@types/jest": "^29.5.7",
|
||||
"@types/node": "^20.8.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/node": "^20.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||
"@typescript-eslint/parser": "^6.13.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint": "^8.55.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^13.1.1",
|
||||
"jest-preset-angular": "^13.1.4",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wait-on": "^7.0.1"
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
|
@@ -33,8 +33,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private renderer: Renderer2,
|
||||
private permissionsService: PermissionsService
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||
this.settings.updateAppearanceSettings()
|
||||
}
|
||||
|
||||
|
@@ -51,7 +51,6 @@ import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-v
|
||||
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
|
||||
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
|
||||
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
|
||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
|
||||
import { YesNoPipe } from './pipes/yes-no.pipe'
|
||||
import { FileSizePipe } from './pipes/file-size.pipe'
|
||||
@@ -105,6 +104,9 @@ import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component'
|
||||
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
|
||||
|
||||
import localeAf from '@angular/common/locales/af'
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
@@ -256,6 +258,9 @@ function initializeApp(settings: SettingsService) {
|
||||
CustomFieldsComponent,
|
||||
CustomFieldEditDialogComponent,
|
||||
CustomFieldsDropdownComponent,
|
||||
ProfileEditDialogComponent,
|
||||
PdfViewerComponent,
|
||||
DocumentLinkComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -265,7 +270,6 @@ function initializeApp(settings: SettingsService) {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxFileDropModule,
|
||||
PdfViewerModule,
|
||||
NgSelectModule,
|
||||
ColorSliderModule,
|
||||
TourNgBootstrapModule,
|
||||
|
@@ -1,14 +1,19 @@
|
||||
<pngx-page-header title="Logs" i18n-title>
|
||||
|
||||
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
|
||||
<li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile">
|
||||
<a ngbNavLink>{{logFile}}.log</a>
|
||||
<a ngbNavLink>
|
||||
{{logFile}}.log
|
||||
</a>
|
||||
</li>
|
||||
<div *ngIf="isLoading && !logFiles.length" class="pb-2">
|
||||
<div *ngIf="isLoading || !logFiles.length" class="ps-2 d-flex align-items-center">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
<ng-container *ngIf="!logFiles.length" i18n>Loading...</ng-container>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
|
@@ -1,4 +1,9 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { LogService } from 'src/app/services/rest/log.service'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { LogsComponent } from './logs.component'
|
||||
@@ -26,6 +31,7 @@ describe('LogsComponent', () => {
|
||||
let fixture: ComponentFixture<LogsComponent>
|
||||
let logService: LogService
|
||||
let logSpy
|
||||
let reloadSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -42,7 +48,9 @@ describe('LogsComponent', () => {
|
||||
})
|
||||
fixture = TestBed.createComponent(LogsComponent)
|
||||
component = fixture.componentInstance
|
||||
reloadSpy = jest.spyOn(component, 'reloadLogs')
|
||||
window.HTMLElement.prototype.scroll = function () {} // mock scroll
|
||||
jest.useFakeTimers()
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
@@ -68,4 +76,14 @@ describe('LogsComponent', () => {
|
||||
component.reloadLogs()
|
||||
expect(component.logs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should auto refresh, allow toggle', () => {
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
component.toggleAutoRefresh()
|
||||
expect(component.autoRefreshInterval).toBeNull()
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
@@ -27,6 +27,8 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
|
||||
public isLoading: boolean = false
|
||||
|
||||
public autoRefreshInterval: any
|
||||
|
||||
@ViewChild('logContainer') logContainer: ElementRef
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -41,6 +43,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
this.activeLog = this.logFiles[0]
|
||||
this.reloadLogs()
|
||||
}
|
||||
this.toggleAutoRefresh()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,4 +94,15 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
behavior: 'auto',
|
||||
})
|
||||
}
|
||||
|
||||
toggleAutoRefresh(): void {
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval)
|
||||
this.autoRefreshInterval = null
|
||||
} else {
|
||||
this.autoRefreshInterval = setInterval(() => {
|
||||
this.reloadLogs()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -184,7 +184,7 @@
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Owner</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-5">
|
||||
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
|
||||
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
||||
</div>
|
||||
@@ -193,9 +193,9 @@
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default View Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -205,7 +205,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Groups:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -220,9 +220,9 @@
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Edit Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -232,7 +232,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Groups:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
@@ -416,7 +416,7 @@ export class SettingsComponent
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.THEME_COLOR,
|
||||
this.settingsForm.value.themeColor.toString()
|
||||
this.settingsForm.value.themeColor
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<pngx-page-header title="File Tasks" i18n-title>
|
||||
<div class="btn-toolbar col col-md-auto">
|
||||
<div class="btn-toolbar col col-md-auto align-items-center">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
@@ -10,15 +10,10 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tasksService.reload()">
|
||||
<svg *ngIf="!tasksService.loading" class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-clockwise"/>
|
||||
</svg>
|
||||
<ng-container *ngIf="tasksService.loading">
|
||||
<div class="spinner-border spinner-border-sm fw-normal" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</ng-container> <ng-container i18n>Refresh</ng-container>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
</div>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
|
@@ -112,6 +112,7 @@ describe('TasksComponent', () => {
|
||||
let modalService: NgbModal
|
||||
let router: Router
|
||||
let httpTestingController: HttpTestingController
|
||||
let reloadSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -141,11 +142,13 @@ describe('TasksComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
tasksService = TestBed.inject(TasksService)
|
||||
reloadSpy = jest.spyOn(tasksService, 'reload')
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
router = TestBed.inject(Router)
|
||||
fixture = TestBed.createComponent(TasksComponent)
|
||||
component = fixture.componentInstance
|
||||
jest.useFakeTimers()
|
||||
fixture.detectChanges()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}tasks/`)
|
||||
@@ -164,7 +167,7 @@ describe('TasksComponent', () => {
|
||||
`Failed${currentTasksLength}`
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('input[type="checkbox"]'))
|
||||
fixture.debugElement.queryAll(By.css('table input[type="checkbox"]'))
|
||||
).toHaveLength(currentTasksLength + 1)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
@@ -245,7 +248,7 @@ describe('TasksComponent', () => {
|
||||
|
||||
it('should support toggle all tasks', () => {
|
||||
const toggleCheck = fixture.debugElement.query(
|
||||
By.css('input[type=checkbox]')
|
||||
By.css('table input[type=checkbox]')
|
||||
)
|
||||
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
@@ -269,4 +272,15 @@ describe('TasksComponent', () => {
|
||||
tasks[3].related_document,
|
||||
])
|
||||
})
|
||||
|
||||
it('should auto refresh, allow toggle', () => {
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1)
|
||||
jest.advanceTimersByTime(5000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
component.toggleAutoRefresh()
|
||||
expect(component.autoRefreshInterval).toBeNull()
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
@@ -23,6 +23,8 @@ export class TasksComponent
|
||||
public pageSize: number = 25
|
||||
public page: number = 1
|
||||
|
||||
public autoRefreshInterval: any
|
||||
|
||||
get dismissButtonText(): string {
|
||||
return this.selectedTasks.size > 0
|
||||
? $localize`Dismiss selected`
|
||||
@@ -39,6 +41,7 @@ export class TasksComponent
|
||||
|
||||
ngOnInit() {
|
||||
this.tasksService.reload()
|
||||
this.toggleAutoRefresh()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -135,4 +138,15 @@ export class TasksComponent
|
||||
return $localize`failed`
|
||||
}
|
||||
}
|
||||
|
||||
toggleAutoRefresh(): void {
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval)
|
||||
this.autoRefreshInterval = null
|
||||
} else {
|
||||
this.autoRefreshInterval = setInterval(() => {
|
||||
this.tasksService.reload()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -89,7 +89,7 @@ export class UsersAndGroupsComponent
|
||||
$localize`Password has been changed, you will be logged out momentarily.`
|
||||
)
|
||||
setTimeout(() => {
|
||||
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/`
|
||||
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
||||
}, 2500)
|
||||
} else {
|
||||
this.toastService.showInfo(
|
||||
|
@@ -39,6 +39,11 @@
|
||||
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
||||
<div class="dropdown-divider"></div>
|
||||
</div>
|
||||
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||
</svg><ng-container i18n>My Profile</ng-container>
|
||||
</button>
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
@@ -87,12 +92,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || sidebarViews?.length > 0'>
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews?.length > 0'>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||
<li class="nav-item w-100" *ngFor="let view of sidebarViews"
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
cdkDragPreviewContainer="parent"
|
||||
|
@@ -27,7 +27,7 @@
|
||||
max-width: 16.66666667%;
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
@media (min-width: 2400px) {
|
||||
max-width: 8.33333333%;
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item .position-absolute {
|
||||
.nav-item > .position-absolute {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
@@ -19,7 +19,7 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { Observable, of, tap, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -32,6 +32,7 @@ import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
|
||||
const saved_views = [
|
||||
{
|
||||
@@ -86,6 +87,7 @@ describe('AppFrameComponent', () => {
|
||||
let documentListViewService: DocumentListViewService
|
||||
let router: Router
|
||||
let savedViewSpy
|
||||
let modalService: NgbModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -98,6 +100,7 @@ describe('AppFrameComponent', () => {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
DragDropModule,
|
||||
NgbModalModule,
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
@@ -120,6 +123,7 @@ describe('AppFrameComponent', () => {
|
||||
ToastService,
|
||||
OpenDocumentsService,
|
||||
SearchService,
|
||||
NgbModal,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
@@ -148,6 +152,7 @@ describe('AppFrameComponent', () => {
|
||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||
searchService = TestBed.inject(SearchService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
router = TestBed.inject(Router)
|
||||
|
||||
jest
|
||||
@@ -293,6 +298,21 @@ describe('AppFrameComponent', () => {
|
||||
expect(autocompleteSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('should handle autocomplete backend failure gracefully', fakeAsync(() => {
|
||||
const serviceAutocompleteSpy = jest.spyOn(searchService, 'autocomplete')
|
||||
serviceAutocompleteSpy.mockReturnValue(
|
||||
throwError(() => new Error('autcomplete failed'))
|
||||
)
|
||||
// serviceAutocompleteSpy.mockReturnValue(of([' world']))
|
||||
let result
|
||||
component.searchAutoComplete(of('hello')).subscribe((res) => {
|
||||
result = res
|
||||
})
|
||||
tick(250)
|
||||
expect(serviceAutocompleteSpy).toHaveBeenCalled()
|
||||
expect(result).toEqual([])
|
||||
}))
|
||||
|
||||
it('should support reset search field', () => {
|
||||
const resetSpy = jest.spyOn(component, 'resetSearchField')
|
||||
const input = (fixture.nativeElement as HTMLDivElement).querySelector(
|
||||
@@ -363,4 +383,12 @@ describe('AppFrameComponent', () => {
|
||||
>)
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support edit profile', () => {
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
component.editProfile()
|
||||
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
map,
|
||||
switchMap,
|
||||
first,
|
||||
catchError,
|
||||
} from 'rxjs/operators'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -39,6 +40,8 @@ import {
|
||||
CdkDragDrop,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-app-frame',
|
||||
@@ -58,8 +61,6 @@ export class AppFrameComponent
|
||||
|
||||
searchField = new FormControl('')
|
||||
|
||||
sidebarViews: PaperlessSavedView[]
|
||||
|
||||
constructor(
|
||||
public router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -71,6 +72,7 @@ export class AppFrameComponent
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
permissionsService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
@@ -90,10 +92,6 @@ export class AppFrameComponent
|
||||
this.checkForUpdates()
|
||||
}
|
||||
this.tasksService.reload()
|
||||
|
||||
this.savedViewService.listAll().subscribe(() => {
|
||||
this.sidebarViews = this.savedViewService.sidebarViews
|
||||
})
|
||||
}
|
||||
|
||||
toggleSlimSidebar(): void {
|
||||
@@ -127,6 +125,13 @@ export class AppFrameComponent
|
||||
this.isMenuCollapsed = true
|
||||
}
|
||||
|
||||
editProfile() {
|
||||
this.modalService.open(ProfileEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
this.closeMenu()
|
||||
}
|
||||
|
||||
get openDocuments(): PaperlessDocument[] {
|
||||
return this.openDocumentsService.getOpenDocuments()
|
||||
}
|
||||
@@ -162,7 +167,13 @@ export class AppFrameComponent
|
||||
}
|
||||
}),
|
||||
switchMap((term) =>
|
||||
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
|
||||
term.length < 2
|
||||
? from([[]])
|
||||
: this.searchService.autocomplete(term).pipe(
|
||||
catchError(() => {
|
||||
return from([[]])
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -240,9 +251,10 @@ export class AppFrameComponent
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<PaperlessSavedView[]>) {
|
||||
moveItemInArray(this.sidebarViews, event.previousIndex, event.currentIndex)
|
||||
const sidebarViews = this.savedViewService.sidebarViews.concat([])
|
||||
moveItemInArray(sidebarViews, event.previousIndex, event.currentIndex)
|
||||
|
||||
this.settingsService.updateSidebarViewsSort(this.sidebarViews).subscribe({
|
||||
this.settingsService.updateSidebarViewsSort(sidebarViews).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Sidebar views updated`)
|
||||
},
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<div class="col-md-4">
|
||||
<h5 class="border-bottom pb-2" i18n>Filters</h5>
|
||||
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
|
||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.filter_filename"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||
@@ -35,6 +35,7 @@
|
||||
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||
@@ -43,7 +44,7 @@
|
||||
<div class="mb-2">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Users:</label>
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
|
||||
@@ -51,7 +52,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Groups:</label>
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
|
||||
@@ -62,7 +63,7 @@
|
||||
<div>
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Users:</label>
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
|
||||
@@ -70,7 +71,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Groups:</label>
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
|
||||
|
@@ -20,6 +20,7 @@ import { TagsComponent } from '../../input/tags/tags.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from './consumption-template-edit-dialog.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
|
||||
describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
let component: ConsumptionTemplateEditDialogComponent
|
||||
@@ -93,6 +94,15 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CustomFieldsService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
|
@@ -18,6 +18,8 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent } from '../edit-dialog.component'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { PaperlessCustomField } from 'src/app/data/paperless-custom-field'
|
||||
|
||||
export const DOCUMENT_SOURCE_OPTIONS = [
|
||||
{
|
||||
@@ -45,6 +47,7 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
storagePaths: PaperlessStoragePath[]
|
||||
mailRules: PaperlessMailRule[]
|
||||
customFields: PaperlessCustomField[]
|
||||
|
||||
constructor(
|
||||
service: ConsumptionTemplateService,
|
||||
@@ -54,7 +57,8 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
|
||||
storagePathService: StoragePathService,
|
||||
mailRuleService: MailRuleService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
settingsService: SettingsService,
|
||||
customFieldsService: CustomFieldsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
|
||||
@@ -77,6 +81,11 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.mailRules = result.results))
|
||||
|
||||
customFieldsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -106,6 +115,7 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
|
||||
assign_view_groups: new FormControl([]),
|
||||
assign_change_users: new FormControl([]),
|
||||
assign_change_groups: new FormControl([]),
|
||||
assign_custom_fields: new FormControl([]),
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,16 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
|
||||
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
|
||||
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -131,6 +131,7 @@ describe('EditDialogComponent', () => {
|
||||
})
|
||||
|
||||
it('should interpolate object permissions', () => {
|
||||
component.getMatchingAlgorithms() // coverage
|
||||
component.object = tag
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
component.ngOnInit()
|
||||
|
@@ -58,8 +58,8 @@ export abstract class EditDialogComponent<
|
||||
objectForm: FormGroup = this.getForm()
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.object != null) {
|
||||
if (this.object['permissions']) {
|
||||
if (this.object != null && this.dialogMode !== EditDialogMode.CREATE) {
|
||||
if ((this.object as ObjectWithPermissions).permissions) {
|
||||
this.object['set_permissions'] = this.object['permissions']
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ export abstract class EditDialogComponent<
|
||||
}
|
||||
this.objectForm.patchValue(this.object)
|
||||
} else {
|
||||
// e.g. if name was set
|
||||
this.objectForm.patchValue(this.object)
|
||||
// defaults from settings
|
||||
this.objectForm.patchValue({
|
||||
permissions_form: {
|
||||
|
@@ -21,7 +21,8 @@
|
||||
<pngx-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter attachment filename includes" formControlName="filter_attachment_filename_include" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename_include"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter attachment filename excluding" formControlName="filter_attachment_filename_exclude" i18n-hint hint="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename_exclude"></pngx-input-text>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select>
|
||||
|
@@ -158,7 +158,8 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
|
||||
filter_to: new FormControl(null),
|
||||
filter_subject: new FormControl(null),
|
||||
filter_body: new FormControl(null),
|
||||
filter_attachment_filename: new FormControl(null),
|
||||
filter_attachment_filename_include: new FormControl(null),
|
||||
filter_attachment_filename_exclude: new FormControl(null),
|
||||
maximum_age: new FormControl(null),
|
||||
attachment_type: new FormControl(MailFilterAttachmentType.Attachments),
|
||||
consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments),
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
<div *ngIf="horizontal" class="d-flex align-items-center position-relative hidden-button-container col-md-3">
|
||||
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
|
@@ -0,0 +1,50 @@
|
||||
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div [class.col-md-9]="horizontal">
|
||||
<div>
|
||||
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
|
||||
[disabled]="disabled"
|
||||
[items]="foundDocuments$ | async"
|
||||
placeholder="Search for documents"
|
||||
[notFoundText]="notFoundText"
|
||||
[multiple]="true"
|
||||
bindValue="id"
|
||||
[compareWith]="compareDocuments"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="loading"
|
||||
[typeahead]="documentsInput$"
|
||||
(change)="onChange(selectedDocuments)">
|
||||
<ng-template ng-label-tmp let-document="item">
|
||||
<div class="d-flex align-items-center">
|
||||
<svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
|
||||
<svg class="sidebaricon-sm me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg><span>{{document.title}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template ng-loadingspinner-tmp>
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</ng-template>
|
||||
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
|
||||
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,14 @@
|
||||
::ng-deep .ng-select-container .ng-value-container .ng-value {
|
||||
background-color: transparent !important;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.sidebaricon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: .75rem;
|
||||
// --bs-primary: var(--pngx-bg-alt);
|
||||
// color: var(--pngx-primary-text-contrast);
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { DocumentLinkComponent } from './document-link.component'
|
||||
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
|
||||
|
||||
const documents = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Document 1 foo',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: 'Document 12 bar',
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
title: 'Document 23 bar',
|
||||
},
|
||||
]
|
||||
|
||||
describe('DocumentLinkComponent', () => {
|
||||
let component: DocumentLinkComponent
|
||||
let fixture: ComponentFixture<DocumentLinkComponent>
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DocumentLinkComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
fixture = TestBed.createComponent(DocumentLinkComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should retrieve selected documents from APIs', () => {
|
||||
const getSpy = jest.spyOn(documentService, 'getCachedMany')
|
||||
getSpy.mockImplementation((ids) => {
|
||||
return of(documents.filter((d) => ids.includes(d.id)))
|
||||
})
|
||||
component.writeValue([1])
|
||||
expect(getSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should search API on select text input', () => {
|
||||
const listSpy = jest.spyOn(documentService, 'listFiltered')
|
||||
listSpy.mockImplementation(
|
||||
(page, pageSize, sortField, sortReverse, filterRules, extraParams) => {
|
||||
const docs = documents.filter((d) =>
|
||||
d.title.includes(filterRules[0].value)
|
||||
)
|
||||
return of({
|
||||
count: docs.length,
|
||||
results: docs,
|
||||
all: docs.map((d) => d.id),
|
||||
})
|
||||
}
|
||||
)
|
||||
component.documentsInput$.next('bar')
|
||||
expect(listSpy).toHaveBeenCalledWith(
|
||||
1,
|
||||
null,
|
||||
'created',
|
||||
true,
|
||||
[{ rule_type: FILTER_TITLE, value: 'bar' }],
|
||||
{ truncate_content: true }
|
||||
)
|
||||
listSpy.mockReturnValueOnce(throwError(() => new Error()))
|
||||
component.documentsInput$.next('foo')
|
||||
})
|
||||
|
||||
it('should load values correctly', () => {
|
||||
jest.spyOn(documentService, 'getCachedMany').mockImplementation((ids) => {
|
||||
return of(documents.filter((d) => ids.includes(d.id)))
|
||||
})
|
||||
component.writeValue([12, 23])
|
||||
expect(component.value).toEqual([12, 23])
|
||||
expect(component.selectedDocuments).toEqual([documents[1], documents[2]])
|
||||
component.writeValue(null)
|
||||
expect(component.value).toEqual([])
|
||||
expect(component.selectedDocuments).toEqual([])
|
||||
component.writeValue([])
|
||||
expect(component.value).toEqual([])
|
||||
expect(component.selectedDocuments).toEqual([])
|
||||
})
|
||||
|
||||
it('should support unselect', () => {
|
||||
const getSpy = jest.spyOn(documentService, 'getCachedMany')
|
||||
getSpy.mockImplementation((ids) => {
|
||||
return of(documents.filter((d) => ids.includes(d.id)))
|
||||
})
|
||||
component.writeValue([12, 23])
|
||||
component.unselect({ id: 23 })
|
||||
fixture.detectChanges()
|
||||
expect(component.selectedDocuments).toEqual([documents[1]])
|
||||
})
|
||||
|
||||
it('should use correct compare, trackBy functions', () => {
|
||||
expect(component.compareDocuments(documents[0], { id: 1 })).toBeTruthy()
|
||||
expect(component.compareDocuments(documents[0], { id: 2 })).toBeFalsy()
|
||||
expect(component.trackByFn(documents[1])).toEqual(12)
|
||||
})
|
||||
})
|
@@ -0,0 +1,120 @@
|
||||
import { Component, forwardRef, OnInit, Input, OnDestroy } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import {
|
||||
Subject,
|
||||
Observable,
|
||||
takeUntil,
|
||||
concat,
|
||||
of,
|
||||
distinctUntilChanged,
|
||||
tap,
|
||||
switchMap,
|
||||
map,
|
||||
catchError,
|
||||
} from 'rxjs'
|
||||
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => DocumentLinkComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'pngx-input-document-link',
|
||||
templateUrl: './document-link.component.html',
|
||||
styleUrls: ['./document-link.component.scss'],
|
||||
})
|
||||
export class DocumentLinkComponent
|
||||
extends AbstractInputComponent<any[]>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
documentsInput$ = new Subject<string>()
|
||||
foundDocuments$: Observable<PaperlessDocument[]>
|
||||
loading = false
|
||||
selectedDocuments: PaperlessDocument[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
@Input()
|
||||
notFoundText: string = $localize`No documents found`
|
||||
|
||||
constructor(private documentsService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadDocs()
|
||||
}
|
||||
|
||||
writeValue(documentIDs: number[]): void {
|
||||
if (!documentIDs || documentIDs.length === 0) {
|
||||
this.selectedDocuments = []
|
||||
super.writeValue([])
|
||||
} else {
|
||||
this.loading = true
|
||||
this.documentsService
|
||||
.getCachedMany(documentIDs)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((documents) => {
|
||||
this.loading = false
|
||||
this.selectedDocuments = documents
|
||||
super.writeValue(documentIDs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private loadDocs() {
|
||||
this.foundDocuments$ = concat(
|
||||
of([]), // default items
|
||||
this.documentsInput$.pipe(
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap(() => (this.loading = true)),
|
||||
switchMap((title) =>
|
||||
this.documentsService
|
||||
.listFiltered(
|
||||
1,
|
||||
null,
|
||||
'created',
|
||||
true,
|
||||
[{ rule_type: FILTER_TITLE, value: title }],
|
||||
{ truncate_content: true }
|
||||
)
|
||||
.pipe(
|
||||
map((results) => results.results),
|
||||
catchError(() => of([])), // empty on error
|
||||
tap(() => (this.loading = false))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
unselect(document: PaperlessDocument): void {
|
||||
this.selectedDocuments = this.selectedDocuments.filter(
|
||||
(d) => d.id !== document.id
|
||||
)
|
||||
this.onChange(this.selectedDocuments.map((d) => d.id))
|
||||
}
|
||||
|
||||
compareDocuments(
|
||||
document: PaperlessDocument,
|
||||
selectedDocument: PaperlessDocument
|
||||
) {
|
||||
return document.id === selectedDocument.id
|
||||
}
|
||||
|
||||
trackByFn(item: PaperlessDocument) {
|
||||
return item.id
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
|
@@ -1,8 +1,15 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
||||
<button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#eye" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
</div>
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { PasswordComponent } from './password.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
describe('PasswordComponent', () => {
|
||||
let component: PasswordComponent
|
||||
@@ -33,4 +34,26 @@ describe('PasswordComponent', () => {
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should support toggling field visibility', () => {
|
||||
expect(input.type).toEqual('password')
|
||||
component.showReveal = true
|
||||
fixture.detectChanges()
|
||||
fixture.debugElement.query(By.css('button')).triggerEventHandler('click')
|
||||
fixture.detectChanges()
|
||||
expect(input.type).toEqual('text')
|
||||
})
|
||||
|
||||
it('should empty field if password is obfuscated on focus', () => {
|
||||
component.value = '*********'
|
||||
component.onFocus()
|
||||
expect(component.value).toEqual('')
|
||||
component.onFocusOut()
|
||||
expect(component.value).toEqual('**********')
|
||||
})
|
||||
|
||||
it('should disable toggle button if no real password', () => {
|
||||
component.value = '*********'
|
||||
expect(component.disableRevealToggle).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, Input, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@@ -15,7 +15,32 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
styleUrls: ['./password.component.scss'],
|
||||
})
|
||||
export class PasswordComponent extends AbstractInputComponent<string> {
|
||||
constructor() {
|
||||
super()
|
||||
@Input()
|
||||
showReveal: boolean = false
|
||||
|
||||
@Input()
|
||||
autocomplete: string
|
||||
|
||||
public textVisible: boolean = false
|
||||
|
||||
public toggleVisibility(): void {
|
||||
this.textVisible = !this.textVisible
|
||||
}
|
||||
|
||||
public onFocus() {
|
||||
if (this.value?.replace(/\*/g, '').length === 0) {
|
||||
this.writeValue('')
|
||||
}
|
||||
}
|
||||
|
||||
public onFocusOut() {
|
||||
if (this.value?.length === 0) {
|
||||
this.writeValue('**********')
|
||||
this.onChange(this.value)
|
||||
}
|
||||
}
|
||||
|
||||
get disableRevealToggle(): boolean {
|
||||
return this.value?.replace(/\*/g, '').length === 0
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label *ngIf="title" class="form-label mb-md-0" [for]="inputId">{{title}}</label>
|
||||
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
@@ -9,7 +9,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div [class.col-md-9]="horizontal">
|
||||
<div [class.input-group]="allowCreateNew || showFilter">
|
||||
<div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
@@ -42,6 +42,9 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
<small *ngIf="getSuggestions().length > 0">
|
||||
<span i18n>Suggestions:</span>
|
||||
|
@@ -17,3 +17,12 @@
|
||||
font-style: italic;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
::ng-deep .is-invalid ng-select .ng-select-container input {
|
||||
// replicate bootstrap
|
||||
padding-right: calc(1.5em + 0.75rem) !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: right calc(0.375em + 0.1875rem) center !important;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled" [class.pb-3]="suggestions">
|
||||
<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled" [class.pb-3]="getSuggestions().length > 0">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center" [class.col-md-3]="horizontal">
|
||||
<label class="form-label mb-md-0" for="tags" i18n>{{title}}</label>
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" for="tags" i18n>{{title}}</label>
|
||||
</div>
|
||||
<div class="position-relative" [class.col-md-9]="horizontal">
|
||||
<div class="input-group flex-nowrap">
|
||||
|
@@ -32,11 +32,9 @@ import { CheckComponent } from '../check/check.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { TextComponent } from '../text/text.component'
|
||||
import { ColorComponent } from '../color/color.component'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../select/select.component'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
const tags: PaperlessTag[] = [
|
||||
{
|
||||
@@ -63,8 +61,8 @@ const tags: PaperlessTag[] = [
|
||||
describe('TagsComponent', () => {
|
||||
let component: TagsComponent
|
||||
let fixture: ComponentFixture<TagsComponent>
|
||||
let input: HTMLInputElement
|
||||
let modalService: NgbModal
|
||||
let settingsService: SettingsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -110,6 +108,7 @@ describe('TagsComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(TagsComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
@@ -139,6 +138,7 @@ describe('TagsComponent', () => {
|
||||
})
|
||||
|
||||
it('should support create new using last search term and open a modal', () => {
|
||||
settingsService.currentUser = { id: 1 }
|
||||
let activeInstances: NgbModalRef[]
|
||||
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
||||
component.select.searchTerm = 'foobar'
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
@@ -9,7 +9,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-relative" [class.col-md-9]="horizontal">
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, Input, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@@ -15,6 +15,9 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
styleUrls: ['./text.component.scss'],
|
||||
})
|
||||
export class TextComponent extends AbstractInputComponent<string> {
|
||||
@Input()
|
||||
autocomplete: string
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.height]="height">
|
||||
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
|
||||
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
||||
<g class="text" style="fill:#000">
|
||||
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -24,13 +24,13 @@ describe('LogoComponent', () => {
|
||||
})
|
||||
|
||||
it('should support setting height', () => {
|
||||
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
|
||||
'6em'
|
||||
expect(fixture.debugElement.query(By.css('svg')).attributes.style).toEqual(
|
||||
'height:6em'
|
||||
)
|
||||
component.height = '10em'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
|
||||
'10em'
|
||||
expect(fixture.debugElement.query(By.css('svg')).attributes.style).toEqual(
|
||||
'height:10em'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<div #pdfViewerContainer class="pngx-pdf-viewer-container">
|
||||
<div class="pdfViewer"></div>
|
||||
</div>
|
@@ -0,0 +1,599 @@
|
||||
/**
|
||||
* This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/pdf-viewer.component.ts
|
||||
* Created by vadimdez on 21/06/16.
|
||||
*/
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
AfterViewChecked,
|
||||
NgZone,
|
||||
} from '@angular/core'
|
||||
import { from, fromEvent, Subject } from 'rxjs'
|
||||
import { debounceTime, filter, takeUntil } from 'rxjs/operators'
|
||||
import * as PDFJS from 'pdfjs-dist'
|
||||
import * as PDFJSViewer from 'pdfjs-dist/web/pdf_viewer'
|
||||
|
||||
import { createEventBus } from './utils/event-bus-utils'
|
||||
|
||||
import type {
|
||||
PDFSource,
|
||||
PDFPageProxy,
|
||||
PDFProgressData,
|
||||
PDFDocumentProxy,
|
||||
PDFDocumentLoadingTask,
|
||||
PDFViewerOptions,
|
||||
ZoomScale,
|
||||
} from './typings'
|
||||
import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer'
|
||||
|
||||
PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS
|
||||
|
||||
// Yea this is a straight hack
|
||||
declare global {
|
||||
interface WeakKeyTypes {
|
||||
symbol: Object
|
||||
}
|
||||
|
||||
type WeakKey = WeakKeyTypes[keyof WeakKeyTypes]
|
||||
}
|
||||
|
||||
export enum RenderTextMode {
|
||||
DISABLED,
|
||||
ENABLED,
|
||||
ENHANCED,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-pdf-viewer',
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
styleUrls: ['./pdf-viewer.component.scss'],
|
||||
})
|
||||
export class PdfViewerComponent
|
||||
implements OnChanges, OnInit, OnDestroy, AfterViewChecked
|
||||
{
|
||||
static CSS_UNITS = 96.0 / 72.0
|
||||
static BORDER_WIDTH = 9
|
||||
|
||||
@ViewChild('pdfViewerContainer')
|
||||
pdfViewerContainer!: ElementRef<HTMLDivElement>
|
||||
|
||||
public eventBus!: PDFJSViewer.EventBus
|
||||
public pdfLinkService!: PDFJSViewer.PDFLinkService
|
||||
public pdfViewer!: PDFJSViewer.PDFViewer | PDFSinglePageViewer
|
||||
|
||||
private isVisible = false
|
||||
|
||||
private _cMapsUrl =
|
||||
typeof PDFJS !== 'undefined'
|
||||
? `https://unpkg.com/pdfjs-dist@${(PDFJS as any).version}/cmaps/`
|
||||
: null
|
||||
private _imageResourcesPath =
|
||||
typeof PDFJS !== 'undefined'
|
||||
? `https://unpkg.com/pdfjs-dist@${(PDFJS as any).version}/web/images/`
|
||||
: undefined
|
||||
private _renderText = true
|
||||
private _renderTextMode: RenderTextMode = RenderTextMode.ENABLED
|
||||
private _stickToPage = false
|
||||
private _originalSize = true
|
||||
private _pdf: PDFDocumentProxy | undefined
|
||||
private _page = 1
|
||||
private _zoom = 1
|
||||
private _zoomScale: ZoomScale = 'page-width'
|
||||
private _rotation = 0
|
||||
private _showAll = true
|
||||
private _canAutoResize = true
|
||||
private _fitToPage = false
|
||||
private _externalLinkTarget = 'blank'
|
||||
private _showBorders = false
|
||||
private lastLoaded!: string | Uint8Array | PDFSource | null
|
||||
private _latestScrolledPage!: number
|
||||
|
||||
private resizeTimeout: number | null = null
|
||||
private pageScrollTimeout: number | null = null
|
||||
private isInitialized = false
|
||||
private loadingTask?: PDFDocumentLoadingTask | null
|
||||
private destroy$ = new Subject<void>()
|
||||
|
||||
@Output('after-load-complete') afterLoadComplete =
|
||||
new EventEmitter<PDFDocumentProxy>()
|
||||
@Output('page-rendered') pageRendered = new EventEmitter<CustomEvent>()
|
||||
@Output('pages-initialized') pageInitialized = new EventEmitter<CustomEvent>()
|
||||
@Output('text-layer-rendered') textLayerRendered =
|
||||
new EventEmitter<CustomEvent>()
|
||||
@Output('error') onError = new EventEmitter<any>()
|
||||
@Output('on-progress') onProgress = new EventEmitter<PDFProgressData>()
|
||||
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>(true)
|
||||
@Input() src?: string | Uint8Array | PDFSource
|
||||
|
||||
@Input('c-maps-url')
|
||||
set cMapsUrl(cMapsUrl: string) {
|
||||
this._cMapsUrl = cMapsUrl
|
||||
}
|
||||
|
||||
@Input('page')
|
||||
set page(_page: number | string | any) {
|
||||
_page = parseInt(_page, 10) || 1
|
||||
const originalPage = _page
|
||||
|
||||
if (this._pdf) {
|
||||
_page = this.getValidPageNumber(_page)
|
||||
}
|
||||
|
||||
this._page = _page
|
||||
if (originalPage !== _page) {
|
||||
this.pageChange.emit(_page)
|
||||
}
|
||||
}
|
||||
|
||||
@Input('render-text')
|
||||
set renderText(renderText: boolean) {
|
||||
this._renderText = renderText
|
||||
}
|
||||
|
||||
@Input('render-text-mode')
|
||||
set renderTextMode(renderTextMode: RenderTextMode) {
|
||||
this._renderTextMode = renderTextMode
|
||||
}
|
||||
|
||||
@Input('original-size')
|
||||
set originalSize(originalSize: boolean) {
|
||||
this._originalSize = originalSize
|
||||
}
|
||||
|
||||
@Input('show-all')
|
||||
set showAll(value: boolean) {
|
||||
this._showAll = value
|
||||
}
|
||||
|
||||
@Input('stick-to-page')
|
||||
set stickToPage(value: boolean) {
|
||||
this._stickToPage = value
|
||||
}
|
||||
|
||||
@Input('zoom')
|
||||
set zoom(value: number) {
|
||||
if (value <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this._zoom = value
|
||||
}
|
||||
|
||||
get zoom() {
|
||||
return this._zoom
|
||||
}
|
||||
|
||||
@Input('zoom-scale')
|
||||
set zoomScale(value: ZoomScale) {
|
||||
this._zoomScale = value
|
||||
}
|
||||
|
||||
get zoomScale() {
|
||||
return this._zoomScale
|
||||
}
|
||||
|
||||
@Input('rotation')
|
||||
set rotation(value: number) {
|
||||
if (!(typeof value === 'number' && value % 90 === 0)) {
|
||||
console.warn('Invalid pages rotation angle.')
|
||||
return
|
||||
}
|
||||
|
||||
this._rotation = value
|
||||
}
|
||||
|
||||
@Input('external-link-target')
|
||||
set externalLinkTarget(value: string) {
|
||||
this._externalLinkTarget = value
|
||||
}
|
||||
|
||||
@Input('autoresize')
|
||||
set autoresize(value: boolean) {
|
||||
this._canAutoResize = Boolean(value)
|
||||
}
|
||||
|
||||
@Input('fit-to-page')
|
||||
set fitToPage(value: boolean) {
|
||||
this._fitToPage = Boolean(value)
|
||||
}
|
||||
|
||||
@Input('show-borders')
|
||||
set showBorders(value: boolean) {
|
||||
this._showBorders = Boolean(value)
|
||||
}
|
||||
|
||||
static getLinkTarget(type: string) {
|
||||
switch (type) {
|
||||
case 'blank':
|
||||
return (PDFJSViewer as any).LinkTarget.BLANK
|
||||
case 'none':
|
||||
return (PDFJSViewer as any).LinkTarget.NONE
|
||||
case 'self':
|
||||
return (PDFJSViewer as any).LinkTarget.SELF
|
||||
case 'parent':
|
||||
return (PDFJSViewer as any).LinkTarget.PARENT
|
||||
case 'top':
|
||||
return (PDFJSViewer as any).LinkTarget.TOP
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
constructor(
|
||||
private element: ElementRef<HTMLElement>,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
PDFJS.GlobalWorkerOptions['workerSrc'] = 'assets/js/pdf.worker.min.js'
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
if (this.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
const offset = this.pdfViewerContainer.nativeElement.offsetParent
|
||||
|
||||
if (this.isVisible === true && offset == null) {
|
||||
this.isVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isVisible === false && offset != null) {
|
||||
this.isVisible = true
|
||||
|
||||
setTimeout(() => {
|
||||
this.initialize()
|
||||
this.ngOnChanges({ src: this.src } as any)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialize()
|
||||
this.setupResizeListener()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.clear()
|
||||
this.destroy$.next()
|
||||
this.loadingTask = null
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!this.isVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
if ('src' in changes) {
|
||||
this.loadPDF()
|
||||
} else if (this._pdf) {
|
||||
if ('renderText' in changes || 'showAll' in changes) {
|
||||
this.setupViewer()
|
||||
this.resetPdfDocument()
|
||||
}
|
||||
if ('page' in changes) {
|
||||
const { page } = changes
|
||||
if (page.currentValue === this._latestScrolledPage) {
|
||||
return
|
||||
}
|
||||
|
||||
// New form of page changing: The viewer will now jump to the specified page when it is changed.
|
||||
// This behavior is introduced by using the PDFSinglePageViewer
|
||||
this.pdfViewer.scrollPageIntoView({ pageNumber: this._page })
|
||||
}
|
||||
|
||||
this.update()
|
||||
}
|
||||
}
|
||||
|
||||
public updateSize() {
|
||||
from(
|
||||
this._pdf!.getPage(
|
||||
this.pdfViewer.currentPageNumber
|
||||
) as unknown as Promise<PDFPageProxy>
|
||||
)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (page: PDFPageProxy) => {
|
||||
const rotation = this._rotation + page.rotate
|
||||
const viewportWidth =
|
||||
(page as any).getViewport({
|
||||
scale: this._zoom,
|
||||
rotation,
|
||||
}).width * PdfViewerComponent.CSS_UNITS
|
||||
let scale = this._zoom
|
||||
let stickToPage = true
|
||||
|
||||
// Scale the document when it shouldn't be in original size or doesn't fit into the viewport
|
||||
if (
|
||||
!this._originalSize ||
|
||||
(this._fitToPage &&
|
||||
viewportWidth > this.pdfViewerContainer.nativeElement.clientWidth)
|
||||
) {
|
||||
const viewPort = (page as any).getViewport({ scale: 1, rotation })
|
||||
scale = this.getScale(viewPort.width, viewPort.height)
|
||||
stickToPage = !this._stickToPage
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.pdfViewer.currentScale = scale
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public clear() {
|
||||
if (this.loadingTask && !this.loadingTask.destroyed) {
|
||||
this.loadingTask.destroy()
|
||||
}
|
||||
|
||||
if (this._pdf) {
|
||||
this._latestScrolledPage = 0
|
||||
this._pdf.destroy()
|
||||
this._pdf = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private getPDFLinkServiceConfig() {
|
||||
const linkTarget = PdfViewerComponent.getLinkTarget(
|
||||
this._externalLinkTarget
|
||||
)
|
||||
|
||||
if (linkTarget) {
|
||||
return { externalLinkTarget: linkTarget }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
private initEventBus() {
|
||||
this.eventBus = createEventBus(PDFJSViewer, this.destroy$)
|
||||
|
||||
fromEvent<CustomEvent>(this.eventBus, 'pagerendered')
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((event) => {
|
||||
this.pageRendered.emit(event)
|
||||
})
|
||||
|
||||
fromEvent<CustomEvent>(this.eventBus, 'pagesinit')
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((event) => {
|
||||
this.pageInitialized.emit(event)
|
||||
})
|
||||
|
||||
fromEvent(this.eventBus, 'pagechanging')
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(({ pageNumber }: any) => {
|
||||
if (this.pageScrollTimeout) {
|
||||
clearTimeout(this.pageScrollTimeout)
|
||||
}
|
||||
|
||||
this.pageScrollTimeout = window.setTimeout(() => {
|
||||
this._latestScrolledPage = pageNumber
|
||||
this.pageChange.emit(pageNumber)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
fromEvent<CustomEvent>(this.eventBus, 'textlayerrendered')
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((event) => {
|
||||
this.textLayerRendered.emit(event)
|
||||
})
|
||||
}
|
||||
|
||||
private initPDFServices() {
|
||||
this.pdfLinkService = new PDFJSViewer.PDFLinkService({
|
||||
eventBus: this.eventBus,
|
||||
...this.getPDFLinkServiceConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
private getPDFOptions(): PDFViewerOptions {
|
||||
return {
|
||||
eventBus: this.eventBus,
|
||||
container: this.element.nativeElement.querySelector('div')!,
|
||||
removePageBorders: !this._showBorders,
|
||||
linkService: this.pdfLinkService,
|
||||
textLayerMode: this._renderText
|
||||
? this._renderTextMode
|
||||
: RenderTextMode.DISABLED,
|
||||
imageResourcesPath: this._imageResourcesPath,
|
||||
}
|
||||
}
|
||||
|
||||
private setupViewer() {
|
||||
PDFJS['disableTextLayer'] = !this._renderText
|
||||
|
||||
this.initPDFServices()
|
||||
|
||||
if (this._showAll) {
|
||||
this.pdfViewer = new PDFJSViewer.PDFViewer(this.getPDFOptions())
|
||||
} else {
|
||||
this.pdfViewer = new PDFJSViewer.PDFSinglePageViewer(this.getPDFOptions())
|
||||
}
|
||||
this.pdfLinkService.setViewer(this.pdfViewer)
|
||||
|
||||
this.pdfViewer._currentPageNumber = this._page
|
||||
}
|
||||
|
||||
private getValidPageNumber(page: number): number {
|
||||
if (page < 1) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (page > this._pdf!.numPages) {
|
||||
return this._pdf!.numPages
|
||||
}
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
private getDocumentParams() {
|
||||
const srcType = typeof this.src
|
||||
|
||||
if (!this._cMapsUrl) {
|
||||
return this.src
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
cMapUrl: this._cMapsUrl,
|
||||
cMapPacked: true,
|
||||
enableXfa: true,
|
||||
}
|
||||
|
||||
if (srcType === 'string') {
|
||||
params.url = this.src
|
||||
} else if (srcType === 'object') {
|
||||
if ((this.src as any).byteLength !== undefined) {
|
||||
params.data = this.src
|
||||
} else {
|
||||
Object.assign(params, this.src)
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
private loadPDF() {
|
||||
if (!this.src) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.lastLoaded === this.src) {
|
||||
this.update()
|
||||
return
|
||||
}
|
||||
|
||||
this.clear()
|
||||
|
||||
this.setupViewer()
|
||||
|
||||
this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
|
||||
|
||||
this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
|
||||
this.onProgress.emit(progressData)
|
||||
}
|
||||
|
||||
const src = this.src
|
||||
|
||||
from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (pdf) => {
|
||||
this._pdf = pdf
|
||||
this.lastLoaded = src
|
||||
|
||||
this.afterLoadComplete.emit(pdf)
|
||||
this.resetPdfDocument()
|
||||
|
||||
this.update()
|
||||
},
|
||||
error: (error) => {
|
||||
this.lastLoaded = null
|
||||
this.onError.emit(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.page = this._page
|
||||
|
||||
this.render()
|
||||
}
|
||||
|
||||
private render() {
|
||||
this._page = this.getValidPageNumber(this._page)
|
||||
|
||||
if (
|
||||
this._rotation !== 0 ||
|
||||
this.pdfViewer.pagesRotation !== this._rotation
|
||||
) {
|
||||
setTimeout(() => {
|
||||
this.pdfViewer.pagesRotation = this._rotation
|
||||
})
|
||||
}
|
||||
|
||||
if (this._stickToPage) {
|
||||
setTimeout(() => {
|
||||
this.pdfViewer.currentPageNumber = this._page
|
||||
})
|
||||
}
|
||||
|
||||
this.updateSize()
|
||||
}
|
||||
|
||||
private getScale(viewportWidth: number, viewportHeight: number) {
|
||||
const borderSize = this._showBorders
|
||||
? 2 * PdfViewerComponent.BORDER_WIDTH
|
||||
: 0
|
||||
const pdfContainerWidth =
|
||||
this.pdfViewerContainer.nativeElement.clientWidth - borderSize
|
||||
const pdfContainerHeight =
|
||||
this.pdfViewerContainer.nativeElement.clientHeight - borderSize
|
||||
|
||||
if (
|
||||
pdfContainerHeight === 0 ||
|
||||
viewportHeight === 0 ||
|
||||
pdfContainerWidth === 0 ||
|
||||
viewportWidth === 0
|
||||
) {
|
||||
return 1
|
||||
}
|
||||
|
||||
let ratio = 1
|
||||
switch (this._zoomScale) {
|
||||
case 'page-fit':
|
||||
ratio = Math.min(
|
||||
pdfContainerHeight / viewportHeight,
|
||||
pdfContainerWidth / viewportWidth
|
||||
)
|
||||
break
|
||||
case 'page-height':
|
||||
ratio = pdfContainerHeight / viewportHeight
|
||||
break
|
||||
case 'page-width':
|
||||
default:
|
||||
ratio = pdfContainerWidth / viewportWidth
|
||||
break
|
||||
}
|
||||
|
||||
return (this._zoom * ratio) / PdfViewerComponent.CSS_UNITS
|
||||
}
|
||||
|
||||
private resetPdfDocument() {
|
||||
this.pdfLinkService.setDocument(this._pdf, null)
|
||||
this.pdfViewer.setDocument(this._pdf!)
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
if (!this.isVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
this.initEventBus()
|
||||
this.setupViewer()
|
||||
}
|
||||
|
||||
private setupResizeListener(): void {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
fromEvent(window, 'resize')
|
||||
.pipe(
|
||||
debounceTime(100),
|
||||
filter(() => this._canAutoResize && !!this._pdf),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.updateSize()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
17
src-ui/src/app/components/common/pdf-viewer/typings.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type PDFPageProxy =
|
||||
import('pdfjs-dist/types/src/display/api').PDFPageProxy
|
||||
export type PDFSource =
|
||||
import('pdfjs-dist/types/src/display/api').DocumentInitParameters
|
||||
export type PDFDocumentProxy =
|
||||
import('pdfjs-dist/types/src/display/api').PDFDocumentProxy
|
||||
export type PDFDocumentLoadingTask =
|
||||
import('pdfjs-dist/types/src/display/api').PDFDocumentLoadingTask
|
||||
export type PDFViewerOptions =
|
||||
import('pdfjs-dist/types/web/pdf_viewer').PDFViewerOptions
|
||||
|
||||
export interface PDFProgressData {
|
||||
loaded: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type ZoomScale = 'page-height' | 'page-fit' | 'page-width'
|
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/utils/event-bus-utils.ts
|
||||
* Created by vadimdez on 21/06/16.
|
||||
*/
|
||||
import { fromEvent, Subject } from 'rxjs'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
|
||||
import type { EventBus } from 'pdfjs-dist/web/pdf_viewer'
|
||||
|
||||
// interface EventBus {
|
||||
// on(eventName: string, listener: Function): void;
|
||||
// off(eventName: string, listener: Function): void;
|
||||
// _listeners: any;
|
||||
// dispatch(eventName: string, data: Object): void;
|
||||
// _on(eventName: any, listener: any, options?: null): void;
|
||||
// _off(eventName: any, listener: any, options?: null): void;
|
||||
// }
|
||||
|
||||
export function createEventBus(pdfJsViewer: any, destroy$: Subject<void>) {
|
||||
const globalEventBus: EventBus = new pdfJsViewer.EventBus()
|
||||
attachDOMEventsToEventBus(globalEventBus, destroy$)
|
||||
return globalEventBus
|
||||
}
|
||||
|
||||
function attachDOMEventsToEventBus(
|
||||
eventBus: EventBus,
|
||||
destroy$: Subject<void>
|
||||
): void {
|
||||
fromEvent(eventBus, 'documentload')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(() => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('documentload', true, true, {})
|
||||
window.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'pagerendered')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ pageNumber, cssTransform, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('pagerendered', true, true, {
|
||||
pageNumber,
|
||||
cssTransform,
|
||||
})
|
||||
source.div.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'textlayerrendered')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ pageNumber, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('textlayerrendered', true, true, { pageNumber })
|
||||
source.textLayerDiv?.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'pagechanging')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ pageNumber, source }: any) => {
|
||||
const event = document.createEvent('UIEvents') as any
|
||||
event.initEvent('pagechanging', true, true)
|
||||
/* tslint:disable:no-string-literal */
|
||||
event['pageNumber'] = pageNumber
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'pagesinit')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('pagesinit', true, true, null)
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'pagesloaded')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ pagesCount, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('pagesloaded', true, true, { pagesCount })
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'scalechange')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ scale, presetValue, source }: any) => {
|
||||
const event = document.createEvent('UIEvents') as any
|
||||
event.initEvent('scalechange', true, true)
|
||||
/* tslint:disable:no-string-literal */
|
||||
event['scale'] = scale
|
||||
/* tslint:disable:no-string-literal */
|
||||
event['presetValue'] = presetValue
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'updateviewarea')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ location, source }: any) => {
|
||||
const event = document.createEvent('UIEvents') as any
|
||||
event.initEvent('updateviewarea', true, true)
|
||||
event['location'] = location
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'find')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(
|
||||
({
|
||||
source,
|
||||
type,
|
||||
query,
|
||||
phraseSearch,
|
||||
caseSensitive,
|
||||
highlightAll,
|
||||
findPrevious,
|
||||
}: any) => {
|
||||
if (source === window) {
|
||||
return // event comes from FirefoxCom, no need to replicate
|
||||
}
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('find' + type, true, true, {
|
||||
query,
|
||||
phraseSearch,
|
||||
caseSensitive,
|
||||
highlightAll,
|
||||
findPrevious,
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
)
|
||||
|
||||
fromEvent(eventBus, 'attachmentsloaded')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ attachmentsCount, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('attachmentsloaded', true, true, {
|
||||
attachmentsCount,
|
||||
})
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'sidebarviewchanged')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ view, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('sidebarviewchanged', true, true, { view })
|
||||
source.outerContainer.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'pagemode')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ mode, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('pagemode', true, true, { mode })
|
||||
source.pdfViewer.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'namedaction')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ action, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('namedaction', true, true, { action })
|
||||
source.pdfViewer.container.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'presentationmodechanged')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ active, switchInProgress }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('presentationmodechanged', true, true, {
|
||||
active,
|
||||
switchInProgress,
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
})
|
||||
|
||||
fromEvent(eventBus, 'outlineloaded')
|
||||
.pipe(takeUntil(destroy$))
|
||||
.subscribe(({ outlineCount, source }: any) => {
|
||||
const event = document.createEvent('CustomEvent')
|
||||
event.initCustomEvent('outlineloaded', true, true, { outlineCount })
|
||||
source.container.dispatchEvent(event)
|
||||
})
|
||||
}
|
@@ -80,7 +80,16 @@ describe('PermissionsDialogComponent', () => {
|
||||
it('should return permissions', () => {
|
||||
expect(component.permissions).toEqual({
|
||||
owner: null,
|
||||
set_permissions: null,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [],
|
||||
groups: [],
|
||||
},
|
||||
change: {
|
||||
users: [],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
component.form.get('permissions_form').setValue(set_permissions)
|
||||
expect(component.permissions).toEqual(set_permissions)
|
||||
|
@@ -52,8 +52,17 @@ export class PermissionsDialogComponent {
|
||||
get permissions() {
|
||||
return {
|
||||
owner: this.form.get('permissions_form').value?.owner ?? null,
|
||||
set_permissions:
|
||||
this.form.get('permissions_form').value?.set_permissions ?? null,
|
||||
set_permissions: this.form.get('permissions_form').value
|
||||
?.set_permissions ?? {
|
||||
view: {
|
||||
users: [],
|
||||
groups: [],
|
||||
},
|
||||
change: {
|
||||
users: [],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,56 @@
|
||||
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody class="p-0 pb-3">
|
||||
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody class="p-0 pb-3">
|
||||
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" i18n>API Auth Token</label>
|
||||
<div class="position-relative">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" formControlName="auth_token" readonly>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
|
||||
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
|
||||
</svg><span class="visually-hidden" i18n>Copy</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
|
||||
</div>
|
||||
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || saveDisabled">Save</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,9 @@
|
||||
::ng-deep {
|
||||
.accordion-body .mb-3 {
|
||||
margin: 0 !important; // hack-ish, for animation
|
||||
}
|
||||
}
|
||||
|
||||
.copied-badge {
|
||||
right: 8em;
|
||||
}
|
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
|
||||
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
|
||||
import { ProfileService } from 'src/app/services/profile.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
NgbAccordionModule,
|
||||
NgbActiveModal,
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { TextComponent } from '../input/text/text.component'
|
||||
import { PasswordComponent } from '../input/password/password.component'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
const profile = {
|
||||
email: 'foo@bar.com',
|
||||
password: '*********',
|
||||
first_name: 'foo',
|
||||
last_name: 'bar',
|
||||
auth_token: '123456789abcdef',
|
||||
}
|
||||
|
||||
describe('ProfileEditDialogComponent', () => {
|
||||
let component: ProfileEditDialogComponent
|
||||
let fixture: ComponentFixture<ProfileEditDialogComponent>
|
||||
let profileService: ProfileService
|
||||
let toastService: ToastService
|
||||
let clipboard: Clipboard
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ProfileEditDialogComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
NgbModalModule,
|
||||
NgbAccordionModule,
|
||||
],
|
||||
})
|
||||
profileService = TestBed.inject(ProfileService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
clipboard = TestBed.inject(Clipboard)
|
||||
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should get profile on init, display in form', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
expect(getSpy).toHaveBeenCalled()
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('email').value).toEqual(profile.email)
|
||||
})
|
||||
|
||||
it('should update profile on save, display error if needed', () => {
|
||||
const newProfile = {
|
||||
email: 'foo@bar2.com',
|
||||
password: profile.password,
|
||||
first_name: 'foo2',
|
||||
last_name: profile.last_name,
|
||||
auth_token: profile.auth_token,
|
||||
}
|
||||
const updateSpy = jest.spyOn(profileService, 'update')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save')))
|
||||
component.save()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
|
||||
updateSpy.mockClear()
|
||||
const infoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
component.form.patchValue(newProfile)
|
||||
updateSpy.mockReturnValueOnce(of(newProfile))
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalledWith(newProfile)
|
||||
expect(infoSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close on cancel', () => {
|
||||
const closeSpy = jest.spyOn(component.activeModal, 'close')
|
||||
component.cancel()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show additional confirmation field when email changes, warn with error & disable save', () => {
|
||||
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
component.form.get('email').patchValue('foo@bar2.com')
|
||||
component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('email_confirm').enabled).toBeTruthy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
'Emails must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeTruthy()
|
||||
|
||||
component.form.get('email_confirm').patchValue('foo@bar2.com')
|
||||
component.onEmailConfirmKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Emails must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
|
||||
component.form.get('email').patchValue(profile.email)
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Emails must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should show additional confirmation field when password changes, warn with error & disable save', () => {
|
||||
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
component.form.get('password').patchValue('new*pass')
|
||||
component.onPasswordKeyUp({
|
||||
target: { value: 'new*pass', tagName: 'input' },
|
||||
} as any)
|
||||
component.onPasswordKeyUp({ target: { tagName: 'button' } } as any) // coverage
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('password_confirm').enabled).toBeTruthy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
'Passwords must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeTruthy()
|
||||
|
||||
component.form.get('password_confirm').patchValue('new*pass')
|
||||
component.onPasswordConfirmKeyUp({ target: { value: 'new*pass' } } as any)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Passwords must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
|
||||
component.form.get('password').patchValue(profile.password)
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Passwords must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should logout on save if password changed', fakeAsync(() => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
component['newPassword'] = 'new*pass'
|
||||
component.form.get('password').patchValue('new*pass')
|
||||
component.form.get('password_confirm').patchValue('new*pass')
|
||||
|
||||
const updateSpy = jest.spyOn(profileService, 'update')
|
||||
updateSpy.mockReturnValue(of(null))
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'http://localhost/',
|
||||
},
|
||||
writable: true, // possibility to override
|
||||
})
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
tick(2600)
|
||||
expect(window.location.href).toContain('logout')
|
||||
}))
|
||||
|
||||
it('should support auth token copy', fakeAsync(() => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||
component.copyAuthToken()
|
||||
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
|
||||
expect(component.copied).toBeTruthy()
|
||||
tick(3000)
|
||||
expect(component.copied).toBeFalsy()
|
||||
}))
|
||||
|
||||
it('should support generate token, display error if needed', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
|
||||
const generateSpy = jest.spyOn(profileService, 'generateAuthToken')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
generateSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('failed to generate'))
|
||||
)
|
||||
component.generateAuthToken()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
|
||||
generateSpy.mockClear()
|
||||
const newToken = '789101112hijk'
|
||||
generateSpy.mockReturnValueOnce(of(newToken))
|
||||
component.generateAuthToken()
|
||||
expect(generateSpy).toHaveBeenCalled()
|
||||
expect(component.form.get('auth_token').value).not.toEqual(
|
||||
profile.auth_token
|
||||
)
|
||||
expect(component.form.get('auth_token').value).toEqual(newToken)
|
||||
})
|
||||
})
|
@@ -0,0 +1,184 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ProfileService } from 'src/app/services/profile.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-profile-edit-dialog',
|
||||
templateUrl: './profile-edit-dialog.component.html',
|
||||
styleUrls: ['./profile-edit-dialog.component.scss'],
|
||||
})
|
||||
export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||
public networkActive: boolean = false
|
||||
public error: any
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
public form = new FormGroup({
|
||||
email: new FormControl(''),
|
||||
email_confirm: new FormControl({ value: null, disabled: true }),
|
||||
password: new FormControl(null),
|
||||
password_confirm: new FormControl({ value: null, disabled: true }),
|
||||
first_name: new FormControl(''),
|
||||
last_name: new FormControl(''),
|
||||
auth_token: new FormControl(''),
|
||||
})
|
||||
|
||||
private currentPassword: string
|
||||
private newPassword: string
|
||||
private passwordConfirm: string
|
||||
public showPasswordConfirm: boolean = false
|
||||
|
||||
private currentEmail: string
|
||||
private newEmail: string
|
||||
private emailConfirm: string
|
||||
public showEmailConfirm: boolean = false
|
||||
|
||||
public copied: boolean = false
|
||||
|
||||
constructor(
|
||||
private profileService: ProfileService,
|
||||
public activeModal: NgbActiveModal,
|
||||
private toastService: ToastService,
|
||||
private clipboard: Clipboard
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.networkActive = true
|
||||
this.profileService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((profile) => {
|
||||
this.networkActive = false
|
||||
this.form.patchValue(profile)
|
||||
this.currentEmail = profile.email
|
||||
this.form.get('email').valueChanges.subscribe((newEmail) => {
|
||||
this.newEmail = newEmail
|
||||
this.onEmailChange()
|
||||
})
|
||||
this.currentPassword = profile.password
|
||||
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
||||
this.newPassword = newPassword
|
||||
this.onPasswordChange()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
get saveDisabled(): boolean {
|
||||
return this.error?.password_confirm || this.error?.email_confirm
|
||||
}
|
||||
|
||||
onEmailKeyUp(event: KeyboardEvent): void {
|
||||
this.newEmail = (event.target as HTMLInputElement)?.value
|
||||
this.onEmailChange()
|
||||
}
|
||||
|
||||
onEmailConfirmKeyUp(event: KeyboardEvent): void {
|
||||
this.emailConfirm = (event.target as HTMLInputElement)?.value
|
||||
this.onEmailChange()
|
||||
}
|
||||
|
||||
onEmailChange(): void {
|
||||
this.showEmailConfirm = this.currentEmail !== this.newEmail
|
||||
if (this.showEmailConfirm) {
|
||||
this.form.get('email_confirm').enable()
|
||||
if (this.newEmail !== this.emailConfirm) {
|
||||
if (!this.error) this.error = {}
|
||||
this.error.email_confirm = $localize`Emails must match`
|
||||
} else {
|
||||
delete this.error?.email_confirm
|
||||
}
|
||||
} else {
|
||||
this.form.get('email_confirm').disable()
|
||||
delete this.error?.email_confirm
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordKeyUp(event: KeyboardEvent): void {
|
||||
if ((event.target as HTMLElement).tagName !== 'input') return // toggle button can trigger this handler
|
||||
this.newPassword = (event.target as HTMLInputElement)?.value
|
||||
this.onPasswordChange()
|
||||
}
|
||||
|
||||
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
|
||||
this.passwordConfirm = (event.target as HTMLInputElement)?.value
|
||||
this.onPasswordChange()
|
||||
}
|
||||
|
||||
onPasswordChange(): void {
|
||||
this.showPasswordConfirm = this.currentPassword !== this.newPassword
|
||||
|
||||
if (this.showPasswordConfirm) {
|
||||
this.form.get('password_confirm').enable()
|
||||
if (this.newPassword !== this.passwordConfirm) {
|
||||
if (!this.error) this.error = {}
|
||||
this.error.password_confirm = $localize`Passwords must match`
|
||||
} else {
|
||||
delete this.error?.password_confirm
|
||||
}
|
||||
} else {
|
||||
this.form.get('password_confirm').disable()
|
||||
delete this.error?.password_confirm
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const passwordChanged = this.currentPassword !== this.newPassword
|
||||
const profile = Object.assign({}, this.form.value)
|
||||
this.networkActive = true
|
||||
this.profileService
|
||||
.update(profile)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Profile updated successfully`)
|
||||
if (passwordChanged) {
|
||||
this.toastService.showInfo(
|
||||
$localize`Password has been changed, you will be logged out momentarily.`
|
||||
)
|
||||
setTimeout(() => {
|
||||
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
||||
}, 2500)
|
||||
}
|
||||
this.activeModal.close()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError($localize`Error saving profile`, error)
|
||||
this.networkActive = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
generateAuthToken(): void {
|
||||
this.profileService.generateAuthToken().subscribe({
|
||||
next: (token: string) => {
|
||||
this.form.patchValue({ auth_token: token })
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error generating auth token`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
copyAuthToken(): void {
|
||||
this.clipboard.copy(this.form.get('auth_token').value)
|
||||
this.copied = true
|
||||
setTimeout(() => {
|
||||
this.copied = false
|
||||
}, 3000)
|
||||
}
|
||||
}
|
@@ -37,13 +37,13 @@
|
||||
</li>
|
||||
<li class="list-group-item pt-3 pb-2">
|
||||
<div class="input-group input-group-sm w-100">
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [(ngModel)]="archiveVersion">
|
||||
<label class="form-check-label small" for="versionSwitch" i18n>Share archive version</label>
|
||||
<div class="form-check form-switch ms-auto small">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion">
|
||||
<label class="form-check-label" for="versionSwitch" i18n>Share archive version</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group input-group-sm w-100 mt-2">
|
||||
<label class="input-group-text" for="addLink">Expires:</label>
|
||||
<label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label>
|
||||
<select class="form-select form-select-sm" [(ngModel)]="expirationDays">
|
||||
<option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
|
@@ -19,6 +19,7 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ShareLinksDropdownComponent } from './share-links-dropdown.component'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
describe('ShareLinksDropdownComponent', () => {
|
||||
let component: ShareLinksDropdownComponent
|
||||
@@ -88,7 +89,7 @@ describe('ShareLinksDropdownComponent', () => {
|
||||
.mockReturnValueOnce(throwError(() => new Error('Unable to get links')))
|
||||
component.documentId = 99
|
||||
|
||||
component.refresh()
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
@@ -97,12 +98,13 @@ describe('ShareLinksDropdownComponent', () => {
|
||||
const createSpy = jest.spyOn(shareLinkService, 'createLinkForDocument')
|
||||
component.documentId = 99
|
||||
component.expirationDays = 7
|
||||
component.archiveVersion = false
|
||||
component.useArchiveVersion = false
|
||||
|
||||
const expiration = new Date()
|
||||
expiration.setDate(expiration.getDate() + 7)
|
||||
|
||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||
copySpy.mockReturnValue(true)
|
||||
const refreshSpy = jest.spyOn(component, 'refresh')
|
||||
|
||||
component.createLink()
|
||||
@@ -117,8 +119,10 @@ describe('ShareLinksDropdownComponent', () => {
|
||||
fixture.detectChanges()
|
||||
tick(3000)
|
||||
|
||||
expect(copySpy).toHaveBeenCalled()
|
||||
expect(refreshSpy).toHaveBeenCalled()
|
||||
expect(copySpy).toHaveBeenCalled()
|
||||
expect(component.copied).toEqual(1)
|
||||
tick(100) // copy timeout
|
||||
}))
|
||||
|
||||
it('should show error on link creation if needed', () => {
|
||||
@@ -192,4 +196,36 @@ describe('ShareLinksDropdownComponent', () => {
|
||||
component.share(link)
|
||||
// expect(navigatorSpy).toHaveBeenCalledWith({ url: component.getShareUrl(link) })
|
||||
})
|
||||
|
||||
it('should correctly generate share URLs', () => {
|
||||
environment.apiBaseUrl = 'http://example.com/api/'
|
||||
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
|
||||
'http://example.com/share/123abc123'
|
||||
)
|
||||
environment.apiBaseUrl = 'http://example.domainwithapiinit.com/api/'
|
||||
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
|
||||
'http://example.domainwithapiinit.com/share/123abc123'
|
||||
)
|
||||
environment.apiBaseUrl = 'http://example.domainwithapiinit.com:1234/api/'
|
||||
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
|
||||
'http://example.domainwithapiinit.com:1234/share/123abc123'
|
||||
)
|
||||
environment.apiBaseUrl =
|
||||
'http://example.domainwithapiinit.com:1234/subpath/api/'
|
||||
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
|
||||
'http://example.domainwithapiinit.com:1234/subpath/share/123abc123'
|
||||
)
|
||||
})
|
||||
|
||||
it('should disable archive switch & option if no archive available', () => {
|
||||
component.hasArchiveVersion = false
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
expect(component.useArchiveVersion).toBeFalsy()
|
||||
expect(
|
||||
fixture.debugElement.query(By.css("input[type='checkbox']")).attributes[
|
||||
'ng-reflect-is-disabled'
|
||||
]
|
||||
).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
@@ -38,6 +38,9 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
@Input()
|
||||
hasArchiveVersion: boolean = true
|
||||
|
||||
shareLinks: PaperlessShareLink[]
|
||||
|
||||
loading: boolean = false
|
||||
@@ -46,7 +49,7 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
|
||||
expirationDays: number = 7
|
||||
|
||||
archiveVersion: boolean = true
|
||||
useArchiveVersion: boolean = true
|
||||
|
||||
constructor(
|
||||
private shareLinkService: ShareLinkService,
|
||||
@@ -56,6 +59,7 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this._documentId !== undefined) this.refresh()
|
||||
this.useArchiveVersion = this.hasArchiveVersion
|
||||
}
|
||||
|
||||
refresh() {
|
||||
@@ -80,7 +84,10 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
}
|
||||
|
||||
getShareUrl(link: PaperlessShareLink): string {
|
||||
return `${environment.apiBaseUrl.replace('api', 'share')}${link.slug}`
|
||||
const apiURL = new URL(environment.apiBaseUrl)
|
||||
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
|
||||
link.slug
|
||||
}`
|
||||
}
|
||||
|
||||
getDaysRemaining(link: PaperlessShareLink): string {
|
||||
@@ -91,11 +98,13 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
}
|
||||
|
||||
copy(link: PaperlessShareLink) {
|
||||
this.clipboard.copy(this.getShareUrl(link))
|
||||
this.copied = link.id
|
||||
setTimeout(() => {
|
||||
this.copied = null
|
||||
}, 3000)
|
||||
const success = this.clipboard.copy(this.getShareUrl(link))
|
||||
if (success) {
|
||||
this.copied = link.id
|
||||
setTimeout(() => {
|
||||
this.copied = null
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
canShare(link: PaperlessShareLink): boolean {
|
||||
@@ -129,7 +138,7 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
this.shareLinkService
|
||||
.createLinkForDocument(
|
||||
this._documentId,
|
||||
this.archiveVersion
|
||||
this.useArchiveVersion
|
||||
? PaperlessFileVersion.Archive
|
||||
: PaperlessFileVersion.Original,
|
||||
expiration
|
||||
@@ -137,7 +146,9 @@ export class ShareLinksDropdownComponent implements OnInit {
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.loading = false
|
||||
this.copy(result)
|
||||
setTimeout(() => {
|
||||
this.copy(result)
|
||||
}, 10)
|
||||
this.refresh()
|
||||
},
|
||||
error: (e) => {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
</pngx-page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto col-lg-8 col-xl-9 mb-4">
|
||||
<div class="col-12 col-lg-8 col-xl-9 mb-4">
|
||||
<div class="row row-cols-1 g-4" tourAnchor="tour.dashboard"
|
||||
cdkDropList
|
||||
[cdkDropListDisabled]="settingsService.globalDropzoneActive"
|
||||
|
@@ -63,7 +63,7 @@
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||
<a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
|
||||
<a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/">
|
||||
<ng-container i18n>Storage Paths</ng-container>:
|
||||
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
|
||||
</a>
|
||||
|