mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge branch 'dev' into feature-consume-eml
This commit is contained in:
		
							
								
								
									
										49
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										49
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -7,34 +7,34 @@ body: | ||||
|     attributes: | ||||
|       value: | | ||||
|         Have a question? 👉 [Start a new discussion](https://github.com/paperless-ngx/paperless-ngx/discussions/new) or [ask in chat](https://matrix.to/#/#paperless:adnidor.de). | ||||
|          | ||||
|         Before opening an issue, please check [the documentation](https://paperless-ngx.readthedocs.io/en/latest/troubleshooting.html) and see if it helps you resolve your issue. Please also make sure that you followed the installation instructions. | ||||
|          | ||||
|         If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support). Remember that Paperless successfully runs on a variety of different systems. If Paperless-ngx does not start, it's likely an issue with your system, not an issue of Paperless-ngx. | ||||
|          | ||||
|         Finally, please search issues and discussions before opening a new bug report. | ||||
|  | ||||
|         Before opening an issue, please double check: | ||||
|  | ||||
|         - [The troubleshooting documentation](https://paperless-ngx.readthedocs.io/en/latest/troubleshooting.html). | ||||
|         - [The installation instructions](https://paperless-ngx.readthedocs.io/en/latest/setup.html#installation). | ||||
|         - [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues). | ||||
|  | ||||
|         If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support). | ||||
|   - type: textarea | ||||
|     id: description | ||||
|     attributes: | ||||
|       label: Description | ||||
|       description: A clear and concise description of what the bug is. | ||||
|       placeholder: Currently... | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: expected-behavior | ||||
|     attributes: | ||||
|       label: Expected behavior | ||||
|       description: A clear and concise description of what you expected to happen. | ||||
|       placeholder: In this situation... | ||||
|       description: A clear and concise description of what the bug is. If applicable, add screenshots to help explain your problem. | ||||
|       placeholder: | | ||||
|         Currently Paperless does not work when... | ||||
|  | ||||
|         [Screenshot if applicable] | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: reproduction | ||||
|     attributes: | ||||
|       label: Steps to reproduce | ||||
|       description: Steps to reproduce the behavior | ||||
|       placeholder: "1. Go to '...', 2. Click on '....', 3. See error" | ||||
|       description: Steps to reproduce the behavior. | ||||
|       placeholder: | | ||||
|         1. Go to '...' | ||||
|         2. Click on '....' | ||||
|         3. See error | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
| @@ -43,11 +43,6 @@ body: | ||||
|       label: Webserver logs | ||||
|       description: If available, post any logs from the web server related to your issue. | ||||
|       render: bash | ||||
|   - type: textarea | ||||
|     id: screenshots | ||||
|     attributes: | ||||
|       label: Screenshots | ||||
|       description: If applicable, add screenshots to help explain your problem. | ||||
|   - type: input | ||||
|     id: version | ||||
|     attributes: | ||||
| @@ -59,8 +54,8 @@ body: | ||||
|     id: host-os | ||||
|     attributes: | ||||
|       label: Host OS | ||||
|       description: Host OS of the machine running paperless-ngx | ||||
|       placeholder: e.g. Archlinux / Ubuntu 20.04 | ||||
|       description: Host OS of the machine running paperless-ngx. Please add the architecture (uname -m) if applicable. | ||||
|       placeholder: e.g. Archlinux / Ubuntu 20.04 / Raspberry Pi `arm64` | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: dropdown | ||||
| @@ -77,7 +72,7 @@ body: | ||||
|     id: browser | ||||
|     attributes: | ||||
|       label: Browser | ||||
|       description: Which browser you are using, if relevant | ||||
|       description: Which browser you are using, if relevant. | ||||
|       placeholder: e.g. Chrome, Safari | ||||
|   - type: input | ||||
|     id: config-changes | ||||
| @@ -88,4 +83,4 @@ body: | ||||
|     id: other | ||||
|     attributes: | ||||
|       label: Other | ||||
|       description: Any other relevant details | ||||
|       description: Any other relevant details. | ||||
|   | ||||
							
								
								
									
										11
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -6,11 +6,14 @@ updates: | ||||
|   # Enable version updates for npm | ||||
|   - package-ecosystem: "npm" | ||||
|     target-branch: "dev" | ||||
|     # Look for `package.json` and `lock` files in the `root` directory | ||||
|     # Look for `package.json` and `lock` files in the `/src-ui` directory | ||||
|     directory: "/src-ui" | ||||
|     # Check the npm registry for updates every month | ||||
|     schedule: | ||||
|       interval: "monthly" | ||||
|     labels: | ||||
|       - "frontend" | ||||
|       - "dependencies" | ||||
|     # Add reviewers | ||||
|     reviewers: | ||||
|       - "paperless-ngx/frontend" | ||||
| @@ -26,9 +29,13 @@ updates: | ||||
|     labels: | ||||
|       - "backend" | ||||
|       - "dependencies" | ||||
|     # Add reviewers | ||||
|     reviewers: | ||||
|       - "paperless-ngx/backend" | ||||
|  | ||||
|   # Enable updates for Github Actions | ||||
|   - package-ecosystem: "github-actions" | ||||
|     target-branch: "dev" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       # Check for updates to GitHub Actions every month | ||||
| @@ -38,4 +45,4 @@ updates: | ||||
|       - "dependencies" | ||||
|     # Add reviewers | ||||
|     reviewers: | ||||
|       - "paperless-ngx/backend" | ||||
|       - "paperless-ngx/ci-cd" | ||||
|   | ||||
							
								
								
									
										5
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -28,9 +28,10 @@ include-labels: | ||||
| replacers: # Changes "Feature: Update checker" to "Update checker" | ||||
|   - search: '/Feature:|Feat:|\[feature\]/gi' | ||||
|     replace: '' | ||||
| change-template: '- $TITLE @$AUTHOR (#$NUMBER)' | ||||
| category-template: '### $TITLE' | ||||
| change-template: '- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))' | ||||
| change-title-escapes: '\<*_&#@' | ||||
| template: | | ||||
|   # Changelog | ||||
|   ## paperless-ngx $RESOLVED_VERSION | ||||
|  | ||||
|   $CHANGES | ||||
|   | ||||
							
								
								
									
										77
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										77
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -57,6 +57,12 @@ jobs: | ||||
|     name: Prepare Docker Pipeline Data | ||||
|     if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v')) | ||||
|     runs-on: ubuntu-20.04 | ||||
|     # If the push triggered the installer library workflow, wait for it to | ||||
|     # complete here.  This ensures the required versions for the final | ||||
|     # image have been built, while not waiting at all if the versions haven't changed | ||||
|     concurrency: | ||||
|       group: build-installer-library | ||||
|       cancel-in-progress: false | ||||
|     needs: | ||||
|       - documentation | ||||
|       - ci-backend | ||||
| @@ -117,55 +123,6 @@ jobs: | ||||
|  | ||||
|       jbig2enc-json: ${{ steps.jbig2enc-setup.outputs.jbig2enc-json}} | ||||
|  | ||||
|   build-qpdf-debs: | ||||
|     name: qpdf | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.qpdf | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }} | ||||
|       build-args: | | ||||
|         QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }} | ||||
|  | ||||
|   build-jbig2enc: | ||||
|     name: jbig2enc | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.jbig2enc | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.jbig2enc-json }} | ||||
|       build-args: | | ||||
|         JBIG2ENC_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }} | ||||
|  | ||||
|   build-psycopg2-wheel: | ||||
|     name: psycopg2 | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.psycopg2 | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }} | ||||
|       build-args: | | ||||
|         PSYCOPG2_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).git_tag }} | ||||
|         PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }} | ||||
|  | ||||
|   build-pikepdf-wheel: | ||||
|     name: pikepdf | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|       - build-qpdf-debs | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.pikepdf | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.pikepdf-json }} | ||||
|       build-args: | | ||||
|         REPO=${{ github.repository }} | ||||
|         QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }} | ||||
|         PIKEPDF_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).git_tag }} | ||||
|         PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }} | ||||
|  | ||||
|   # build and push image to docker hub. | ||||
|   build-docker-image: | ||||
|     runs-on: ubuntu-20.04 | ||||
| @@ -174,10 +131,6 @@ jobs: | ||||
|       cancel-in-progress: true | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|       - build-psycopg2-wheel | ||||
|       - build-jbig2enc | ||||
|       - build-qpdf-debs | ||||
|       - build-pikepdf-wheel | ||||
|     steps: | ||||
|       - | ||||
|         name: Check pushing to Docker Hub | ||||
| @@ -381,3 +334,21 @@ jobs: | ||||
|           asset_path: ./paperless-ngx.tar.xz | ||||
|           asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz | ||||
|           asset_content_type: application/x-xz | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           ref: main | ||||
|       - | ||||
|         name: Append Changelog to docs | ||||
|         id: append-Changelog | ||||
|         working-directory: docs | ||||
|         run: | | ||||
|           echo -e "# Changelog\n\n${{ steps.create-release.outputs.body }}\n" > changelog-new.md | ||||
|           CURRENT_CHANGELOG=`tail --lines +2 changelog.md` | ||||
|           echo -e "$CURRENT_CHANGELOG" >> changelog-new.md | ||||
|           mv changelog-new.md changelog.md | ||||
|           git config --global user.name "github-actions" | ||||
|           git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||||
|           git commit -am "Changelog ${{ steps.get_version.outputs.version }} - GHA" | ||||
|           git push origin HEAD:main | ||||
|   | ||||
							
								
								
									
										141
									
								
								.github/workflows/installer-library.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								.github/workflows/installer-library.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| # This workflow will run to update the installer library of | ||||
| # Docker images.  These are the images which provide updated wheels | ||||
| # .deb installation packages or maybe just some compiled library | ||||
|  | ||||
| name: Build Image Library | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     # Must match one of these branches AND one of the paths | ||||
|     # to be triggered | ||||
|     branches: | ||||
|       - "main" | ||||
|       - "dev" | ||||
|       - "library-*" | ||||
|       - "feature-*" | ||||
|     paths: | ||||
|       # Trigger the workflow if a Dockerfile changed | ||||
|       - "docker-builders/**" | ||||
|       # Trigger if a package was updated | ||||
|       - ".build-config.json" | ||||
|       - "Pipfile.lock" | ||||
|       # Also trigger on workflow changes related to the library | ||||
|       - ".github/workflows/installer-library.yml" | ||||
|       - ".github/workflows/reusable-workflow-builder.yml" | ||||
|       - ".github/scripts/**" | ||||
|  | ||||
| # Set a workflow level concurrency group so primary workflow | ||||
| # can wait for this to complete if needed | ||||
| # DO NOT CHANGE without updating main workflow group | ||||
| concurrency: | ||||
|   group: build-installer-library | ||||
|   cancel-in-progress: false | ||||
|  | ||||
| jobs: | ||||
|   prepare-docker-build: | ||||
|     name: Prepare Docker Image Version Data | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|       - | ||||
|         name: Set up Python | ||||
|         uses: actions/setup-python@v3 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - | ||||
|         name: Setup qpdf image | ||||
|         id: qpdf-setup | ||||
|         run: | | ||||
|           build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py qpdf) | ||||
|  | ||||
|           echo ${build_json} | ||||
|  | ||||
|           echo ::set-output name=qpdf-json::${build_json} | ||||
|       - | ||||
|         name: Setup psycopg2 image | ||||
|         id: psycopg2-setup | ||||
|         run: | | ||||
|           build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py psycopg2) | ||||
|  | ||||
|           echo ${build_json} | ||||
|  | ||||
|           echo ::set-output name=psycopg2-json::${build_json} | ||||
|       - | ||||
|         name: Setup pikepdf image | ||||
|         id: pikepdf-setup | ||||
|         run: | | ||||
|           build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py pikepdf) | ||||
|  | ||||
|           echo ${build_json} | ||||
|  | ||||
|           echo ::set-output name=pikepdf-json::${build_json} | ||||
|       - | ||||
|         name: Setup jbig2enc image | ||||
|         id: jbig2enc-setup | ||||
|         run: | | ||||
|           build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py jbig2enc) | ||||
|  | ||||
|           echo ${build_json} | ||||
|  | ||||
|           echo ::set-output name=jbig2enc-json::${build_json} | ||||
|  | ||||
|     outputs: | ||||
|  | ||||
|       qpdf-json: ${{ steps.qpdf-setup.outputs.qpdf-json }} | ||||
|  | ||||
|       pikepdf-json: ${{ steps.pikepdf-setup.outputs.pikepdf-json }} | ||||
|  | ||||
|       psycopg2-json: ${{ steps.psycopg2-setup.outputs.psycopg2-json }} | ||||
|  | ||||
|       jbig2enc-json: ${{ steps.jbig2enc-setup.outputs.jbig2enc-json}} | ||||
|  | ||||
|   build-qpdf-debs: | ||||
|     name: qpdf | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.qpdf | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }} | ||||
|       build-args: | | ||||
|         QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }} | ||||
|  | ||||
|   build-jbig2enc: | ||||
|     name: jbig2enc | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.jbig2enc | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.jbig2enc-json }} | ||||
|       build-args: | | ||||
|         JBIG2ENC_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }} | ||||
|  | ||||
|   build-psycopg2-wheel: | ||||
|     name: psycopg2 | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.psycopg2 | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }} | ||||
|       build-args: | | ||||
|         PSYCOPG2_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).git_tag }} | ||||
|         PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }} | ||||
|  | ||||
|   build-pikepdf-wheel: | ||||
|     name: pikepdf | ||||
|     needs: | ||||
|       - prepare-docker-build | ||||
|       - build-qpdf-debs | ||||
|     uses: ./.github/workflows/reusable-workflow-builder.yml | ||||
|     with: | ||||
|       dockerfile: ./docker-builders/Dockerfile.pikepdf | ||||
|       build-json: ${{ needs.prepare-docker-build.outputs.pikepdf-json }} | ||||
|       build-args: | | ||||
|         REPO=${{ github.repository }} | ||||
|         QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }} | ||||
|         PIKEPDF_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).git_tag }} | ||||
|         PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }} | ||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -63,6 +63,8 @@ target/ | ||||
|  | ||||
| # VS Code | ||||
| .vscode | ||||
| /src-ui/.vscode | ||||
| /docs/.vscode | ||||
|  | ||||
| # Other stuff that doesn't belong | ||||
| .virtualenv | ||||
| @@ -84,8 +86,9 @@ scripts/nuke | ||||
| /paperless.conf | ||||
| /consume/ | ||||
| /export/ | ||||
| /src-ui/.vscode | ||||
|  | ||||
| # this is where the compiled frontend is moved to. | ||||
| /src/documents/static/frontend/ | ||||
| /docs/.vscode/settings.json | ||||
|  | ||||
| # mac os | ||||
| .DS_Store | ||||
|   | ||||
| @@ -37,7 +37,7 @@ repos: | ||||
|         exclude: "(^Pipfile\\.lock$)" | ||||
|   # Python hooks | ||||
|   - repo: https://github.com/asottile/reorder_python_imports | ||||
|     rev: v3.0.1 | ||||
|     rev: v3.1.0 | ||||
|     hooks: | ||||
|       - id: reorder-python-imports | ||||
|         exclude: "(migrations)" | ||||
| @@ -62,6 +62,13 @@ repos: | ||||
|     rev: 22.3.0 | ||||
|     hooks: | ||||
|       - id: black | ||||
|   - repo: https://github.com/asottile/pyupgrade | ||||
|     rev: v2.32.1 | ||||
|     hooks: | ||||
|       - id: pyupgrade | ||||
|         exclude: "(migrations)" | ||||
|         args: | ||||
|           - "--py38-plus" | ||||
|   # Dockerfile hooks | ||||
|   - repo: https://github.com/AleksaC/hadolint-py | ||||
|     rev: v2.10.0 | ||||
|   | ||||
							
								
								
									
										139
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,3 +1,5 @@ | ||||
| # syntax=docker/dockerfile:1.4 | ||||
|  | ||||
| # Pull the installer images from the library | ||||
| # These are all built previously | ||||
| # They provide either a .deb or .whl | ||||
| @@ -24,7 +26,7 @@ COPY ./src-ui /src/src-ui | ||||
| WORKDIR /src/src-ui | ||||
| RUN set -eux \ | ||||
|   && npm update npm -g \ | ||||
|   && npm ci --no-optional | ||||
|   && npm ci --omit=optional | ||||
| RUN set -eux \ | ||||
|   && ./node_modules/.bin/ng build --configuration production | ||||
|  | ||||
| @@ -38,11 +40,16 @@ LABEL org.opencontainers.image.licenses="GPL-3.0-only" | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
| # Packages needed only for building | ||||
| ARG BUILD_PACKAGES="\ | ||||
|   build-essential \ | ||||
|   git \ | ||||
|   python3-dev" | ||||
| # | ||||
| # Begin installation and configuration | ||||
| # Order the steps below from least often changed to most | ||||
| # | ||||
|  | ||||
| # copy jbig2enc | ||||
| # Basically will never change again | ||||
| COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/ | ||||
| COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/jbig2 /usr/local/bin/ | ||||
| COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/*.h /usr/local/include/ | ||||
|  | ||||
| # Packages need for running | ||||
| ARG RUNTIME_PACKAGES="\ | ||||
| @@ -94,45 +101,81 @@ ARG RUNTIME_PACKAGES="\ | ||||
|   libzbar0 \ | ||||
|   poppler-utils" | ||||
|  | ||||
| WORKDIR /usr/src/paperless/src/ | ||||
| # Install basic runtime packages. | ||||
| # These change very infrequently | ||||
| RUN set -eux \ | ||||
|   echo "Installing system packages" \ | ||||
|     && apt-get update \ | ||||
|     && apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \ | ||||
|     && rm -rf /var/lib/apt/lists/* \ | ||||
|   && echo "Installing supervisor" \ | ||||
|     && python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor==4.2.4 | ||||
|  | ||||
| # Copy qpdf and runtime library | ||||
| COPY --from=qpdf-builder /usr/src/qpdf/libqpdf28_*.deb ./ | ||||
| COPY --from=qpdf-builder /usr/src/qpdf/qpdf_*.deb ./ | ||||
| # Copy gunicorn config | ||||
| # Changes very infrequently | ||||
| WORKDIR /usr/src/paperless/ | ||||
|  | ||||
| # Copy pikepdf wheel and dependencies | ||||
| COPY --from=pikepdf-builder /usr/src/pikepdf/wheels/*.whl ./ | ||||
| COPY gunicorn.conf.py . | ||||
|  | ||||
| # Copy psycopg2 wheel | ||||
| COPY --from=psycopg2-builder /usr/src/psycopg2/wheels/psycopg2*.whl ./ | ||||
| # setup docker-specific things | ||||
| # Use mounts to avoid copying installer files into the image | ||||
| # These change sometimes, but rarely | ||||
| WORKDIR /usr/src/paperless/src/docker/ | ||||
|  | ||||
| # copy jbig2enc | ||||
| COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/ | ||||
| COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/jbig2 /usr/local/bin/ | ||||
| COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/*.h /usr/local/include/ | ||||
| RUN --mount=type=bind,readwrite,source=docker,target=./ \ | ||||
|   set -eux \ | ||||
|   && echo "Configuring ImageMagick" \ | ||||
|     && cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ | ||||
|   && echo "Configuring supervisord" \ | ||||
|     && mkdir /var/log/supervisord /var/run/supervisord \ | ||||
|     && cp supervisord.conf /etc/supervisord.conf \ | ||||
|   && echo "Setting up Docker scripts" \ | ||||
|     && cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \ | ||||
|     && chmod 755 /sbin/docker-entrypoint.sh \ | ||||
|     && cp docker-prepare.sh /sbin/docker-prepare.sh \ | ||||
|     && chmod 755 /sbin/docker-prepare.sh \ | ||||
|     && cp wait-for-redis.py /sbin/wait-for-redis.py \ | ||||
|     && chmod 755 /sbin/wait-for-redis.py \ | ||||
|   && echo "Installing managment commands" \ | ||||
|     && chmod +x install_management_commands.sh \ | ||||
|     && ./install_management_commands.sh | ||||
|  | ||||
| COPY requirements.txt ../ | ||||
| # Install the built packages from the installer library images | ||||
| # Use mounts to avoid copying installer files into the image | ||||
| # These change sometimes | ||||
| RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \ | ||||
|     --mount=type=bind,from=psycopg2-builder,target=/psycopg2 \ | ||||
|     --mount=type=bind,from=pikepdf-builder,target=/pikepdf \ | ||||
|   set -eux \ | ||||
|   && echo "Installing qpdf" \ | ||||
|     && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf28_*.deb \ | ||||
|     && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \ | ||||
|   && echo "Installing pikepdf and dependencies" \ | ||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/packaging*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/lxml*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/Pillow*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/pyparsing*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/pikepdf*.whl \ | ||||
|     && python -m pip list \ | ||||
|   && echo "Installing psycopg2" \ | ||||
|     && python3 -m pip install --no-cache-dir /psycopg2/usr/src/psycopg2/wheels/psycopg2*.whl \ | ||||
|     && python -m pip list | ||||
|  | ||||
| # Python dependencies | ||||
| # Change pretty frequently | ||||
| COPY requirements.txt ../ | ||||
|  | ||||
| # Packages needed only for building a few quick Python | ||||
| # dependencies | ||||
| ARG BUILD_PACKAGES="\ | ||||
|   build-essential \ | ||||
|   python3-dev" | ||||
|  | ||||
| RUN set -eux \ | ||||
|   && apt-get update \ | ||||
|   && apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} ${BUILD_PACKAGES} \ | ||||
|   && python3 -m pip install --no-cache-dir --upgrade wheel \ | ||||
|   && echo "Installing qpdf" \ | ||||
|     && apt-get install --yes --no-install-recommends ./libqpdf28_*.deb \ | ||||
|     && apt-get install --yes --no-install-recommends ./qpdf_*.deb \ | ||||
|   && echo "Installing pikepdf and dependencies wheel" \ | ||||
|     && python3 -m pip install --no-cache-dir packaging*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir lxml*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir Pillow*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir pyparsing*.whl \ | ||||
|     && python3 -m pip install --no-cache-dir pikepdf*.whl \ | ||||
|     && python -m pip list \ | ||||
|   && echo "Installing psycopg2 wheel" \ | ||||
|     && python3 -m pip install --no-cache-dir psycopg2*.whl \ | ||||
|     && python -m pip list \ | ||||
|   && echo "Installing supervisor" \ | ||||
|     && python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \ | ||||
|   && echo "Installing build system packages" \ | ||||
|     && apt-get update \ | ||||
|     && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ | ||||
|     && python3 -m pip install --no-cache-dir --upgrade wheel \ | ||||
|   && echo "Installing Python requirements" \ | ||||
|     && python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \ | ||||
|   && echo "Cleaning up image" \ | ||||
| @@ -145,28 +188,6 @@ RUN set -eux \ | ||||
|     && rm -rf /var/cache/apt/archives/* \ | ||||
|     && truncate -s 0 /var/log/*log | ||||
|  | ||||
| # setup docker-specific things | ||||
| COPY docker/ ./docker/ | ||||
|  | ||||
| WORKDIR /usr/src/paperless/src/docker/ | ||||
|  | ||||
| RUN set -eux \ | ||||
|   && cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ | ||||
|   && mkdir /var/log/supervisord /var/run/supervisord \ | ||||
|   && cp supervisord.conf /etc/supervisord.conf \ | ||||
|   && cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \ | ||||
|   && chmod 755 /sbin/docker-entrypoint.sh \ | ||||
|   && cp docker-prepare.sh /sbin/docker-prepare.sh \ | ||||
|   && chmod 755 /sbin/docker-prepare.sh \ | ||||
|   && cp wait-for-redis.py /sbin/wait-for-redis.py \ | ||||
|   && chmod 755 /sbin/wait-for-redis.py \ | ||||
|   && chmod +x install_management_commands.sh \ | ||||
|   && ./install_management_commands.sh | ||||
|  | ||||
| WORKDIR /usr/src/paperless/ | ||||
|  | ||||
| COPY gunicorn.conf.py . | ||||
|  | ||||
| WORKDIR /usr/src/paperless/src/ | ||||
|  | ||||
| # copy backend | ||||
|   | ||||
							
								
								
									
										4
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -19,7 +19,7 @@ djangorestframework = "~=3.13" | ||||
| filelock = "*" | ||||
| fuzzywuzzy = {extras = ["speedup"], version = "*"} | ||||
| gunicorn = "*" | ||||
| imap-tools = "~=0.54.0" | ||||
| imap-tools = "*" | ||||
| langdetect = "*" | ||||
| pathvalidate = "*" | ||||
| pillow = "~=9.1" | ||||
| @@ -70,3 +70,5 @@ sphinx_rtd_theme = "*" | ||||
| tox = "*" | ||||
| black = "*" | ||||
| pre-commit = "*" | ||||
| sphinx-autobuild = "*" | ||||
| myst-parser = "*" | ||||
|   | ||||
							
								
								
									
										444
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										444
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "9573af313c811561d467d814c52c6bd1439bc48e3b31d7f56afed5f0ebe4b648" | ||||
|             "sha256": "818f3513df4a757e6302baf5a17ce61e85c7d69a7666e7d49e7e50e78e064ae3" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": {}, | ||||
| @@ -28,11 +28,11 @@ | ||||
|         }, | ||||
|         "anyio": { | ||||
|             "hashes": [ | ||||
|                 "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6", | ||||
|                 "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e" | ||||
|                 "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", | ||||
|                 "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" | ||||
|             ], | ||||
|             "markers": "python_full_version >= '3.6.2'", | ||||
|             "version": "==3.5.0" | ||||
|             "version": "==3.6.1" | ||||
|         }, | ||||
|         "arrow": { | ||||
|             "hashes": [ | ||||
| @@ -44,11 +44,11 @@ | ||||
|         }, | ||||
|         "asgiref": { | ||||
|             "hashes": [ | ||||
|                 "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1", | ||||
|                 "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865" | ||||
|                 "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", | ||||
|                 "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==3.5.1" | ||||
|             "version": "==3.5.2" | ||||
|         }, | ||||
|         "async-timeout": { | ||||
|             "hashes": [ | ||||
| @@ -68,10 +68,10 @@ | ||||
|         }, | ||||
|         "autobahn": { | ||||
|             "hashes": [ | ||||
|                 "sha256:58a887c7a196bb08d8b6624cb3695f493a9e5c9f00fd350d8d6f829b47ff9036" | ||||
|                 "sha256:57b7acf228d50d83cf327372b889e2a168a869275b26e17917ed0b4cf4d823a6" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==22.3.2" | ||||
|             "version": "==22.4.2" | ||||
|         }, | ||||
|         "automat": { | ||||
|             "hashes": [ | ||||
| @@ -99,7 +99,6 @@ | ||||
|                 "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", | ||||
|                 "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "markers": "python_version < '3.9'", | ||||
|             "version": "==0.2.1" | ||||
|         }, | ||||
| @@ -189,20 +188,12 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==3.4.0" | ||||
|         }, | ||||
|         "chardet": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", | ||||
|                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.1'", | ||||
|             "version": "==4.0.0" | ||||
|         }, | ||||
|         "charset-normalizer": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", | ||||
|                 "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==2.0.12" | ||||
|         }, | ||||
|         "click": { | ||||
| @@ -238,31 +229,29 @@ | ||||
|         }, | ||||
|         "cryptography": { | ||||
|             "hashes": [ | ||||
|                 "sha256:06bfafa6e53ccbfb7a94be4687b211a025ce0625e3f3c60bb15cd048a18f3ed8", | ||||
|                 "sha256:0db5cf21bd7d092baacb576482b0245102cea2d3cf09f09271ce9f69624ecb6f", | ||||
|                 "sha256:125702572be12bcd318e3a14e9e70acd4be69a43664a75f0397e8650fe3c6cc3", | ||||
|                 "sha256:1858eff6246bb8bbc080eee78f3dd1528739e3f416cba5f9914e8631b8df9871", | ||||
|                 "sha256:315af6268de72bcfa0bb3401350ce7d921f216e6b60de12a363dad128d9d459f", | ||||
|                 "sha256:451aaff8b8adf2dd0597cbb1fdcfc8a7d580f33f843b7cce75307a7f20112dd8", | ||||
|                 "sha256:58021d6e9b1d88b1105269d0da5e60e778b37dfc0e824efc71343dd003726831", | ||||
|                 "sha256:618391152147a1221c87b1b0b7f792cafcfd4b5a685c5c72eeea2ddd29aeceff", | ||||
|                 "sha256:6d4daf890e674d191757d8d7d60dc3a29c58c72c7a76a05f1c0a326013f47e8b", | ||||
|                 "sha256:74b55f67f4cf026cb84da7a1b04fc2a1d260193d4ad0ea5e9897c8b74c1e76ac", | ||||
|                 "sha256:7ceae26f876aabe193b13a0c36d1bb8e3e7e608d17351861b437bd882f617e9f", | ||||
|                 "sha256:930b829e8a2abaf43a19f38277ae3c5e1ffcf547b936a927d2587769ae52c296", | ||||
|                 "sha256:a18ff4bfa9d64914a84d7b06c46eb86e0cc03113470b3c111255aceb6dcaf81d", | ||||
|                 "sha256:ae1cd29fbe6b716855454e44f4bf743465152e15d2d317303fe3b58ee9e5af7a", | ||||
|                 "sha256:b1ee5c82cf03b30f6ae4e32d2bcb1e167ef74d6071cbb77c2af30f101d0b360b", | ||||
|                 "sha256:bf585476fcbcd37bed08072e8e2db3954ce1bfc68087a2dc9c19cfe0b90979ca", | ||||
|                 "sha256:c4a58eeafbd7409054be41a377e726a7904a17c26f45abf18125d21b1215b08b", | ||||
|                 "sha256:cce90609e01e1b192fae9e13665058ab46b2ea53a3c05a3ea74a3eb8c3af8857", | ||||
|                 "sha256:d610d0ee14dd9109006215c7c0de15eee91230b70a9bce2263461cf7c3720b83", | ||||
|                 "sha256:e69a0e36e62279120e648e787b76d79b41e0f9e86c1c636a4f38d415595c722e", | ||||
|                 "sha256:f095988548ec5095e3750cdb30e6962273d239b1998ba1aac66c0d5bee7111c1", | ||||
|                 "sha256:faf0f5456c059c7b1c29441bdd5e988f0ba75bdc3eea776520d8dcb1e30e1b5c" | ||||
|                 "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b", | ||||
|                 "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51", | ||||
|                 "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7", | ||||
|                 "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d", | ||||
|                 "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6", | ||||
|                 "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29", | ||||
|                 "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9", | ||||
|                 "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf", | ||||
|                 "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815", | ||||
|                 "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf", | ||||
|                 "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85", | ||||
|                 "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77", | ||||
|                 "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86", | ||||
|                 "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb", | ||||
|                 "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e", | ||||
|                 "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0", | ||||
|                 "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3", | ||||
|                 "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84", | ||||
|                 "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", | ||||
|                 "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==37.0.1" | ||||
|             "version": "==36.0.2" | ||||
|         }, | ||||
|         "daphne": { | ||||
|             "hashes": [ | ||||
| @@ -290,11 +279,11 @@ | ||||
|         }, | ||||
|         "django-cors-headers": { | ||||
|             "hashes": [ | ||||
|                 "sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b", | ||||
|                 "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456" | ||||
|                 "sha256:39d1d5acb872c1860ecfd88b8572bfbb3a1f201b5685ede951d71fc57c7dfae5", | ||||
|                 "sha256:5f07e2ff8a95c887698e748588a4a0b2ad0ad1b5a292e2d33132f1253e2a97cb" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.11.0" | ||||
|             "version": "==3.12.0" | ||||
|         }, | ||||
|         "django-extensions": { | ||||
|             "hashes": [ | ||||
| @@ -338,11 +327,11 @@ | ||||
|         }, | ||||
|         "filelock": { | ||||
|             "hashes": [ | ||||
|                 "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85", | ||||
|                 "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0" | ||||
|                 "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20", | ||||
|                 "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.6.0" | ||||
|             "version": "==3.7.0" | ||||
|         }, | ||||
|         "fuzzywuzzy": { | ||||
|             "extras": [ | ||||
| @@ -477,16 +466,16 @@ | ||||
|                 "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", | ||||
|                 "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==3.3" | ||||
|         }, | ||||
|         "imap-tools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:15d20ac8695fc4978a913c2186f482a802f5229c41c6e0c66c7bad8f1f590cf1", | ||||
|                 "sha256:606b73a1b5ecc4c72eea5ad19231e07a88bf9ba9adbdd4acb8cf71a359dd43ec" | ||||
|                 "sha256:81e0069d81483aecc3ca46e57f5c41ffc39f1ba0041e416591d829f99f682623", | ||||
|                 "sha256:d32f3e165af9e56542c1a5beb2866537265ef4832c8bd2eb25982ee8ac70ea4f" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.54.0" | ||||
|             "version": "==0.55.0" | ||||
|         }, | ||||
|         "img2pdf": { | ||||
|             "hashes": [ | ||||
| @@ -675,11 +664,11 @@ | ||||
|         }, | ||||
|         "ocrmypdf": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0c1cc0a7596fa9da1bfde67141227eeb813aba5e954f88199eacc5f51f1d67d9", | ||||
|                 "sha256:48bbdd5d15b76f34aa3a91910918e51f91bb3833b4e86da45f8542afda118404" | ||||
|                 "sha256:1169e7acee4cb12d0d61ca1c2cf1f78250ed5d2d0e33fd68f58defbaf3770af7", | ||||
|                 "sha256:d132a9e5dcd73a477f8bd89f15de2c4ae64b394ca296644971fcd004168ace9d" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==13.4.3" | ||||
|             "version": "==13.4.4" | ||||
|         }, | ||||
|         "packaging": { | ||||
|             "hashes": [ | ||||
| @@ -707,44 +696,45 @@ | ||||
|         }, | ||||
|         "pdfminer.six": { | ||||
|             "hashes": [ | ||||
|                 "sha256:af0630f98a292bad4170f54e80f82ca81b916dd0b2c996437ec45c02f11d8762", | ||||
|                 "sha256:eff2ce0abeaa4df94dc3461f70eab104487c7b4a2b3c7e9fd0aeec6c5f44d6a6" | ||||
|                 "sha256:0960be95fe8724a4847f83d53d0331b890871f6035ba706841568caa2b541bf5", | ||||
|                 "sha256:3d65c1a0f4a0465c709e191550ec77a684ebe0bcb562337ccbfb7aa228c52076" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==20220319" | ||||
|             "version": "==20220506" | ||||
|         }, | ||||
|         "pikepdf": { | ||||
|             "hashes": [ | ||||
|                 "sha256:101ec256a8d312c17decae52226cf32a3e7dc834583134300c2f4e60b70e6e91", | ||||
|                 "sha256:12b5b3cfc649e2542576a7e55c11e245278f14f727f116904893e54329102867", | ||||
|                 "sha256:1b8f68a75c0a6f6d4d102d0821365ae2aaa9ab635c6eb6c840569a56b1a266f4", | ||||
|                 "sha256:1bef3512be59fe0f481375b7eb415ca51ee7c80555031401f5f17ee3392e4add", | ||||
|                 "sha256:1d3141916dc9efc433fd22beba544f67a53a805800c3ff902baffa398ef4c85e", | ||||
|                 "sha256:3052df8514d26b676c50e65afc49a1d260c43a08c322c75cc2592c10a9a5b26d", | ||||
|                 "sha256:356d5554516a295fc10db3f25cfde4e92326f6d015da55d71b84f5ced2a07a5a", | ||||
|                 "sha256:55330c24b8e04ee09f1bc514c2b6107bb03a5eeb0b74929a61100cd6be22ae29", | ||||
|                 "sha256:553cf11933fdfe07fdd357ab40b9732db102e921b27c1065239308d42b7b858f", | ||||
|                 "sha256:5626312990a894c5db3a269455f7eb98df5f59188dde1797c0e352d60fdf89af", | ||||
|                 "sha256:59c24a65c94693ab4a7e92f4809f847b57461120256c083054e61c99c4952e84", | ||||
|                 "sha256:607deb1181a7cf5369cf70edfc41574d46c0a17c0cac1f6234272bd4cb3487e4", | ||||
|                 "sha256:60bdd49e6251f8c99989e6769d4ad29b209c1eaf88090f49d4b30fba98442e40", | ||||
|                 "sha256:73a7cc3c42609e00393b9d4e1b9ee132f528060254a174bf18ef31a154be0386", | ||||
|                 "sha256:75f1e2917b4d2d6573fe3d1c3b2ec70829b64515b2f723f5c3bebcdd65761e6c", | ||||
|                 "sha256:92ca9191680eccc21697e9e9c218e600ab31e7c24f6125749738c10ae2dc7c07", | ||||
|                 "sha256:9bcaf96e2f571f0fc7e3178cdf1bcca7c13e5c68128e8246031226d47ecf23f1", | ||||
|                 "sha256:a8f3e2229e2683497fe4ccc4af06050c125160a11bb3562b6c4ddaee4d0cc5c5", | ||||
|                 "sha256:c277066938ca0ddb2bfe75874ef8dd3aa259936fe15c4cf7d4282f89ba82ab3a", | ||||
|                 "sha256:c532542a99757d9f41df0cf1fc8f64a044d0eb822822cc069c80be35731df275", | ||||
|                 "sha256:dfa89bd86e01413531c1d7d201fb01f0e62b52ea926a8e8ca6f99f86ed761e95", | ||||
|                 "sha256:e064010b733b0a2ec4ec97982cd2887f9025292f2d228c6d5e6eca9d84851e53", | ||||
|                 "sha256:ea927afe7cb04cc7ade30b961f528ef53e8d9cf467dcc4639cf944fef872a1a1", | ||||
|                 "sha256:f40703b6267aa43d7f72468fa0a3b505ffff74ece2a4c69cfd3c90e023c41381", | ||||
|                 "sha256:f45cc4544bbd4c308a525a6bb8e2e29b3f849803ee557c6e35c684447f0a92e5", | ||||
|                 "sha256:f8ccda5ee992c73f647bcd96c9aa30f5eb9e8a6c5bdd6e3dcb29ebbffbe01a69", | ||||
|                 "sha256:fe386d93345c9b5a9690f7a7bfb789a5ec5467c34402628e10bda8a4f5bac73e" | ||||
|                 "sha256:00e66dcb803cc4f5348ae117287b85bd940744be38c111a88503843b0787ddfe", | ||||
|                 "sha256:0e392c32e1f82f8cb4e891cadaf137c8cbdb1e88b4714eeb3dcd2cc6ee227575", | ||||
|                 "sha256:143eef60457c828874000e11a34bc79c348544c7453f89e09ad9697b7102a108", | ||||
|                 "sha256:18907145f6d2772fc77b1ea8d4eb3971dbd8e2594698764a64c22c1c43645eee", | ||||
|                 "sha256:210cea8dc30bc9e668de1a380c83a31ec1b0edf8a2f91334ac99b23ec74b01e1", | ||||
|                 "sha256:270b4805e00f4599108231c4342701574de47b1c7df3573c2d80aed176dd6843", | ||||
|                 "sha256:35e0a82fd01e3fd57de473b65a56231e99cf17aaa632de0bafc02df5d25fd443", | ||||
|                 "sha256:397dc297ec4390a67f920466b868cfd100b9be6bc12ab4134f455226c3d1ddef", | ||||
|                 "sha256:44c909f2e16fe4646a3175273fad60e102750383c448a01ca8073b05cc086ffa", | ||||
|                 "sha256:4f4dcc07d9222faa976b9ea6e21a30f6669cd8152cd2fa5e7a898d325da059c2", | ||||
|                 "sha256:524785518325fac3cda133b339ff4cd56687cefc4a43b14aef102f06fabc0ffa", | ||||
|                 "sha256:7a2aaaa6968b7268c97dc6beffc5be5148470f9675fcb940681a30ca68f00f6d", | ||||
|                 "sha256:84915fcff1c28bfba4caa9df0a72a53264e1218b17f0870744aad43a477a13c0", | ||||
|                 "sha256:85965ff4f42542998db1daa641f33b9b7a6a03a0e7531a76c06a185d8bf6653b", | ||||
|                 "sha256:85ce51ac8354c1d6912edcb3b018e39fc9678cf1946087ef526d990eedf49756", | ||||
|                 "sha256:8ce07b03affe29628f7babf25a54010f765f89d887a02e12393f365a8dc37d65", | ||||
|                 "sha256:a074af7054206d91a70513269254b932e7ad244fed6cfb37c590cf7b665dcd53", | ||||
|                 "sha256:aadb3e93b10299c8d3a6c5366e507ccdded727fc22e5b7ebae61bcd6883774dc", | ||||
|                 "sha256:afcd1d4a1c4b9c99226b6ce1ceb5132fa219b420e562984564a431bed7160949", | ||||
|                 "sha256:b43c57f37cd6278dcb52d3bda7f482b1961c2eb8a5fc1127cabe941914a858a9", | ||||
|                 "sha256:c34e4239661d2ddf23caa1c4256f636c866ce8069a5052a2bf8ee06e3cae22f3", | ||||
|                 "sha256:d3015baf32d9a559c50aa5ff15ac7bf5d2733efa4d1996eb846fb289e3f5a0e4", | ||||
|                 "sha256:d37aee5705ee23f28513f480749773495f88d72a9177de6c5af32b20a2ad1592", | ||||
|                 "sha256:e1b7edde3d3599a23a283341f15a41943a5894308df623cf6e04ff58304bd4b3", | ||||
|                 "sha256:e552419963bdc4adab4f5a912994de96ba2dfc313ce935d8b727a9e40d6ccd67", | ||||
|                 "sha256:e5941c4c6e61895114cf0caff871e7b17ce9145822c5068b6c6f16114dd1267b", | ||||
|                 "sha256:f808eadb454adc6f4fb72b651eaa41a2926812e2b0306e2c320e5e3181440e49", | ||||
|                 "sha256:ffa2b7d68fe45202895e74ba660619172e8528950905bee0ec862529760c4246" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==5.1.2" | ||||
|             "version": "==5.1.3" | ||||
|         }, | ||||
|         "pillow": { | ||||
|             "hashes": [ | ||||
| @@ -875,11 +865,11 @@ | ||||
|         }, | ||||
|         "pyparsing": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", | ||||
|                 "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" | ||||
|                 "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", | ||||
|                 "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" | ||||
|             ], | ||||
|             "markers": "python_full_version >= '3.6.8'", | ||||
|             "version": "==3.0.8" | ||||
|             "version": "==3.0.9" | ||||
|         }, | ||||
|         "python-dateutil": { | ||||
|             "hashes": [ | ||||
| @@ -1195,11 +1185,11 @@ | ||||
|         }, | ||||
|         "setuptools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", | ||||
|                 "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" | ||||
|                 "sha256:5534570b9980fc650d45c62877ff603c7aaaf24893371708736cc016bd221c3c", | ||||
|                 "sha256:ca6ba73b7fd5f734ae70ece8c4c1f7062b07f3352f6428f6277e27c8f5c64237" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==62.1.0" | ||||
|             "version": "==62.2.0" | ||||
|         }, | ||||
|         "six": { | ||||
|             "hashes": [ | ||||
| @@ -1333,33 +1323,34 @@ | ||||
|         }, | ||||
|         "watchdog": { | ||||
|             "hashes": [ | ||||
|                 "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385", | ||||
|                 "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690", | ||||
|                 "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a", | ||||
|                 "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383", | ||||
|                 "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99", | ||||
|                 "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4", | ||||
|                 "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd", | ||||
|                 "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566", | ||||
|                 "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572", | ||||
|                 "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480", | ||||
|                 "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6", | ||||
|                 "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa", | ||||
|                 "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8", | ||||
|                 "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca", | ||||
|                 "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab", | ||||
|                 "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd", | ||||
|                 "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055", | ||||
|                 "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601", | ||||
|                 "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c", | ||||
|                 "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b", | ||||
|                 "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2", | ||||
|                 "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f", | ||||
|                 "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420", | ||||
|                 "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d" | ||||
|                 "sha256:036ed15f7cd656351bf4e17244447be0a09a61aaa92014332d50719fc5973bc0", | ||||
|                 "sha256:0c520009b8cce79099237d810aaa19bc920941c268578436b62013b2f0102320", | ||||
|                 "sha256:0fb60c7d31474b21acba54079ce9ff0136411183e9a591369417cddb1d7d00d7", | ||||
|                 "sha256:156ec3a94695ea68cfb83454b98754af6e276031ba1ae7ae724dc6bf8973b92a", | ||||
|                 "sha256:1ae17b6be788fb8e4d8753d8d599de948f0275a232416e16436363c682c6f850", | ||||
|                 "sha256:1e5d0fdfaa265c29dc12621913a76ae99656cf7587d03950dfeb3595e5a26102", | ||||
|                 "sha256:24dedcc3ce75e150f2a1d704661f6879764461a481ba15a57dc80543de46021c", | ||||
|                 "sha256:2962628a8777650703e8f6f2593065884c602df7bae95759b2df267bd89b2ef5", | ||||
|                 "sha256:47598fe6713fc1fee86b1ca85c9cbe77e9b72d002d6adeab9c3b608f8a5ead10", | ||||
|                 "sha256:4978db33fc0934c92013ee163a9db158ec216099b69fce5aec790aba704da412", | ||||
|                 "sha256:5e2e51c53666850c3ecffe9d265fc5d7351db644de17b15e9c685dd3cdcd6f97", | ||||
|                 "sha256:676263bee67b165f16b05abc52acc7a94feac5b5ab2449b491f1a97638a79277", | ||||
|                 "sha256:68dbe75e0fa1ba4d73ab3f8e67b21770fbed0651d32ce515cd38919a26873266", | ||||
|                 "sha256:6d03149126864abd32715d4e9267d2754cede25a69052901399356ad3bc5ecff", | ||||
|                 "sha256:6ddf67bc9f413791072e3afb466e46cc72c6799ba73dea18439b412e8f2e3257", | ||||
|                 "sha256:746e4c197ec1083581bb1f64d07d1136accf03437badb5ff8fcb862565c193b2", | ||||
|                 "sha256:7721ac736170b191c50806f43357407138c6748e4eb3e69b071397f7f7aaeedd", | ||||
|                 "sha256:88ef3e8640ef0a64b7ad7394b0f23384f58ac19dd759da7eaa9bc04b2898943f", | ||||
|                 "sha256:aa68d2d9a89d686fae99d28a6edf3b18595e78f5adf4f5c18fbfda549ac0f20c", | ||||
|                 "sha256:b962de4d7d92ff78fb2dbc6a0cb292a679dea879a0eb5568911484d56545b153", | ||||
|                 "sha256:ce7376aed3da5fd777483fe5ebc8475a440c6d18f23998024f832134b2938e7b", | ||||
|                 "sha256:ddde157dc1447d8130cb5b8df102fad845916fe4335e3d3c3f44c16565becbb7", | ||||
|                 "sha256:efcc8cbc1b43902571b3dce7ef53003f5b97fe4f275fe0489565fc6e2ebe3314", | ||||
|                 "sha256:f9ee4c6bf3a1b2ed6be90a2d78f3f4bbd8105b6390c04a86eb48ed67bbfa0b0b", | ||||
|                 "sha256:fed4de6e45a4f16e4046ea00917b4fe1700b97244e5d114f594b4a1b9de6bed8" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.1.7" | ||||
|             "version": "==2.1.8" | ||||
|         }, | ||||
|         "watchgod": { | ||||
|             "hashes": [ | ||||
| @@ -1585,7 +1576,7 @@ | ||||
|                 "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", | ||||
|                 "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==2.0.12" | ||||
|         }, | ||||
|         "click": { | ||||
| @@ -1596,53 +1587,63 @@ | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==8.1.3" | ||||
|         }, | ||||
|         "coverage": { | ||||
|             "extras": [], | ||||
|         "colorama": { | ||||
|             "hashes": [ | ||||
|                 "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", | ||||
|                 "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", | ||||
|                 "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", | ||||
|                 "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", | ||||
|                 "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", | ||||
|                 "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", | ||||
|                 "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", | ||||
|                 "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", | ||||
|                 "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", | ||||
|                 "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", | ||||
|                 "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", | ||||
|                 "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", | ||||
|                 "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", | ||||
|                 "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", | ||||
|                 "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", | ||||
|                 "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", | ||||
|                 "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", | ||||
|                 "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", | ||||
|                 "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", | ||||
|                 "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", | ||||
|                 "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", | ||||
|                 "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", | ||||
|                 "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", | ||||
|                 "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", | ||||
|                 "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", | ||||
|                 "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", | ||||
|                 "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", | ||||
|                 "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", | ||||
|                 "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", | ||||
|                 "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", | ||||
|                 "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", | ||||
|                 "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", | ||||
|                 "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", | ||||
|                 "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", | ||||
|                 "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", | ||||
|                 "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", | ||||
|                 "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", | ||||
|                 "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", | ||||
|                 "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", | ||||
|                 "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", | ||||
|                 "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" | ||||
|                 "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", | ||||
|                 "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==0.4.4" | ||||
|         }, | ||||
|         "coverage": { | ||||
|             "extras": [ | ||||
|                 "toml" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8", | ||||
|                 "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d", | ||||
|                 "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31", | ||||
|                 "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879", | ||||
|                 "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69", | ||||
|                 "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3", | ||||
|                 "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7", | ||||
|                 "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81", | ||||
|                 "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579", | ||||
|                 "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c", | ||||
|                 "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53", | ||||
|                 "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4", | ||||
|                 "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9", | ||||
|                 "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d", | ||||
|                 "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3", | ||||
|                 "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293", | ||||
|                 "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20", | ||||
|                 "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9", | ||||
|                 "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579", | ||||
|                 "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548", | ||||
|                 "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d", | ||||
|                 "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284", | ||||
|                 "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b", | ||||
|                 "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a", | ||||
|                 "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572", | ||||
|                 "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f", | ||||
|                 "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9", | ||||
|                 "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63", | ||||
|                 "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94", | ||||
|                 "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d", | ||||
|                 "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2", | ||||
|                 "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a", | ||||
|                 "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130", | ||||
|                 "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0", | ||||
|                 "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe", | ||||
|                 "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73", | ||||
|                 "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8", | ||||
|                 "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738", | ||||
|                 "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e", | ||||
|                 "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8", | ||||
|                 "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==6.3.2" | ||||
|             "version": "==6.3.3" | ||||
|         }, | ||||
|         "coveralls": { | ||||
|             "hashes": [ | ||||
| @@ -1691,19 +1692,19 @@ | ||||
|         }, | ||||
|         "faker": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0301ace8365d98f3d0bf6e9a40200c8548e845d3812402ae1daf589effe3fb01", | ||||
|                 "sha256:b1903db92175d78051858128ada397c7dc76f376f6967975419da232b3ebd429" | ||||
|                 "sha256:c6ff91847d7c820afc0a74d95e824b48aab71ddfd9003f300641e42d58ae886f", | ||||
|                 "sha256:cad1f69d72a68878cd67855140b6fe3e44c11628971cd838595d289c98bc45de" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==13.7.0" | ||||
|             "version": "==13.11.1" | ||||
|         }, | ||||
|         "filelock": { | ||||
|             "hashes": [ | ||||
|                 "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85", | ||||
|                 "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0" | ||||
|                 "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20", | ||||
|                 "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.6.0" | ||||
|             "version": "==3.7.0" | ||||
|         }, | ||||
|         "identify": { | ||||
|             "hashes": [ | ||||
| @@ -1718,7 +1719,7 @@ | ||||
|                 "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", | ||||
|                 "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==3.3" | ||||
|         }, | ||||
|         "imagesize": { | ||||
| @@ -1752,6 +1753,20 @@ | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==3.1.2" | ||||
|         }, | ||||
|         "livereload": { | ||||
|             "hashes": [ | ||||
|                 "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869" | ||||
|             ], | ||||
|             "version": "==2.6.3" | ||||
|         }, | ||||
|         "markdown-it-py": { | ||||
|             "hashes": [ | ||||
|                 "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27", | ||||
|                 "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==2.1.0" | ||||
|         }, | ||||
|         "markupsafe": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", | ||||
| @@ -1798,6 +1813,22 @@ | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==2.1.1" | ||||
|         }, | ||||
|         "mdit-py-plugins": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b1279701cee2dbf50e188d3da5f51fee8d78d038cdf99be57c6b9d1aa93b4073", | ||||
|                 "sha256:ecc24f51eeec6ab7eecc2f9724e8272c2fb191c2e93cf98109120c2cace69750" | ||||
|             ], | ||||
|             "markers": "python_version ~= '3.6'", | ||||
|             "version": "==0.3.0" | ||||
|         }, | ||||
|         "mdurl": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6a8f6804087b7128040b2fb2ebe242bdc2affaeaa034d5fc9feeed30b443651b", | ||||
|                 "sha256:f79c9709944df218a4cdb0fcc0b0c7ead2f44594e3e84dc566606f04ad749c20" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==0.1.1" | ||||
|         }, | ||||
|         "mypy-extensions": { | ||||
|             "hashes": [ | ||||
|                 "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", | ||||
| @@ -1805,6 +1836,14 @@ | ||||
|             ], | ||||
|             "version": "==0.4.3" | ||||
|         }, | ||||
|         "myst-parser": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1635ce3c18965a528d6de980f989ff64d6a1effb482e1f611b1bfb79e38f3d98", | ||||
|                 "sha256:4c076d649e066f9f5c7c661bae2658be1ca06e76b002bb97f02a09398707686c" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.17.2" | ||||
|         }, | ||||
|         "nodeenv": { | ||||
|             "hashes": [ | ||||
|                 "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", | ||||
| @@ -1845,11 +1884,11 @@ | ||||
|         }, | ||||
|         "pre-commit": { | ||||
|             "hashes": [ | ||||
|                 "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2", | ||||
|                 "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10" | ||||
|                 "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10", | ||||
|                 "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.18.1" | ||||
|             "version": "==2.19.0" | ||||
|         }, | ||||
|         "py": { | ||||
|             "hashes": [ | ||||
| @@ -1877,11 +1916,11 @@ | ||||
|         }, | ||||
|         "pyparsing": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", | ||||
|                 "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" | ||||
|                 "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", | ||||
|                 "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" | ||||
|             ], | ||||
|             "markers": "python_full_version >= '3.6.8'", | ||||
|             "version": "==3.0.8" | ||||
|             "version": "==3.0.9" | ||||
|         }, | ||||
|         "pytest": { | ||||
|             "hashes": [ | ||||
| @@ -2021,6 +2060,14 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==4.5.0" | ||||
|         }, | ||||
|         "sphinx-autobuild": { | ||||
|             "hashes": [ | ||||
|                 "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", | ||||
|                 "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2021.3.14" | ||||
|         }, | ||||
|         "sphinx-rtd-theme": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", | ||||
| @@ -2099,6 +2146,53 @@ | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==2.0.1" | ||||
|         }, | ||||
|         "tornado": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", | ||||
|                 "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", | ||||
|                 "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", | ||||
|                 "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", | ||||
|                 "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", | ||||
|                 "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", | ||||
|                 "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", | ||||
|                 "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", | ||||
|                 "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", | ||||
|                 "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", | ||||
|                 "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", | ||||
|                 "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", | ||||
|                 "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", | ||||
|                 "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", | ||||
|                 "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", | ||||
|                 "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", | ||||
|                 "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", | ||||
|                 "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", | ||||
|                 "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", | ||||
|                 "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", | ||||
|                 "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", | ||||
|                 "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", | ||||
|                 "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", | ||||
|                 "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", | ||||
|                 "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", | ||||
|                 "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", | ||||
|                 "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", | ||||
|                 "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", | ||||
|                 "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", | ||||
|                 "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", | ||||
|                 "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", | ||||
|                 "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", | ||||
|                 "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", | ||||
|                 "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", | ||||
|                 "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", | ||||
|                 "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", | ||||
|                 "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", | ||||
|                 "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", | ||||
|                 "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", | ||||
|                 "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", | ||||
|                 "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==6.1" | ||||
|         }, | ||||
|         "tox": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a", | ||||
|   | ||||
| @@ -102,7 +102,7 @@ For bugs please [open an issue](https://github.com/paperless-ngx/paperless-ngx/i | ||||
|  | ||||
| Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list: | ||||
|  | ||||
| - [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ngx. | ||||
| - [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ng. | ||||
| - [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents. | ||||
| - [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for Paperless. | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,6 @@ COPY ./src-ui /src/src-ui | ||||
| WORKDIR /src/src-ui | ||||
| RUN set -eux \ | ||||
|   && npm update npm -g \ | ||||
|   && npm ci --no-optional | ||||
|   && npm ci --omit=optional | ||||
| RUN set -eux \ | ||||
|   && ./node_modules/.bin/ng build --configuration production | ||||
|   | ||||
| @@ -55,7 +55,7 @@ services: | ||||
|     ports: | ||||
|       - 8010:8000 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://localhost:8000"] | ||||
|       test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 5 | ||||
|   | ||||
| @@ -59,7 +59,7 @@ services: | ||||
|     ports: | ||||
|       - 8000:8000 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://localhost:8000"] | ||||
|       test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 5 | ||||
|   | ||||
| @@ -53,7 +53,7 @@ services: | ||||
|     ports: | ||||
|       - 8000:8000 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://localhost:8000"] | ||||
|       test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 5 | ||||
|   | ||||
| @@ -48,7 +48,7 @@ services: | ||||
|     ports: | ||||
|       - 8000:8000 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://localhost:8000"] | ||||
|       test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 5 | ||||
|   | ||||
| @@ -39,7 +39,7 @@ services: | ||||
|     ports: | ||||
|       - 8000:8000 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://localhost:8000"] | ||||
|       test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 5 | ||||
|   | ||||
| @@ -52,7 +52,7 @@ search_index() { | ||||
|  | ||||
| 	if [[ (! -f "$index_version_file") || $(<$index_version_file) != "$index_version" ]]; then | ||||
| 		echo "Search index out of date. Updating..." | ||||
| 		python3 manage.py document_index reindex | ||||
| 		python3 manage.py document_index reindex --no-progress-bar | ||||
| 		echo $index_version | tee $index_version_file >/dev/null | ||||
| 	fi | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ stderr_logfile_maxbytes=0 | ||||
| [program:scheduler] | ||||
| command=python3 manage.py qcluster | ||||
| user=paperless | ||||
| stopasgroup = true | ||||
|  | ||||
| stdout_logfile=/dev/stdout | ||||
| stdout_logfile_maxbytes=0 | ||||
|   | ||||
| @@ -24,6 +24,7 @@ I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | ||||
| help: | ||||
| 	@echo "Please use \`make <target>' where <target> is one of" | ||||
| 	@echo "  html       to make standalone HTML files" | ||||
| 	@echo "  livehtml   to preview changes with live reload in your browser" | ||||
| 	@echo "  dirhtml    to make HTML files named index.html in directories" | ||||
| 	@echo "  singlehtml to make a single large HTML file" | ||||
| 	@echo "  pickle     to make pickle files" | ||||
| @@ -54,6 +55,9 @@ html: | ||||
| 	@echo | ||||
| 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | ||||
|  | ||||
| livehtml: | ||||
| 	sphinx-autobuild "./" "$(BUILDDIR)" $(O) | ||||
|  | ||||
| dirhtml: | ||||
| 	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | ||||
| 	@echo | ||||
|   | ||||
							
								
								
									
										4
									
								
								docs/_static/css/custom.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								docs/_static/css/custom.css
									
									
									
									
										vendored
									
									
								
							| @@ -64,6 +64,10 @@ body { | ||||
|   color: var(--color-text-body); | ||||
| } | ||||
|  | ||||
| .rst-content p { | ||||
|   word-break: break-word; | ||||
| } | ||||
|  | ||||
| h1, h2, h3, h4, h5, h6 { | ||||
|   font-family: inherit; | ||||
| } | ||||
|   | ||||
							
								
								
									
										52
									
								
								docs/_static/js/darkmode.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										52
									
								
								docs/_static/js/darkmode.js
									
									
									
									
										vendored
									
									
								
							| @@ -1,47 +1,47 @@ | ||||
| let toggleButton; | ||||
| let icon; | ||||
| let toggleButton | ||||
| let icon | ||||
|  | ||||
| function load() { | ||||
| 	"use strict"; | ||||
| 	'use strict' | ||||
|  | ||||
| 	toggleButton = document.createElement("button"); | ||||
| 	toggleButton.setAttribute("title", "Toggle dark mode"); | ||||
| 	toggleButton.classList.add("dark-mode-toggle"); | ||||
| 	icon = document.createElement("i"); | ||||
| 	icon.classList.add("fa", darkModeState ? "fa-sun-o" : "fa-moon-o"); | ||||
| 	toggleButton.appendChild(icon); | ||||
| 	document.body.prepend(toggleButton); | ||||
| 	toggleButton = document.createElement('button') | ||||
| 	toggleButton.setAttribute('title', 'Toggle dark mode') | ||||
| 	toggleButton.classList.add('dark-mode-toggle') | ||||
| 	icon = document.createElement('i') | ||||
| 	icon.classList.add('fa', darkModeState ? 'fa-sun-o' : 'fa-moon-o') | ||||
| 	toggleButton.appendChild(icon) | ||||
| 	document.body.prepend(toggleButton) | ||||
|  | ||||
| 	// Listen for changes in the OS settings | ||||
| 	// addListener is used because older versions of Safari don't support addEventListener | ||||
| 	// prefersDarkQuery set in <head> | ||||
| 	if (prefersDarkQuery) { | ||||
| 		prefersDarkQuery.addListener(function (evt) { | ||||
| 			toggleDarkMode(evt.matches); | ||||
| 		}); | ||||
| 			toggleDarkMode(evt.matches) | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Initial setting depending on the prefers-color-mode or localstorage | ||||
| 	// darkModeState should be set in the document <head> to prevent flash | ||||
| 	if (darkModeState == undefined) darkModeState = false; | ||||
| 	toggleDarkMode(darkModeState); | ||||
| 	if (darkModeState == undefined) darkModeState = false | ||||
| 	toggleDarkMode(darkModeState) | ||||
|  | ||||
| 	// Toggles the "dark-mode" class on click and sets localStorage state | ||||
| 	toggleButton.addEventListener("click", () => { | ||||
| 		darkModeState = !darkModeState; | ||||
| 	toggleButton.addEventListener('click', () => { | ||||
| 		darkModeState = !darkModeState | ||||
|  | ||||
| 		toggleDarkMode(darkModeState); | ||||
| 		localStorage.setItem("dark-mode", darkModeState); | ||||
| 	}); | ||||
| 		toggleDarkMode(darkModeState) | ||||
| 		localStorage.setItem('dark-mode', darkModeState) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| function toggleDarkMode(state) { | ||||
| 	document.documentElement.classList.toggle("dark-mode", state); | ||||
| 	document.documentElement.classList.toggle("light-mode", !state); | ||||
| 	icon.classList.remove("fa-sun-o"); | ||||
| 	icon.classList.remove("fa-moon-o"); | ||||
| 	icon.classList.add(state ? "fa-sun-o" : "fa-moon-o"); | ||||
| 	darkModeState = state; | ||||
| 	document.documentElement.classList.toggle('dark-mode', state) | ||||
| 	document.documentElement.classList.toggle('light-mode', !state) | ||||
| 	icon.classList.remove('fa-sun-o') | ||||
| 	icon.classList.remove('fa-moon-o') | ||||
| 	icon.classList.add(state ? 'fa-sun-o' : 'fa-moon-o') | ||||
| 	darkModeState = state | ||||
| } | ||||
|  | ||||
| document.addEventListener("DOMContentLoaded", load); | ||||
| document.addEventListener('DOMContentLoaded', load) | ||||
|   | ||||
| @@ -118,10 +118,10 @@ Then you can start paperless-ngx with ``-d`` to have it run in the background. | ||||
|                 image: ghcr.io/paperless-ngx/paperless-ngx:latest | ||||
|  | ||||
|     .. note:: | ||||
|         In version 1.7.1 and onwards, the Docker image can now pinned to a release series. | ||||
|         In version 1.7.1 and onwards, the Docker image can now be pinned to a release series. | ||||
|         This is often combined with automatic updaters such as Watchtower to allow safer | ||||
|         unattended upgrading to new bugfix releases only.  It is still recommended to always | ||||
|         review release notes before upgrading.  To ping your install to a release series, edit | ||||
|         review release notes before upgrading.  To pin your install to a release series, edit | ||||
|         the ``docker-compose.yml`` find the line that says | ||||
|  | ||||
|             .. code:: | ||||
| @@ -287,6 +287,10 @@ When you use the provided docker compose script, put the export inside the | ||||
| ``export`` folder in your paperless source directory. Specify ``../export`` | ||||
| as the ``source``. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     Importing from a previous version of Paperless may work, but for best results | ||||
|     it is suggested to match the versions. | ||||
|  | ||||
| .. _utilities-retagger: | ||||
|  | ||||
| @@ -386,8 +390,8 @@ the naming scheme. | ||||
|  | ||||
| .. warning:: | ||||
|  | ||||
|     Since this command moves you documents around a lot, it is advised to to | ||||
|     a backup before. The renaming logic is robust and will never overwrite | ||||
|     Since this command moves your documents, it is advised to do | ||||
|     a backup beforehand. The renaming logic is robust and will never overwrite | ||||
|     or delete a file, but you can't ever be careful enough. | ||||
|  | ||||
| .. code:: | ||||
|   | ||||
| @@ -7,12 +7,12 @@ easier. | ||||
|  | ||||
| .. _advanced-matching: | ||||
|  | ||||
| Matching tags, correspondents and document types | ||||
| ################################################ | ||||
| Matching tags, correspondents, document types, and storage paths | ||||
| ################################################################ | ||||
|  | ||||
| Paperless will compare the matching algorithms defined by every tag and | ||||
| correspondent already set in your database to see if they apply to the text in | ||||
| a document.  In other words, if you defined a tag called ``Home Utility`` | ||||
| Paperless will compare the matching algorithms defined by every tag, correspondent, | ||||
| document type, and storage path in your database to see if they apply to the text | ||||
| in a document. In other words, if you define a tag called ``Home Utility`` | ||||
| that had a ``match`` property of ``bc hydro`` and a ``matching_algorithm`` of | ||||
| ``literal``, Paperless will automatically tag your newly-consumed document with | ||||
| your ``Home Utility`` tag so long as the text ``bc hydro`` appears in the body | ||||
| @@ -22,10 +22,10 @@ The matching logic is quite powerful. It supports searching the text of your | ||||
| document with different algorithms, and as such, some experimentation may be | ||||
| necessary to get things right. | ||||
|  | ||||
| In order to have a tag, correspondent, or type assigned automatically to newly | ||||
| consumed documents, assign a match and matching algorithm using the web | ||||
| interface. These settings define when to assign correspondents, tags, and types | ||||
| to documents. | ||||
| In order to have a tag, correspondent, document type, or storage path assigned | ||||
| automatically to newly consumed documents, assign a match and matching algorithm | ||||
| using the web interface. These settings define when to assign tags, correspondents, | ||||
| document types, and storage paths to documents. | ||||
|  | ||||
| The following algorithms are available: | ||||
|  | ||||
| @@ -37,7 +37,7 @@ The following algorithms are available: | ||||
| * **Literal:** Matches only if the match appears exactly as provided (i.e. preserve ordering) in the PDF. | ||||
| * **Regular expression:** Parses the match as a regular expression and tries to | ||||
|   find a match within the document. | ||||
| * **Fuzzy match:** I dont know. Look at the source. | ||||
| * **Fuzzy match:** I don't know. Look at the source. | ||||
| * **Auto:** Tries to automatically match new documents. This does not require you | ||||
|   to set a match. See the notes below. | ||||
|  | ||||
| @@ -47,9 +47,9 @@ defining a match text of ``"Bank of America" BofA`` using the *any* algorithm, | ||||
| will match documents that contain either "Bank of America" or "BofA", but will | ||||
| not match documents containing "Bank of South America". | ||||
|  | ||||
| Then just save your tag/correspondent and run another document through the | ||||
| consumer.  Once complete, you should see the newly-created document, | ||||
| automatically tagged with the appropriate data. | ||||
| Then just save your tag, correspondent, document type, or storage path and run | ||||
| another document through the consumer.  Once complete, you should see the | ||||
| newly-created document, automatically tagged with the appropriate data. | ||||
|  | ||||
|  | ||||
| .. _advanced-automatic_matching: | ||||
| @@ -58,9 +58,9 @@ Automatic matching | ||||
| ================== | ||||
|  | ||||
| Paperless-ngx comes with a new matching algorithm called *Auto*. This matching | ||||
| algorithm tries to assign tags, correspondents, and document types to your | ||||
| documents based on how you have already assigned these on existing documents. It | ||||
| uses a neural network under the hood. | ||||
| algorithm tries to assign tags, correspondents, document types, and storage paths | ||||
| to your documents based on how you have already assigned these on existing documents. | ||||
| It uses a neural network under the hood. | ||||
|  | ||||
| If, for example, all your bank statements of your account 123 at the Bank of | ||||
| America are tagged with the tag "bofa_123" and the matching algorithm of this | ||||
| @@ -80,20 +80,21 @@ feature: | ||||
|   that the neural network only learns from documents which you have correctly | ||||
|   tagged before. | ||||
| * The matching algorithm can only work if there is a correlation between the | ||||
|   tag, correspondent, or document type and the document itself. Your bank | ||||
|   statements usually contain your bank account number and the name of the bank, | ||||
|   so this works reasonably well, However, tags such as "TODO" cannot be | ||||
|   automatically assigned. | ||||
|   tag, correspondent, document type, or storage path and the document itself. | ||||
|   Your bank statements usually contain your bank account number and the name | ||||
|   of the bank, so this works reasonably well, However, tags such as "TODO" | ||||
|   cannot be automatically assigned. | ||||
| * The matching algorithm needs a reasonable number of documents to identify when | ||||
|   to assign tags, correspondents, and types. If one out of a thousand documents | ||||
|   has the correspondent "Very obscure web shop I bought something five years | ||||
|   ago", it will probably not assign this correspondent automatically if you buy | ||||
|   something from them again. The more documents, the better. | ||||
|   to assign tags, correspondents, storage paths, and types. If one out of a | ||||
|   thousand documents has the correspondent "Very obscure web shop I bought | ||||
|   something five years ago", it will probably not assign this correspondent | ||||
|   automatically if you buy something from them again. The more documents, the better. | ||||
| * Paperless also needs a reasonable amount of negative examples to decide when | ||||
|   not to assign a certain tag, correspondent or type. This will usually be the | ||||
|   case as you start filling up paperless with documents. Example: If all your | ||||
|   documents are either from "Webshop" and "Bank", paperless will assign one of | ||||
|   these correspondents to ANY new document, if both are set to automatic matching. | ||||
|   not to assign a certain tag, correspondent, document type, or storage path. This will | ||||
|   usually be the case as you start filling up paperless with documents. | ||||
|   Example: If all your documents are either from "Webshop" and "Bank", paperless | ||||
|   will assign one of these correspondents to ANY new document, if both are set | ||||
|   to automatic matching. | ||||
|  | ||||
| Hooking into the consumption process | ||||
| #################################### | ||||
| @@ -268,6 +269,17 @@ If paperless detects that two documents share the same filename, paperless will | ||||
| append ``_01``, ``_02``, etc to the filename. This happens if all the placeholders in a filename | ||||
| evaluate to the same value. | ||||
|  | ||||
| .. hint:: | ||||
|     You can affect how empty placeholders are treated by changing the following setting to | ||||
|     `true`. | ||||
|  | ||||
|     .. code:: | ||||
|  | ||||
|         PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=True | ||||
|  | ||||
|     Doing this results in all empty placeholders resolving to "" instead of "none" as stated above. | ||||
|     Spaces before empty placeholders are removed as well, empty directories are omitted. | ||||
|  | ||||
| .. hint:: | ||||
|  | ||||
|     Paperless checks the filename of a document whenever it is saved. Therefore, | ||||
| @@ -290,3 +302,59 @@ evaluate to the same value. | ||||
|  | ||||
|     However, keep in mind that inside docker, if files get stored outside of the | ||||
|     predefined volumes, they will be lost after a restart of paperless. | ||||
|  | ||||
|  | ||||
| Storage paths | ||||
| ############# | ||||
|  | ||||
| One of the best things in Paperless is that you can not only access the documents via the | ||||
| web interface, but also via the file system. | ||||
|  | ||||
| When as single storage layout is not sufficient for your use case, storage paths come to | ||||
| the rescue. Storage paths allow you to configure more precisely where each document is stored | ||||
| in the file system. | ||||
|  | ||||
| - Each storage path is a `PAPERLESS_FILENAME_FORMAT` and follows the rules described above | ||||
| - Each document is assigned a storage path using the matching algorithms described above, but | ||||
|   can be overwritten at any time | ||||
|  | ||||
| For example, you could define the following two storage paths: | ||||
|  | ||||
| 1. Normal communications are put into a folder structure sorted by `year/correspondent` | ||||
| 2. Communications with insurance companies are stored in a flat structure with longer file names, | ||||
|    but containing the full date of the correspondence. | ||||
|  | ||||
| .. code:: | ||||
|  | ||||
|     By Year = {created_year}/{correspondent}/{title} | ||||
|     Insurances = Insurances/{correspondent}/{created_year}-{created_month}-{created_day} {title} | ||||
|  | ||||
|  | ||||
| If you then map these storage paths to the documents, you might get the following result. | ||||
| For simplicity, `By Year` defines the same structure as in the previous example above. | ||||
|  | ||||
| .. code:: text | ||||
|  | ||||
|    2019/                                   # By Year | ||||
|       My bank/ | ||||
|         Statement January.pdf | ||||
|         Statement February.pdf | ||||
|  | ||||
|     Insurances/                           # Insurances | ||||
|       Healthcare 123/ | ||||
|         2022-01-01 Statement January.pdf | ||||
|         2022-02-02 Letter.pdf | ||||
|         2022-02-03 Letter.pdf | ||||
|       Dental 456/ | ||||
|         2021-12-01 New Conditions.pdf | ||||
|  | ||||
|  | ||||
| .. hint:: | ||||
|  | ||||
|     Defining a storage path is optional. If no storage path is defined for a document, the global | ||||
|     `PAPERLESS_FILENAME_FORMAT` is applied. | ||||
|  | ||||
| .. caution:: | ||||
|  | ||||
|     If you adjust the format of an existing storage path, old documents don't get relocated automatically. | ||||
|     You need to run the :ref:`document renamer <utilities-renamer>` to adjust their pathes. | ||||
|   | ||||
| @@ -240,11 +240,13 @@ be instructed to consume the document from there. | ||||
| The endpoint supports the following optional form fields: | ||||
|  | ||||
| *   ``title``: Specify a title that the consumer should use for the document. | ||||
| *   ``created``: Specify a DateTime where the document was created (e.g. "2016-04-19" or "2016-04-19 06:15:00+02:00"). | ||||
| *   ``correspondent``: Specify the ID of a correspondent that the consumer should use for the document. | ||||
| *   ``document_type``: Similar to correspondent. | ||||
| *   ``tags``: Similar to correspondent. Specify this multiple times to have multiple tags added | ||||
|     to the document. | ||||
|  | ||||
|  | ||||
| The endpoint will immediately return "OK" if the document consumption process | ||||
| was started successfully. No additional status information about the consumption | ||||
| process itself is available, since that happens in a different process. | ||||
|   | ||||
							
								
								
									
										1947
									
								
								docs/changelog.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1947
									
								
								docs/changelog.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1787
									
								
								docs/changelog.rst
									
									
									
									
									
								
							
							
						
						
									
										1787
									
								
								docs/changelog.rst
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -14,13 +14,17 @@ extensions = [ | ||||
|     "sphinx.ext.imgmath", | ||||
|     "sphinx.ext.viewcode", | ||||
|     "sphinx_rtd_theme", | ||||
|     "myst_parser", | ||||
| ] | ||||
|  | ||||
| # Add any paths that contain templates here, relative to this directory. | ||||
| templates_path = ["_templates"] | ||||
|  | ||||
| # The suffix of source filenames. | ||||
| source_suffix = ".rst" | ||||
| source_suffix = { | ||||
|     ".rst": "restructuredtext", | ||||
|     ".md": "markdown", | ||||
| } | ||||
|  | ||||
| # The encoding of source files. | ||||
| # source_encoding = 'utf-8-sig' | ||||
|   | ||||
| @@ -111,6 +111,14 @@ PAPERLESS_FILENAME_FORMAT=<format> | ||||
|  | ||||
|     Default is none, which disables this feature. | ||||
|  | ||||
| PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=<bool> | ||||
|     Tells paperless to replace placeholders in `PAPERLESS_FILENAME_FORMAT` that would resolve | ||||
|     to 'none' to be omitted from the resulting filename. This also holds true for directory | ||||
|     names. | ||||
|     See :ref:`advanced-file_name_handling` for details. | ||||
|  | ||||
|     Defaults to `false` which disables this feature. | ||||
|  | ||||
| PAPERLESS_LOGGING_DIR=<path> | ||||
|     This is where paperless will store log files. | ||||
|  | ||||
| @@ -587,6 +595,28 @@ PAPERLESS_CONSUMER_POLLING=<num> | ||||
|  | ||||
|     Defaults to 0, which disables polling and uses filesystem notifications. | ||||
|  | ||||
| PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=<num> | ||||
|     If consumer polling is enabled, sets the number of times paperless will check for a | ||||
|     file to remain unmodified. | ||||
|  | ||||
|     Defaults to 5. | ||||
|  | ||||
| PAPERLESS_CONSUMER_POLLING_DELAY=<num> | ||||
|     If consumer polling is enabled, sets the delay in seconds between each check (above) paperless | ||||
|     will do while waiting for a file to remain unmodified. | ||||
|  | ||||
|     Defaults to 5. | ||||
|  | ||||
| .. _configuration-inotify: | ||||
|  | ||||
| PAPERLESS_CONSUMER_INOTIFY_DELAY=<num> | ||||
|     Sets the time in seconds the consumer will wait for additional events | ||||
|     from inotify before the consumer will consider a file ready and begin consumption. | ||||
|     Certain scanners or network setups may generate multiple events for a single file, | ||||
|     leading to multiple consumers working on the same file.  Configure this to | ||||
|     prevent that. | ||||
|  | ||||
|     Defaults to 0.5 seconds. | ||||
|  | ||||
| PAPERLESS_CONSUMER_DELETE_DUPLICATES=<bool> | ||||
|     When the consumer detects a duplicate document, it will not touch the | ||||
| @@ -647,7 +677,6 @@ PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT | ||||
|  | ||||
|   Defaults to "PATCHT" | ||||
|  | ||||
|  | ||||
| PAPERLESS_CONVERT_MEMORY_LIMIT=<num> | ||||
|     On smaller systems, or even in the case of Very Large Documents, the consumer | ||||
|     may explode, complaining about how it's "unable to extend pixel cache".  In | ||||
| @@ -693,6 +722,9 @@ PAPERLESS_FILENAME_DATE_ORDER=<format> | ||||
|     The filename will be checked first, and if nothing is found, the document | ||||
|     text will be checked as normal. | ||||
|  | ||||
|     A date in a filename must have some separators (`.`, `-`, `/`, etc) | ||||
|     for it to be parsed. | ||||
|  | ||||
|     Defaults to none, which disables this feature. | ||||
|  | ||||
| PAPERLESS_THUMBNAIL_FONT_NAME=<filename> | ||||
| @@ -710,10 +742,7 @@ PAPERLESS_IGNORE_DATES=<string> | ||||
|     this process. This is useful for special dates (like date of birth) that appear | ||||
|     in documents regularly but are very unlikely to be the documents creation date. | ||||
|  | ||||
|     You may specify dates in a multitude of formats supported by dateparser (see | ||||
|     https://dateparser.readthedocs.io/en/latest/#popular-formats) but as the dates | ||||
|     need to be comma separated, the options are limited. | ||||
|     Example: "2020-12-02,22.04.1999" | ||||
|     The date is parsed using the order specified in PAPERLESS_DATE_ORDER | ||||
|  | ||||
|     Defaults to an empty string to not ignore any dates. | ||||
|  | ||||
|   | ||||
| @@ -52,7 +52,7 @@ resources in the documentation: | ||||
| *   Paperless is now integrated with a | ||||
|     :ref:`task processing queue <setup-task_processor>` that tells you | ||||
|     at a glance when and why something is not working. | ||||
| *   The :ref:`changelog <paperless_changelog>` contains a detailed list of all changes | ||||
| *   The :doc:`changelog </changelog>` contains a detailed list of all changes | ||||
|     in paperless-ngx. | ||||
|  | ||||
| Contents | ||||
|   | ||||
| @@ -0,0 +1 @@ | ||||
| myst-parser==0.17.2 | ||||
|   | ||||
| @@ -13,43 +13,45 @@ that works right for you based on recommendations from other Paperless users. | ||||
| Physical scanners | ||||
| ================= | ||||
|  | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brand   | Model          | Supports                                 | Recommended By | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| |         |                | FTP | SFTP | NFS | SMB | SMTP | API [1]_ |                | | ||||
| +=========+================+=====+======+=====+=====+======+==========+================+ | ||||
| | Brother | `ADS-1700W`_   | yes |      |     | yes | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1600W`_   | yes |      |     | yes | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1500W`_   | yes |      |     | yes | yes  |          |`danielquinn`_  | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1100W`_   | yes |      |     |     |      |          |`ytzelf`_       | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-2800W`_   | yes | yes  |     | yes | yes  |          |`philpagel`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-J6930DW`_ | yes |      |     |     |      |          |`ayounggun`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-L5850DW`_ | yes |      |     |     | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-L2750DW`_ | yes |      |     | yes | yes  |          |`muued`_        | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-J5910DW`_ | yes |      |     |     |      |          |`bmsleight`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-8950DW`_  | yes |      |     | yes | yes  |          |`philpagel`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-9142CDN`_ | yes |      |     | yes |      |          |`REOLDEV`_      | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Fujitsu | `ix500`_       | yes |      |     | yes |      |          |`eonist`_       | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Epson   | `ES-580W`_     | yes |      |     | yes | yes  |          |`fignew`_       | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Epson   | `WF-7710DWF`_  | yes |      |     | yes |      |          |`Skylinar`_     | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Fujitsu | `S1300i`_      | yes |      |     | yes |      |          |`jonaswinkler`_ | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Doxie   | `Q2`_          |     |      |     |     |      | yes      |`Unkn0wnCat`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brand   | Model             | Supports                                      | Recommended By | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| |         |                   | FTP | SFTP | NFS | SMB      | SMTP | API [1]_ |                | | ||||
| +=========+===================+=====+======+=====+==========+======+==========+================+ | ||||
| | Brother | `ADS-1700W`_      | yes |      |     | yes      | yes  |          |`holzhannes`_   | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `ADS-1600W`_      | yes |      |     | yes      | yes  |          |`holzhannes`_   | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `ADS-1500W`_      | yes |      |     | yes      | yes  |          |`danielquinn`_  | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `ADS-1100W`_      | yes |      |     |          |      |          |`ytzelf`_       | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `ADS-2800W`_      | yes | yes  |     | yes      | yes  |          |`philpagel`_    | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `MFC-J6930DW`_    | yes |      |     |          |      |          |`ayounggun`_    | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `MFC-L5850DW`_    | yes |      |     |          | yes  |          |`holzhannes`_   | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `MFC-L2750DW`_    | yes |      |     | yes      | yes  |          |`muued`_        | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `MFC-J5910DW`_    | yes |      |     |          |      |          |`bmsleight`_    | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `MFC-8950DW`_     | yes |      |     | yes      | yes  |          |`philpagel`_    | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Brother | `MFC-9142CDN`_    | yes |      |     | yes      |      |          |`REOLDEV`_      | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Canon   | `Maxify MB 5350`_ |     |      |     | yes [2]_ | yes  |          |`eingemaischt`_ | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Fujitsu | `ix500`_          | yes |      |     | yes      |      |          |`eonist`_       | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Epson   | `ES-580W`_        | yes |      |     | yes      | yes  |          |`fignew`_       | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Epson   | `WF-7710DWF`_     | yes |      |     | yes      |      |          |`Skylinar`_     | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Fujitsu | `S1300i`_         | yes |      |     | yes      |      |          |`jonaswinkler`_ | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
| | Doxie   | `Q2`_             |     |      |     |          |      | yes      |`Unkn0wnCat`_   | | ||||
| +---------+-------------------+-----+------+-----+----------+------+----------+----------------+ | ||||
|  | ||||
| .. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw | ||||
| .. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw | ||||
| @@ -58,6 +60,7 @@ Physical scanners | ||||
| .. _ADS-1500W: https://www.brother.ca/en/p/ads1500w | ||||
| .. _ADS-1100W: https://support.brother.com/g/b/downloadtop.aspx?c=fr&lang=fr&prod=ads1100w_eu_as_cn | ||||
| .. _ADS-2800W: https://www.brother-usa.com/products/ads2800w | ||||
| .. _Maxify MB 5350: https://www.canon.de/printers/inkjet/maxify/maxify_mb5350/specification.html | ||||
| .. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW | ||||
| .. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw | ||||
| .. _MFC-8950DW: https://www.brother-usa.com/products/mfc8950dw | ||||
| @@ -81,8 +84,11 @@ Physical scanners | ||||
| .. _Unkn0wnCat: https://github.com/Unkn0wnCat | ||||
| .. _muued: https://github.com/muued | ||||
| .. _philpagel: https://github.com/philpagel | ||||
| .. _eingemaischt: https://github.com/eingemaischt | ||||
|  | ||||
| .. [1] Scanners with API Integration allow to push scanned documents directly to :ref:`Paperless API <api-file_uploads>`, sometimes referred to as Webhook or Document POST. | ||||
| .. [2] Canon Multi Function Printers show strange behavior over SMB. They close and reopen the file after every page. It's recommended to tune the | ||||
|        :ref:`polling <configuration-polling>` and :ref:`inotify <configuration-inotify>` configuration values for your scanner. The scanner timeout is 3 minutes, so ``180`` is a good starting point. | ||||
|  | ||||
| Mobile phone software | ||||
| ===================== | ||||
| @@ -105,6 +111,9 @@ You can use your phone to "scan" documents. The regular camera app will work, bu | ||||
|  | ||||
| On Android, you can use these applications in combination with one of the :ref:`Paperless-ngx compatible apps <usage-mobile_upload>` to "Share" the documents produced by these scanner apps with paperless. On iOS, you can share the scanned documents via iOS-Sharing to other mail, WebDav or FTP apps. | ||||
|  | ||||
| There is also an iOS Shortcut that allows you to directly upload text, PDF and image documents available here: https://www.icloud.com/shortcuts/d234abc0885040129d9d75fa45fe1154 | ||||
| Please note this only works for documents downloaded to iCloud / the device, in other words not directly from a URL. | ||||
|  | ||||
| .. _Office Lens: https://play.google.com/store/apps/details?id=com.microsoft.office.officelens | ||||
| .. _Genius Scan: https://play.google.com/store/apps/details?id=com.thegrizzlylabs.geniusscan.free | ||||
| .. _OCR Scanner - QuickScan: https://apps.apple.com/us/app/quickscan-scanner-text-ocr/id1513790291 | ||||
|   | ||||
| @@ -332,6 +332,12 @@ writing. Windows is not and will never be supported. | ||||
| 3.  Optional. Install ``postgresql`` and configure a database, user and password for paperless. If you do not wish | ||||
|     to use PostgreSQL, SQLite is available as well. | ||||
|  | ||||
|     .. note:: | ||||
|  | ||||
|         On bare-metal installations using SQLite, ensure the | ||||
|         `JSON1 extension <https://code.djangoproject.com/wiki/JSON1Extension>`_ is enabled. This is | ||||
|         usually the case, but not always. | ||||
|  | ||||
| 4.  Get the release archive from `<https://github.com/paperless-ngx/paperless-ngx/releases>`_. | ||||
|     If you clone the git repo as it is, you also have to compile the front end by yourself. | ||||
|     Extract the archive to a place from where you wish to execute it, such as ``/opt/paperless``. | ||||
| @@ -513,7 +519,7 @@ how you installed paperless. | ||||
| This setup describes how to update an existing paperless Docker installation. | ||||
| The important things to keep in mind are as follows: | ||||
|  | ||||
| * Read the :ref:`changelog <paperless_changelog>` and take note of breaking changes. | ||||
| * Read the :doc:`changelog </changelog>` and take note of breaking changes. | ||||
| * You should decide if you want to stick with SQLite or want to migrate your database | ||||
|   to PostgreSQL. See :ref:`setup-sqlite_to_psql` for details on how to move your data from | ||||
|   SQLite to PostgreSQL. Both work fine with paperless. However, if you already have a | ||||
|   | ||||
| @@ -234,3 +234,69 @@ You might find messages like these in your log files: | ||||
| This indicates that paperless failed to read PDF metadata from one of your documents. This happens when you | ||||
| open the affected documents in paperless for editing. Paperless will continue to work, and will simply not | ||||
| show the invalid metadata. | ||||
|  | ||||
| Consumer fails with a FileNotFoundError | ||||
| ####################################### | ||||
|  | ||||
| You might find messages like these in your log files: | ||||
|  | ||||
| .. code:: | ||||
|  | ||||
|     [ERROR] [paperless.consumer] Error while consuming document SCN_0001.pdf: FileNotFoundError: [Errno 2] No such file or directory: '/tmp/ocrmypdf.io.yhk3zbv0/origin.pdf' | ||||
|     Traceback (most recent call last): | ||||
|       File "/app/paperless/src/paperless_tesseract/parsers.py", line 261, in parse | ||||
|         ocrmypdf.ocr(**args) | ||||
|       File "/usr/local/lib/python3.8/dist-packages/ocrmypdf/api.py", line 337, in ocr | ||||
|         return run_pipeline(options=options, plugin_manager=plugin_manager, api=True) | ||||
|       File "/usr/local/lib/python3.8/dist-packages/ocrmypdf/_sync.py", line 385, in run_pipeline | ||||
|         exec_concurrent(context, executor) | ||||
|       File "/usr/local/lib/python3.8/dist-packages/ocrmypdf/_sync.py", line 302, in exec_concurrent | ||||
|         pdf = post_process(pdf, context, executor) | ||||
|       File "/usr/local/lib/python3.8/dist-packages/ocrmypdf/_sync.py", line 235, in post_process | ||||
|         pdf_out = metadata_fixup(pdf_out, context) | ||||
|       File "/usr/local/lib/python3.8/dist-packages/ocrmypdf/_pipeline.py", line 798, in metadata_fixup | ||||
|         with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf: | ||||
|       File "/usr/local/lib/python3.8/dist-packages/pikepdf/_methods.py", line 923, in open | ||||
|         pdf = Pdf._open( | ||||
|     FileNotFoundError: [Errno 2] No such file or directory: '/tmp/ocrmypdf.io.yhk3zbv0/origin.pdf' | ||||
|  | ||||
| This probably indicates paperless tried to consume the same file twice.  This can happen for a number of reasons, | ||||
| depending on how documents are placed into the consume folder.  If paperless is using inotify (the default) to | ||||
| check for documents, try adjusting the :ref:`inotify configuration <configuration-inotify>`.  If polling is enabled, | ||||
| try adjusting the :ref:`polling configuration <configuration-polling>`. | ||||
|  | ||||
| Consumer fails waiting for file to remain unmodified. | ||||
| ##################################################### | ||||
|  | ||||
| You might find messages like these in your log files: | ||||
|  | ||||
| .. code:: | ||||
|  | ||||
|     [ERROR] [paperless.management.consumer] Timeout while waiting on file /usr/src/paperless/src/../consume/SCN_0001.pdf to remain unmodified. | ||||
|  | ||||
| This indicates paperless timed out while waiting for the file to be completely written to the consume folder. | ||||
| Adjusting :ref:`polling configuration <configuration-polling>` values should resolve the issue. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     The user will need to manually move the file out of the consume folder and | ||||
|     back in, for the initial failing file to be consumed. | ||||
|  | ||||
| Consumer fails reporting "OS reports file as busy still". | ||||
| ######################################################### | ||||
|  | ||||
| You might find messages like these in your log files: | ||||
|  | ||||
| .. code:: | ||||
|  | ||||
|     [WARNING] [paperless.management.consumer] Not consuming file /usr/src/paperless/src/../consume/SCN_0001.pdf: OS reports file as busy still | ||||
|  | ||||
| This indicates paperless was unable to open the file, as the OS reported the file as still being in use.  To prevent a | ||||
| crash, paperless did not try to consume the file.  If paperless is using inotify (the default) to | ||||
| check for documents, try adjusting the :ref:`inotify configuration <configuration-inotify>`.  If polling is enabled, | ||||
| try adjusting the :ref:`polling configuration <configuration-polling>`. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     The user will need to manually move the file out of the consume folder and | ||||
|     back in, for the initial failing file to be consumed. | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import os | ||||
|  | ||||
| bind = f'0.0.0.0:{os.getenv("PAPERLESS_PORT", 8000)}' | ||||
| bind = f'[::]:{os.getenv("PAPERLESS_PORT", 8000)}' | ||||
| workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 2)) | ||||
| worker_class = "paperless.workers.ConfigurableWorker" | ||||
| timeout = 120 | ||||
| @@ -24,7 +24,7 @@ def worker_int(worker): | ||||
|     ## get traceback info | ||||
|     import threading, sys, traceback | ||||
|  | ||||
|     id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) | ||||
|     id2name = {th.ident: th.name for th in threading.enumerate()} | ||||
|     code = [] | ||||
|     for threadId, stack in sys._current_frames().items(): | ||||
|         code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId)) | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| #PAPERLESS_MEDIA_ROOT=../media | ||||
| #PAPERLESS_STATICDIR=../static | ||||
| #PAPERLESS_FILENAME_FORMAT= | ||||
| #PAPERLESS_FILENAME_FORMAT_REMOVE_NONE= | ||||
|  | ||||
| # Security and hosting | ||||
|  | ||||
|   | ||||
| @@ -8,12 +8,12 @@ | ||||
| -i https://pypi.python.org/simple | ||||
| --extra-index-url https://www.piwheels.org/simple | ||||
| aioredis==1.3.1 | ||||
| anyio==3.5.0; python_full_version >= '3.6.2' | ||||
| anyio==3.6.1; python_full_version >= '3.6.2' | ||||
| arrow==1.2.2; python_version >= '3.6' | ||||
| asgiref==3.5.1; python_version >= '3.7' | ||||
| asgiref==3.5.2; python_version >= '3.7' | ||||
| async-timeout==4.0.2; python_version >= '3.6' | ||||
| attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||
| autobahn==22.3.2; python_version >= '3.7' | ||||
| autobahn==22.4.2; python_version >= '3.7' | ||||
| automat==20.2.0 | ||||
| backports.zoneinfo==0.2.1; python_version < '3.9' | ||||
| blessed==1.19.1; python_version >= '2.7' | ||||
| @@ -21,23 +21,22 @@ certifi==2021.10.8 | ||||
| cffi==1.15.0 | ||||
| channels-redis==3.4.0 | ||||
| channels==3.0.4 | ||||
| chardet==4.0.0; python_version >= '3.1' | ||||
| charset-normalizer==2.0.12; python_version >= '3' | ||||
| charset-normalizer==2.0.12; python_version >= '3.5' | ||||
| click==8.1.3; python_version >= '3.7' | ||||
| coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||
| concurrent-log-handler==0.9.20 | ||||
| constantly==15.1.0 | ||||
| cryptography==37.0.1; python_version >= '3.6' | ||||
| cryptography==36.0.2; python_version >= '3.6' | ||||
| daphne==3.0.2; python_version >= '3.6' | ||||
| dateparser==1.1.1 | ||||
| django-cors-headers==3.11.0 | ||||
| django-cors-headers==3.12.0 | ||||
| django-extensions==3.1.5 | ||||
| django-filter==21.1 | ||||
| django-picklefield==3.0.1; python_version >= '3' | ||||
| django-q==1.3.9 | ||||
| django==4.0.4 | ||||
| djangorestframework==3.13.1 | ||||
| filelock==3.6.0 | ||||
| filelock==3.7.0 | ||||
| fuzzywuzzy[speedup]==0.18.0 | ||||
| gunicorn==20.1.0 | ||||
| h11==0.13.0; python_version >= '3.6' | ||||
| @@ -46,7 +45,7 @@ httptools==0.4.0 | ||||
| humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||
| hyperlink==21.0.0 | ||||
| idna==3.3; python_version >= '3' | ||||
| imap-tools==0.54.0 | ||||
| imap-tools==0.55.0 | ||||
| img2pdf==0.4.4 | ||||
| importlib-resources==5.7.1; python_version < '3.9' | ||||
| incremental==21.3.0 | ||||
| @@ -57,12 +56,12 @@ langdetect==1.0.9 | ||||
| lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||
| msgpack==1.0.3 | ||||
| numpy==1.22.3; python_version >= '3.8' | ||||
| ocrmypdf==13.4.3 | ||||
| ocrmypdf==13.4.4 | ||||
| packaging==21.3; python_version >= '3.6' | ||||
| pathvalidate==2.5.0 | ||||
| pdf2image==1.16.0 | ||||
| pdfminer.six==20220319 | ||||
| pikepdf==5.1.2 | ||||
| pdfminer.six==20220506 | ||||
| pikepdf==5.1.3 | ||||
| pillow==9.1.0 | ||||
| pluggy==1.0.0; python_version >= '3.6' | ||||
| portalocker==2.4.0; python_version >= '3' | ||||
| @@ -71,7 +70,7 @@ pyasn1-modules==0.2.8 | ||||
| pyasn1==0.4.8 | ||||
| pycparser==2.21 | ||||
| pyopenssl==22.0.0 | ||||
| pyparsing==3.0.8; python_full_version >= '3.6.8' | ||||
| pyparsing==3.0.9; python_full_version >= '3.6.8' | ||||
| python-dateutil==2.8.2 | ||||
| python-dotenv==0.20.0 | ||||
| python-gnupg==0.4.8 | ||||
| @@ -88,7 +87,7 @@ requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3 | ||||
| scikit-learn==1.0.2 | ||||
| scipy==1.8.0; python_version < '3.11' and python_version >= '3.8' | ||||
| service-identity==21.1.0 | ||||
| setuptools==62.1.0; python_version >= '3.7' | ||||
| setuptools==62.2.0; python_version >= '3.7' | ||||
| six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||
| sniffio==1.2.0; python_version >= '3.5' | ||||
| sqlparse==0.4.2; python_version >= '3.5' | ||||
| @@ -103,7 +102,7 @@ tzlocal==4.2; python_version >= '3.6' | ||||
| urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' | ||||
| uvicorn[standard]==0.17.6 | ||||
| uvloop==0.16.0 | ||||
| watchdog==2.1.7 | ||||
| watchdog==2.1.8 | ||||
| watchgod==0.8.2 | ||||
| wcwidth==0.2.5 | ||||
| websockets==10.3 | ||||
|   | ||||
| @@ -6,4 +6,4 @@ | ||||
|   "pluginsFile": "cypress/plugins/index.ts", | ||||
|   "fixturesFolder": "cypress/fixtures", | ||||
|   "baseUrl": "http://localhost:4200" | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										34
									
								
								src-ui/cypress/fixtures/ui_settings/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src-ui/cypress/fixtures/ui_settings/settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| { | ||||
|     "user_id": 1, | ||||
|     "username": "admin", | ||||
|     "display_name": "Admin", | ||||
|     "settings": { | ||||
|         "language": "", | ||||
|         "bulk_edit": { | ||||
|             "confirmation_dialogs": true, | ||||
|             "apply_on_close": false | ||||
|         }, | ||||
|         "documentListSize": 50, | ||||
|         "dark_mode": { | ||||
|             "use_system": true, | ||||
|             "enabled": "false", | ||||
|             "thumb_inverted": "true" | ||||
|         }, | ||||
|         "theme": { | ||||
|             "color": "#b198e5" | ||||
|         }, | ||||
|         "document_details": { | ||||
|             "native_pdf_viewer": false | ||||
|         }, | ||||
|         "date_display": { | ||||
|             "date_locale": "", | ||||
|             "date_format": "mediumDate" | ||||
|         }, | ||||
|         "notifications": { | ||||
|             "consumer_new_documents": true, | ||||
|             "consumer_success": true, | ||||
|             "consumer_failed": true, | ||||
|             "consumer_suppress_on_dashboard": true | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,6 +2,9 @@ describe('document-detail', () => { | ||||
|   beforeEach(() => { | ||||
|     this.modifiedDocuments = [] | ||||
|  | ||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { | ||||
|       fixture: 'ui_settings/settings.json', | ||||
|     }) | ||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||
|         let response = { ...documentsJson } | ||||
|   | ||||
| @@ -3,6 +3,9 @@ describe('documents-list', () => { | ||||
|     this.bulkEdits = {} | ||||
|  | ||||
|     // mock API methods | ||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { | ||||
|       fixture: 'ui_settings/settings.json', | ||||
|     }) | ||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|       // bulk edit | ||||
|       cy.intercept( | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| describe('manage', () => { | ||||
|   beforeEach(() => { | ||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { | ||||
|       fixture: 'ui_settings/settings.json', | ||||
|     }) | ||||
|     cy.intercept('http://localhost:8000/api/correspondents/*', { | ||||
|       fixture: 'correspondents/correspondents.json', | ||||
|     }) | ||||
| @@ -26,7 +29,7 @@ describe('manage', () => { | ||||
|         req.reply({ count: 3, next: null, previous: null, results: [] }) | ||||
|     }) | ||||
|     cy.visit('/tags') | ||||
|     cy.get('tbody').find('button').contains('Documents').first().click() // id = 4 | ||||
|     cy.get('tbody').find('button:visible').contains('Documents').first().click() // id = 4 | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -3,45 +3,53 @@ describe('settings', () => { | ||||
|     this.modifiedViews = [] | ||||
|  | ||||
|     // mock API methods | ||||
|     cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { | ||||
|       // saved views PATCH | ||||
|       cy.intercept( | ||||
|         'PATCH', | ||||
|         'http://localhost:8000/api/saved_views/*', | ||||
|         (req) => { | ||||
|           this.modifiedViews.push(req.body) // store this for later | ||||
|           req.reply({ result: 'OK' }) | ||||
|         } | ||||
|       ) | ||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { | ||||
|       fixture: 'ui_settings/settings.json', | ||||
|     }).then(() => { | ||||
|       cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { | ||||
|         // saved views PATCH | ||||
|         cy.intercept( | ||||
|           'PATCH', | ||||
|           'http://localhost:8000/api/saved_views/*', | ||||
|           (req) => { | ||||
|             this.modifiedViews.push(req.body) // store this for later | ||||
|             req.reply({ result: 'OK' }) | ||||
|           } | ||||
|         ) | ||||
|  | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/saved_views/*', (req) => { | ||||
|         let response = { ...savedViewsJson } | ||||
|         if (this.modifiedViews.length) { | ||||
|           response.results = response.results.map((v) => { | ||||
|             if (this.modifiedViews.find((mv) => mv.id == v.id)) | ||||
|               v = this.modifiedViews.find((mv) => mv.id == v.id) | ||||
|             return v | ||||
|           }) | ||||
|         } | ||||
|         cy.intercept( | ||||
|           'GET', | ||||
|           'http://localhost:8000/api/saved_views/*', | ||||
|           (req) => { | ||||
|             let response = { ...savedViewsJson } | ||||
|             if (this.modifiedViews.length) { | ||||
|               response.results = response.results.map((v) => { | ||||
|                 if (this.modifiedViews.find((mv) => mv.id == v.id)) | ||||
|                   v = this.modifiedViews.find((mv) => mv.id == v.id) | ||||
|                 return v | ||||
|               }) | ||||
|             } | ||||
|  | ||||
|         req.reply(response) | ||||
|       }).as('savedViews') | ||||
|     }) | ||||
|  | ||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||
|         let response = { ...documentsJson } | ||||
|         response = response.results.find((d) => d.id == 1) | ||||
|         req.reply(response) | ||||
|             req.reply(response) | ||||
|           } | ||||
|         ).as('savedViews') | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     cy.intercept('http://localhost:8000/api/documents/1/metadata/', { | ||||
|       fixture: 'documents/1/metadata.json', | ||||
|     }) | ||||
|       cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|         cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||
|           let response = { ...documentsJson } | ||||
|           response = response.results.find((d) => d.id == 1) | ||||
|           req.reply(response) | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
|     cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { | ||||
|       fixture: 'documents/1/suggestions.json', | ||||
|       cy.intercept('http://localhost:8000/api/documents/1/metadata/', { | ||||
|         fixture: 'documents/1/metadata.json', | ||||
|       }) | ||||
|  | ||||
|       cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { | ||||
|         fixture: 'documents/1/suggestions.json', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     cy.viewport(1024, 1024) | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -12,6 +12,7 @@ import { TagListComponent } from './components/manage/tag-list/tag-list.componen | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component' | ||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | ||||
| import { DirtyFormGuard } from './guards/dirty-form.guard' | ||||
| import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component' | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, | ||||
| @@ -27,6 +28,7 @@ const routes: Routes = [ | ||||
|       { path: 'tags', component: TagListComponent }, | ||||
|       { path: 'documenttypes', component: DocumentTypeListComponent }, | ||||
|       { path: 'correspondents', component: CorrespondentListComponent }, | ||||
|       { path: 'storagepaths', component: StoragePathListComponent }, | ||||
|       { path: 'logs', component: LogsComponent }, | ||||
|       { | ||||
|         path: 'settings', | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { SettingsService, SETTINGS_KEYS } from './services/settings.service' | ||||
| import { SettingsService } from './services/settings.service' | ||||
| import { SETTINGS_KEYS } from './data/paperless-uisettings' | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { Subscription } from 'rxjs' | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { BrowserModule } from '@angular/platform-browser' | ||||
| import { NgModule } from '@angular/core' | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core' | ||||
| import { AppRoutingModule } from './app-routing.module' | ||||
| import { AppComponent } from './app.component' | ||||
| import { | ||||
| @@ -87,6 +87,9 @@ import localeSr from '@angular/common/locales/sr' | ||||
| import localeSv from '@angular/common/locales/sv' | ||||
| import localeTr from '@angular/common/locales/tr' | ||||
| import localeZh from '@angular/common/locales/zh' | ||||
| import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component' | ||||
| import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' | ||||
| import { SettingsService } from './services/settings.service' | ||||
|  | ||||
| registerLocaleData(localeBe) | ||||
| registerLocaleData(localeCs) | ||||
| @@ -109,6 +112,12 @@ registerLocaleData(localeSv) | ||||
| registerLocaleData(localeTr) | ||||
| registerLocaleData(localeZh) | ||||
|  | ||||
| function initializeApp(settings: SettingsService) { | ||||
|   return () => { | ||||
|     return settings.initializeSettings() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
|     AppComponent, | ||||
| @@ -118,6 +127,7 @@ registerLocaleData(localeZh) | ||||
|     TagListComponent, | ||||
|     DocumentTypeListComponent, | ||||
|     CorrespondentListComponent, | ||||
|     StoragePathListComponent, | ||||
|     LogsComponent, | ||||
|     SettingsComponent, | ||||
|     NotFoundComponent, | ||||
| @@ -125,6 +135,7 @@ registerLocaleData(localeZh) | ||||
|     ConfirmDialogComponent, | ||||
|     TagEditDialogComponent, | ||||
|     DocumentTypeEditDialogComponent, | ||||
|     StoragePathEditDialogComponent, | ||||
|     TagComponent, | ||||
|     PageHeaderComponent, | ||||
|     AppFrameComponent, | ||||
| @@ -174,6 +185,12 @@ registerLocaleData(localeZh) | ||||
|     ColorSliderModule, | ||||
|   ], | ||||
|   providers: [ | ||||
|     { | ||||
|       provide: APP_INITIALIZER, | ||||
|       useFactory: initializeApp, | ||||
|       deps: [SettingsService], | ||||
|       multi: true, | ||||
|     }, | ||||
|     DatePipe, | ||||
|     CookieService, | ||||
|     { | ||||
|   | ||||
| @@ -21,17 +21,17 @@ | ||||
|   </div> | ||||
|   <ul ngbNav class="order-sm-3"> | ||||
|     <li ngbDropdown class="nav-item dropdown"> | ||||
|       <button class="btn text-light" id="userDropdown" ngbDropdownToggle> | ||||
|         <span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline"> | ||||
|           {{displayName}} | ||||
|       <button class="btn" id="userDropdown" ngbDropdownToggle> | ||||
|         <span class="small me-2 d-none d-sm-inline"> | ||||
|           {{this.settingsService.displayName}} | ||||
|         </span> | ||||
|         <svg width="1.3em" height="1.3em" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#person-circle"/> | ||||
|         </svg> | ||||
|       </button> | ||||
|       <div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown"> | ||||
|         <div *ngIf="displayName" class="d-sm-none"> | ||||
|           <p class="small mb-0 px-3 text-muted" i18n>Logged in as {{displayName}}</p> | ||||
|         <div class="d-sm-none"> | ||||
|           <p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p> | ||||
|           <div class="dropdown-divider"></div> | ||||
|         </div> | ||||
|         <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"> | ||||
| @@ -70,8 +70,9 @@ | ||||
|           </li> | ||||
|         </ul> | ||||
|  | ||||
|         <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'> | ||||
|         <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'> | ||||
|           <ng-container i18n>Saved views</ng-container> | ||||
|           <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"> | ||||
|           <li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews"> | ||||
| @@ -133,6 +134,13 @@ | ||||
|               </svg> <ng-container i18n>Document types</ng-container> | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#folder"/> | ||||
|               </svg> <ng-container i18n>Storage paths</ng-container> | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
| @@ -187,7 +195,7 @@ | ||||
|               <div class="me-3">{{ versionString }}</div> | ||||
|               <div *ngIf="appRemoteVersion" class="version-check"> | ||||
|                 <ng-template #updateAvailablePopContent> | ||||
|                   <span class="small">Paperless-ngx v{{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> | ||||
|                   <span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> | ||||
|                 </ng-template> | ||||
|                 <ng-template #updateCheckingNotEnabledPopContent> | ||||
|                   <span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span> | ||||
|   | ||||
| @@ -9,6 +9,11 @@ | ||||
|   z-index: 995; /* Behind the navbar */ | ||||
|   padding: 50px 0 0; /* Height of navbar */ | ||||
|   box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); | ||||
|  | ||||
|   .sidebar-heading .spinner-border { | ||||
|     width: 0.8em; | ||||
|     height: 0.8em; | ||||
|   } | ||||
| } | ||||
| @media (max-width: 767.98px) { | ||||
|   .sidebar { | ||||
|   | ||||
| @@ -22,6 +22,8 @@ import { | ||||
|   RemoteVersionService, | ||||
|   AppRemoteVersion, | ||||
| } from 'src/app/services/rest/remote-version.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-app-frame', | ||||
| @@ -35,9 +37,9 @@ export class AppFrameComponent { | ||||
|     private openDocumentsService: OpenDocumentsService, | ||||
|     private searchService: SearchService, | ||||
|     public savedViewService: SavedViewService, | ||||
|     private list: DocumentListViewService, | ||||
|     private meta: Meta, | ||||
|     private remoteVersionService: RemoteVersionService | ||||
|     private remoteVersionService: RemoteVersionService, | ||||
|     private queryParamsService: QueryParamsService, | ||||
|     public settingsService: SettingsService | ||||
|   ) { | ||||
|     this.remoteVersionService | ||||
|       .checkForUpdates() | ||||
| @@ -92,7 +94,7 @@ export class AppFrameComponent { | ||||
|  | ||||
|   search() { | ||||
|     this.closeMenu() | ||||
|     this.list.quickFilter([ | ||||
|     this.queryParamsService.navigateWithFilterRules([ | ||||
|       { | ||||
|         rule_type: FILTER_FULLTEXT_QUERY, | ||||
|         value: (this.searchField.value as string).trim(), | ||||
| @@ -141,17 +143,4 @@ export class AppFrameComponent { | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   get displayName() { | ||||
|     // TODO: taken from dashboard component, is this the best way to pass around username? | ||||
|     let tagFullName = this.meta.getTag('name=full_name') | ||||
|     let tagUsername = this.meta.getTag('name=username') | ||||
|     if (tagFullName && tagFullName.content) { | ||||
|       return tagFullName.content | ||||
|     } else if (tagUsername && tagUsername.content) { | ||||
|       return tagUsername.content | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <p *ngIf="messageBold"><b>{{messageBold}}</b></p> | ||||
|       <p *ngIf="message">{{message}}</p> | ||||
|       <p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> | ||||
|   | ||||
| @@ -0,0 +1,24 @@ | ||||
| <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||
|   <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"> | ||||
|  | ||||
|     <p *ngIf="this.dialogMode == 'edit'" i18n> | ||||
|       <em>Note that editing a path does not apply changes to stored files until you have run the 'document_renamer' utility. See the <a target="_blank" href="https://paperless-ngx.readthedocs.io/en/latest/administration.html#utilities-renamer">documentation</a>.</em> | ||||
|     </p> | ||||
|  | ||||
|     <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||
|     <app-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></app-input-text> | ||||
|     <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> | ||||
|     <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> | ||||
|     <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> | ||||
|  | ||||
|   </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> | ||||
| @@ -0,0 +1,50 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-storage-path-edit-dialog', | ||||
|   templateUrl: './storage-path-edit-dialog.component.html', | ||||
|   styleUrls: ['./storage-path-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { | ||||
|   constructor( | ||||
|     service: StoragePathService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   } | ||||
|  | ||||
|   get pathHint() { | ||||
|     return ( | ||||
|       $localize`e.g.` + | ||||
|       ' <code>{created_year}-{title}</code> ' + | ||||
|       $localize`or use slashes to add directories e.g.` + | ||||
|       ' <code>{created_year}/{correspondent}/{title}</code>. ' + | ||||
|       $localize`See <a target="_blank" href="https://paperless-ngx.readthedocs.io/en/latest/advanced_usage.html#file-name-handling">documentation</a> for full list.` | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|     return $localize`Create new storage path` | ||||
|   } | ||||
|  | ||||
|   getEditTitle() { | ||||
|     return $localize`Edit storage path` | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       path: new FormControl(''), | ||||
|       matching_algorithm: new FormControl(1), | ||||
|       match: new FormControl(''), | ||||
|       is_insensitive: new FormControl(true), | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| @@ -9,7 +9,8 @@ | ||||
|         [items]="items" | ||||
|         [addTag]="allowCreateNew && addItemRef" | ||||
|         addTagText="Add item" | ||||
|         i18n-addTagText="Used for both types and correspondents" | ||||
|         i18n-addTagText="Used for both types, correspondents, storage paths" | ||||
|         [placeholder]="placeholder" | ||||
|         bindLabel="name" | ||||
|         bindValue="id" | ||||
|         (change)="onChange(value)" | ||||
|   | ||||
| @@ -41,6 +41,9 @@ export class SelectComponent extends AbstractInputComponent<number> { | ||||
|   @Input() | ||||
|   suggestions: number[] | ||||
|  | ||||
|   @Input() | ||||
|   placeholder: string | ||||
|  | ||||
|   @Output() | ||||
|   createNew = new EventEmitter<string>() | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <div class="mb-3"> | ||||
|   <label class="form-label" [for]="inputId">{{title}}</label> | ||||
|   <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> | ||||
|   <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||
|   <small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   </div> | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|  | ||||
| .toast:not(.show) { | ||||
|   display: block; // this corrects an ng-bootstrap bug that prevented animations | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -21,9 +21,14 @@ | ||||
|  | ||||
| <div class='row'> | ||||
|   <div class="col-lg-8"> | ||||
|     <app-welcome-widget *ngIf="savedViews.length == 0"></app-welcome-widget> | ||||
|     <ng-container *ngIf="savedViewService.loading"> | ||||
|       <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|       <ng-container i18n>Loading...</ng-container> | ||||
|     </ng-container> | ||||
|  | ||||
|     <ng-container *ngFor="let v of savedViews"> | ||||
|     <app-welcome-widget *ngIf="!savedViewService.loading && savedViewService.dashboardViews.length == 0"></app-welcome-widget> | ||||
|  | ||||
|     <ng-container *ngFor="let v of savedViewService.dashboardViews"> | ||||
|       <app-saved-view-widget [savedView]="v"></app-saved-view-widget> | ||||
|     </ng-container> | ||||
|  | ||||
|   | ||||
| @@ -1,43 +1,24 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Meta } from '@angular/platform-browser' | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||
| import { SavedViewService } from 'src/app/services/rest/saved-view.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-dashboard', | ||||
|   templateUrl: './dashboard.component.html', | ||||
|   styleUrls: ['./dashboard.component.scss'], | ||||
| }) | ||||
| export class DashboardComponent implements OnInit { | ||||
|   constructor(private savedViewService: SavedViewService, private meta: Meta) {} | ||||
|  | ||||
|   get displayName() { | ||||
|     let tagFullName = this.meta.getTag('name=full_name') | ||||
|     let tagUsername = this.meta.getTag('name=username') | ||||
|     if (tagFullName && tagFullName.content) { | ||||
|       return tagFullName.content | ||||
|     } else if (tagUsername && tagUsername.content) { | ||||
|       return tagUsername.content | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
| export class DashboardComponent { | ||||
|   constructor( | ||||
|     public savedViewService: SavedViewService, | ||||
|     public settingsService: SettingsService | ||||
|   ) {} | ||||
|  | ||||
|   get subtitle() { | ||||
|     if (this.displayName) { | ||||
|       return $localize`Hello ${this.displayName}, welcome to Paperless-ngx!` | ||||
|     if (this.settingsService.displayName) { | ||||
|       return $localize`Hello ${this.settingsService.displayName}, welcome to Paperless-ngx!` | ||||
|     } else { | ||||
|       return $localize`Welcome to Paperless-ngx!` | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   savedViews: PaperlessSavedView[] = [] | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.savedViewService.listAll().subscribe((results) => { | ||||
|       this.savedViews = results.results.filter( | ||||
|         (savedView) => savedView.show_on_dashboard | ||||
|       ) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <app-widget-frame [title]="savedView.name"> | ||||
| <app-widget-frame [title]="savedView.name" [loading]="loading"> | ||||
|  | ||||
|   <a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a> | ||||
|  | ||||
| @@ -11,7 +11,7 @@ | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let doc of documents" [routerLink]="['/', 'documents', doc.id]"> | ||||
|       <tr *ngFor="let doc of documents" (click)="openDocumentsService.openDocument(doc)"> | ||||
|         <td>{{doc.created | customDate}}</td> | ||||
|         <td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t); $event.stopPropagation();"></app-tag></td> | ||||
|       </tr> | ||||
|   | ||||
| @@ -3,11 +3,12 @@ import { Router } from '@angular/router' | ||||
| import { Subscription } from 'rxjs' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||
| import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-saved-view-widget', | ||||
| @@ -15,11 +16,14 @@ import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type' | ||||
|   styleUrls: ['./saved-view-widget.component.scss'], | ||||
| }) | ||||
| export class SavedViewWidgetComponent implements OnInit, OnDestroy { | ||||
|   loading: boolean = true | ||||
|  | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private router: Router, | ||||
|     private list: DocumentListViewService, | ||||
|     private consumerStatusService: ConsumerStatusService | ||||
|     private queryParamsService: QueryParamsService, | ||||
|     private consumerStatusService: ConsumerStatusService, | ||||
|     public openDocumentsService: OpenDocumentsService | ||||
|   ) {} | ||||
|  | ||||
|   @Input() | ||||
| @@ -43,6 +47,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
|     this.loading = true | ||||
|     this.documentService | ||||
|       .listFiltered( | ||||
|         1, | ||||
| @@ -52,6 +57,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy { | ||||
|         this.savedView.filter_rules | ||||
|       ) | ||||
|       .subscribe((result) => { | ||||
|         this.loading = false | ||||
|         this.documents = result.results | ||||
|       }) | ||||
|   } | ||||
| @@ -60,13 +66,14 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy { | ||||
|     if (this.savedView.show_in_sidebar) { | ||||
|       this.router.navigate(['view', this.savedView.id]) | ||||
|     } else { | ||||
|       this.list.loadSavedView(this.savedView, true) | ||||
|       this.router.navigate(['documents']) | ||||
|       this.router.navigate(['documents'], { | ||||
|         queryParams: { view: this.savedView.id }, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   clickTag(tag: PaperlessTag) { | ||||
|     this.list.quickFilter([ | ||||
|     this.queryParamsService.navigateWithFilterRules([ | ||||
|       { rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() }, | ||||
|     ]) | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <app-widget-frame title="Statistics" i18n-title> | ||||
| <app-widget-frame title="Statistics" [loading]="loading" i18n-title> | ||||
|   <ng-container content> | ||||
|     <p class="card-text" i18n *ngIf="statistics?.documents_inbox != null">Documents in inbox: {{statistics?.documents_inbox}}</p> | ||||
|     <p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p> | ||||
|   | ||||
| @@ -15,6 +15,8 @@ export interface Statistics { | ||||
|   styleUrls: ['./statistics-widget.component.scss'], | ||||
| }) | ||||
| export class StatisticsWidgetComponent implements OnInit, OnDestroy { | ||||
|   loading: boolean = true | ||||
|  | ||||
|   constructor( | ||||
|     private http: HttpClient, | ||||
|     private consumerStatusService: ConsumerStatusService | ||||
| @@ -29,7 +31,9 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
|     this.loading = true | ||||
|     this.getStatistics().subscribe((statistics) => { | ||||
|       this.loading = false | ||||
|       this.statistics = statistics | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -2,6 +2,10 @@ | ||||
|   <div class="card-header"> | ||||
|     <div class="d-flex justify-content-between align-items-center"> | ||||
|       <h5 class="card-title mb-0">{{title}}</h5> | ||||
|       <ng-container *ngIf="loading"> | ||||
|         <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|         <div class="visually-hidden" i18n>Loading...</div> | ||||
|       </ng-container> | ||||
|       <ng-content select ="[header-buttons]"></ng-content> | ||||
|     </div> | ||||
|  | ||||
|   | ||||
| @@ -11,5 +11,8 @@ export class WidgetFrameComponent implements OnInit { | ||||
|   @Input() | ||||
|   title: string | ||||
|  | ||||
|   @Input() | ||||
|   loading: boolean = false | ||||
|  | ||||
|   ngOnInit(): void {} | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ | ||||
|  | ||||
|     <button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="moreLike()"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#three-dots" /> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#diagram-3" /> | ||||
|         </svg> <span class="d-none d-lg-inline" i18n>More like this</span> | ||||
|     </button> | ||||
|  | ||||
| @@ -73,6 +73,8 @@ | ||||
|                             (createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select> | ||||
|                         <app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" | ||||
|                             (createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types"></app-input-select> | ||||
|                         <app-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" | ||||
|                             (createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default"></app-input-select> | ||||
|                         <app-input-tags formControlName="tags" [suggestions]="suggestions?.tags"></app-input-tags> | ||||
|  | ||||
|                     </ng-template> | ||||
|   | ||||
| @@ -18,10 +18,7 @@ import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document- | ||||
| import { PDFDocumentProxy } from 'ng2-pdf-viewer' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { TextComponent } from '../common/input/text/text.component' | ||||
| import { | ||||
|   SettingsService, | ||||
|   SETTINGS_KEYS, | ||||
| } from 'src/app/services/settings.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' | ||||
| import { Observable, Subject, BehaviorSubject } from 'rxjs' | ||||
| import { | ||||
| @@ -35,6 +32,11 @@ import { | ||||
| import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions' | ||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | ||||
| import { normalizeDateStr } from 'src/app/utils/date' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-detail', | ||||
| @@ -67,6 +69,7 @@ export class DocumentDetailComponent | ||||
|  | ||||
|   correspondents: PaperlessCorrespondent[] | ||||
|   documentTypes: PaperlessDocumentType[] | ||||
|   storagePaths: PaperlessStoragePath[] | ||||
|  | ||||
|   documentForm: FormGroup = new FormGroup({ | ||||
|     title: new FormControl(''), | ||||
| @@ -74,6 +77,7 @@ export class DocumentDetailComponent | ||||
|     created: new FormControl(), | ||||
|     correspondent: new FormControl(), | ||||
|     document_type: new FormControl(), | ||||
|     storage_path: new FormControl(), | ||||
|     archive_serial_number: new FormControl(), | ||||
|     tags: new FormControl([]), | ||||
|   }) | ||||
| @@ -84,6 +88,7 @@ export class DocumentDetailComponent | ||||
|   store: BehaviorSubject<any> | ||||
|   isDirty$: Observable<boolean> | ||||
|   unsubscribeNotifier: Subject<any> = new Subject() | ||||
|   docChangeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   requiresPassword: boolean = false | ||||
|   password: string | ||||
| @@ -114,19 +119,10 @@ export class DocumentDetailComponent | ||||
|     private documentListViewService: DocumentListViewService, | ||||
|     private documentTitlePipe: DocumentTitlePipe, | ||||
|     private toastService: ToastService, | ||||
|     private settings: SettingsService | ||||
|   ) { | ||||
|     this.titleSubject | ||||
|       .pipe( | ||||
|         debounceTime(1000), | ||||
|         distinctUntilChanged(), | ||||
|         takeUntil(this.unsubscribeNotifier) | ||||
|       ) | ||||
|       .subscribe((titleValue) => { | ||||
|         this.title = titleValue | ||||
|         this.documentForm.patchValue({ title: titleValue }) | ||||
|       }) | ||||
|   } | ||||
|     private settings: SettingsService, | ||||
|     private storagePathService: StoragePathService, | ||||
|     private queryParamsService: QueryParamsService | ||||
|   ) {} | ||||
|  | ||||
|   titleKeyUp(event) { | ||||
|     this.titleSubject.next(event.target?.value) | ||||
| @@ -173,15 +169,23 @@ export class DocumentDetailComponent | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.correspondents = result.results)) | ||||
|  | ||||
|     this.documentTypeService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.documentTypes = result.results)) | ||||
|  | ||||
|     this.storagePathService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.storagePaths = result.results)) | ||||
|  | ||||
|     this.route.paramMap | ||||
|       .pipe( | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         switchMap((paramMap) => { | ||||
|           const documentId = +paramMap.get('id') | ||||
|           this.docChangeNotifier.next(documentId) | ||||
|           return this.documentsService.get(documentId) | ||||
|         }) | ||||
|       ) | ||||
| @@ -202,10 +206,33 @@ export class DocumentDetailComponent | ||||
|               this.openDocumentService.getOpenDocument(this.documentId) | ||||
|             ) | ||||
|           } else { | ||||
|             this.openDocumentService.openDocument(doc) | ||||
|             this.openDocumentService.openDocument(doc, false) | ||||
|             this.updateComponent(doc) | ||||
|           } | ||||
|  | ||||
|           this.titleSubject | ||||
|             .pipe( | ||||
|               debounceTime(1000), | ||||
|               distinctUntilChanged(), | ||||
|               takeUntil(this.docChangeNotifier), | ||||
|               takeUntil(this.unsubscribeNotifier) | ||||
|             ) | ||||
|             .subscribe({ | ||||
|               next: (titleValue) => { | ||||
|                 this.title = titleValue | ||||
|                 this.documentForm.patchValue({ title: titleValue }) | ||||
|               }, | ||||
|               complete: () => { | ||||
|                 // doc changed so we manually check dirty in case title was changed | ||||
|                 if ( | ||||
|                   this.store.getValue().title !== | ||||
|                   this.documentForm.get('title').value | ||||
|                 ) { | ||||
|                   this.openDocumentService.setDirty(doc.id, true) | ||||
|                 } | ||||
|               }, | ||||
|             }) | ||||
|  | ||||
|           this.ogDate = new Date(normalizeDateStr(doc.created.toString())) | ||||
|  | ||||
|           // Initialize dirtyCheck | ||||
| @@ -215,6 +242,7 @@ export class DocumentDetailComponent | ||||
|             created: this.ogDate.toISOString(), | ||||
|             correspondent: doc.correspondent, | ||||
|             document_type: doc.document_type, | ||||
|             storage_path: doc.storage_path, | ||||
|             archive_serial_number: doc.archive_serial_number, | ||||
|             tags: [...doc.tags], | ||||
|           }) | ||||
| @@ -233,7 +261,6 @@ export class DocumentDetailComponent | ||||
|           return this.isDirty$.pipe(map((dirty) => ({ doc, dirty }))) | ||||
|         }) | ||||
|       ) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: ({ doc, dirty }) => { | ||||
|           this.openDocumentService.setDirty(doc.id, dirty) | ||||
| @@ -322,6 +349,27 @@ export class DocumentDetailComponent | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   createStoragePath(newName: string) { | ||||
|     var modal = this.modalService.open(StoragePathEditDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|     }) | ||||
|     modal.componentInstance.dialogMode = 'create' | ||||
|     if (newName) modal.componentInstance.object = { name: newName } | ||||
|     modal.componentInstance.success | ||||
|       .pipe( | ||||
|         switchMap((newStoragePath) => { | ||||
|           return this.storagePathService | ||||
|             .listAll() | ||||
|             .pipe(map((storagePaths) => ({ newStoragePath, storagePaths }))) | ||||
|         }) | ||||
|       ) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(({ newStoragePath, documentTypes: storagePaths }) => { | ||||
|         this.storagePaths = storagePaths.results | ||||
|         this.documentForm.get('storage_path').setValue(newStoragePath.id) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   discard() { | ||||
|     this.documentsService | ||||
|       .get(this.documentId) | ||||
| @@ -446,7 +494,7 @@ export class DocumentDetailComponent | ||||
|   } | ||||
|  | ||||
|   moreLike() { | ||||
|     this.documentListViewService.quickFilter([ | ||||
|     this.queryParamsService.navigateWithFilterRules([ | ||||
|       { | ||||
|         rule_type: FILTER_FULLTEXT_MORELIKE, | ||||
|         value: this.documentId.toString(), | ||||
|   | ||||
| @@ -53,6 +53,15 @@ | ||||
|         [(selectionModel)]="documentTypeSelectionModel" | ||||
|         (apply)="setDocumentTypes($event)"> | ||||
|       </app-filterable-dropdown> | ||||
|       <app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title | ||||
|         filterPlaceholder="Filter storage paths" i18n-filterPlaceholder | ||||
|         [items]="storagePaths" | ||||
|         [editing]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (open)="openStoragePathDropdown()" | ||||
|         [(selectionModel)]="storagePathsSelectionModel" | ||||
|         (apply)="setStoragePaths($event)"> | ||||
|       </app-filterable-dropdown> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> | ||||
|   | ||||
| @@ -19,12 +19,12 @@ import { | ||||
| } from '../../common/filterable-dropdown/filterable-dropdown.component' | ||||
| import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' | ||||
| import { MatchingModel } from 'src/app/data/matching-model' | ||||
| import { | ||||
|   SettingsService, | ||||
|   SETTINGS_KEYS, | ||||
| } from 'src/app/services/settings.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { saveAs } from 'file-saver' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-bulk-editor', | ||||
| @@ -35,10 +35,12 @@ export class BulkEditorComponent { | ||||
|   tags: PaperlessTag[] | ||||
|   correspondents: PaperlessCorrespondent[] | ||||
|   documentTypes: PaperlessDocumentType[] | ||||
|   storagePaths: PaperlessStoragePath[] | ||||
|  | ||||
|   tagSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   correspondentSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   documentTypeSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   storagePathsSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   awaitingDownload: boolean | ||||
|  | ||||
|   constructor( | ||||
| @@ -50,7 +52,8 @@ export class BulkEditorComponent { | ||||
|     private modalService: NgbModal, | ||||
|     private openDocumentService: OpenDocumentsService, | ||||
|     private settings: SettingsService, | ||||
|     private toastService: ToastService | ||||
|     private toastService: ToastService, | ||||
|     private storagePathService: StoragePathService | ||||
|   ) {} | ||||
|  | ||||
|   applyOnClose: boolean = this.settings.get( | ||||
| @@ -70,6 +73,9 @@ export class BulkEditorComponent { | ||||
|     this.documentTypeService | ||||
|       .listAll() | ||||
|       .subscribe((result) => (this.documentTypes = result.results)) | ||||
|     this.storagePathService | ||||
|       .listAll() | ||||
|       .subscribe((result) => (this.storagePaths = result.results)) | ||||
|   } | ||||
|  | ||||
|   private executeBulkOperation(modal, method: string, args) { | ||||
| @@ -147,6 +153,17 @@ export class BulkEditorComponent { | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   openStoragePathDropdown() { | ||||
|     this.documentService | ||||
|       .getSelectionData(Array.from(this.list.selected)) | ||||
|       .subscribe((s) => { | ||||
|         this.applySelectionData( | ||||
|           s.selected_storage_paths, | ||||
|           this.storagePathsSelectionModel | ||||
|         ) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   private _localizeList(items: MatchingModel[]) { | ||||
|     if (items.length == 0) { | ||||
|       return '' | ||||
| @@ -301,6 +318,42 @@ export class BulkEditorComponent { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   setStoragePaths(changedDocumentPaths: ChangedItems) { | ||||
|     if ( | ||||
|       changedDocumentPaths.itemsToAdd.length == 0 && | ||||
|       changedDocumentPaths.itemsToRemove.length == 0 | ||||
|     ) | ||||
|       return | ||||
|  | ||||
|     let storagePath = | ||||
|       changedDocumentPaths.itemsToAdd.length > 0 | ||||
|         ? changedDocumentPaths.itemsToAdd[0] | ||||
|         : null | ||||
|  | ||||
|     if (this.showConfirmationDialogs) { | ||||
|       let modal = this.modalService.open(ConfirmDialogComponent, { | ||||
|         backdrop: 'static', | ||||
|       }) | ||||
|       modal.componentInstance.title = $localize`Confirm storage path assignment` | ||||
|       if (storagePath) { | ||||
|         modal.componentInstance.message = $localize`This operation will assign the storage path "${storagePath.name}" to ${this.list.selected.size} selected document(s).` | ||||
|       } else { | ||||
|         modal.componentInstance.message = $localize`This operation will remove the storage path from ${this.list.selected.size} selected document(s).` | ||||
|       } | ||||
|       modal.componentInstance.btnClass = 'btn-warning' | ||||
|       modal.componentInstance.btnCaption = $localize`Confirm` | ||||
|       modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|         this.executeBulkOperation(modal, 'set_storage_path', { | ||||
|           storage_path: storagePath ? storagePath.id : null, | ||||
|         }) | ||||
|       }) | ||||
|     } else { | ||||
|       this.executeBulkOperation(null, 'set_storage_path', { | ||||
|         storage_path: storagePath ? storagePath.id : null, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   applyDelete() { | ||||
|     let modal = this.modalService.open(ConfirmDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|   | ||||
| @@ -33,56 +33,67 @@ | ||||
|         <div class="d-flex flex-column flex-md-row align-items-md-center"> | ||||
|           <div class="btn-group"> | ||||
|             <a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16"> | ||||
|                 <path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/> | ||||
|               </svg> <span class="d-block d-md-inline" i18n>More like this</span> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#diagram-3"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>More like this</span> | ||||
|             </a> | ||||
|             <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|                 <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> | ||||
|               </svg> <span class="d-block d-md-inline" i18n>Edit</span> | ||||
|             <a (click)="openDocumentsService.openDocument(document)" class="btn btn-sm btn-outline-secondary"> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#pencil"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>Edit</span> | ||||
|             </a> | ||||
|             <a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl" | ||||
|             [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" | ||||
|             autoClose="true" popoverClass="shadow" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> | ||||
|                 <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> | ||||
|                 <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> | ||||
|               </svg> <span class="d-block d-md-inline" i18n>View</span> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#eye"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>View</span> | ||||
|             </a> | ||||
|             <ng-template #previewContent> | ||||
|               <object [data]="previewUrl | safeUrl" class="preview" width="100%"></object> | ||||
|             </ng-template> | ||||
|             <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|                 <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> | ||||
|                 <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> | ||||
|               </svg> <span class="d-block d-md-inline" i18n>Download</span> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#download"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>Download</span> | ||||
|             </a> | ||||
|           </div> | ||||
|  | ||||
|           <div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0"> | ||||
|             <button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" | ||||
|             <button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title | ||||
|              (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|               <svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor"> | ||||
|                 <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> | ||||
|               </svg> | ||||
|               <small>{{(document.document_type$ | async)?.name}}</small> | ||||
|             </button> | ||||
|             <button *ngIf="document.storage_path" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title | ||||
|              (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|               <svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor"> | ||||
|                 <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/> | ||||
|               </svg> | ||||
|               <small>{{(document.storage_path$ | async)?.name}}</small> | ||||
|             </button> | ||||
|             <div *ngIf="document.archive_serial_number" class="list-group-item me-2 bg-light text-dark p-1 border-0"> | ||||
|               <svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor"> | ||||
|                 <path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/> | ||||
|               </svg> | ||||
|               <small>#{{document.archive_serial_number}}</small> | ||||
|             </div> | ||||
|             <div class="list-group-item bg-light text-dark p-1 border-0" ngbTooltip="Added: {{document.added | customDate:'shortDate'}} Created: {{document.created | customDate:'shortDate'}}"> | ||||
|             <ng-template #dateTooltip> | ||||
|               <div class="d-flex flex-column"> | ||||
|                 <span i18n>Created: {{ document.created | customDate }}</span> | ||||
|                 <span i18n>Added: {{ document.added | customDate }}</span> | ||||
|                 <span i18n>Modified: {{ document.modified | customDate }}</span> | ||||
|               </div> | ||||
|             </ng-template> | ||||
|             <div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip"> | ||||
|               <svg class="metadata-icon me-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor"> | ||||
|                 <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> | ||||
|                 <path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> | ||||
|               </svg> | ||||
|               <small>{{document.created | customDate:'mediumDate'}}</small> | ||||
|             </div> | ||||
|  | ||||
|             <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> | ||||
|               <small class="text-muted" i18n>Score:</small> | ||||
|               <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|   | ||||
| @@ -6,16 +6,14 @@ import { | ||||
|   Output, | ||||
|   ViewChild, | ||||
| } from '@angular/core' | ||||
| import { DomSanitizer } from '@angular/platform-browser' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| import { | ||||
|   SettingsService, | ||||
|   SETTINGS_KEYS, | ||||
| } from 'src/app/services/settings.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-card-large', | ||||
| @@ -28,8 +26,8 @@ import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | ||||
| export class DocumentCardLargeComponent implements OnInit { | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private sanitizer: DomSanitizer, | ||||
|     private settingsService: SettingsService | ||||
|     private settingsService: SettingsService, | ||||
|     public openDocumentsService: OpenDocumentsService | ||||
|   ) {} | ||||
|  | ||||
|   @Input() | ||||
| @@ -54,6 +52,9 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|   @Output() | ||||
|   clickDocumentType = new EventEmitter<number>() | ||||
|  | ||||
|   @Output() | ||||
|   clickStoragePath = new EventEmitter<number>() | ||||
|  | ||||
|   @Output() | ||||
|   clickMoreLike = new EventEmitter() | ||||
|  | ||||
|   | ||||
| @@ -30,22 +30,28 @@ | ||||
|     </div> | ||||
|     <div class="card-footer pt-0 pb-2 px-2"> | ||||
|       <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info"> | ||||
|         <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by document type" | ||||
|         <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by document type" i18n-title | ||||
|          (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|           <svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor"> | ||||
|             <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> | ||||
|           </svg> | ||||
|           <small>{{(document.document_type$ | async)?.name}}</small> | ||||
|         </button> | ||||
|         <button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by storage path" i18n-title | ||||
|          (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|           <svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor"> | ||||
|             <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/> | ||||
|           </svg> | ||||
|           <small>{{(document.storage_path$ | async)?.name}}</small> | ||||
|         </button> | ||||
|         <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> | ||||
|           <ng-template #dateTooltip> | ||||
|             <div class="d-flex flex-column"> | ||||
|               <span i18n>Created: {{ document.created | customDate}}</span> | ||||
|               <span i18n>Added: {{ document.added | customDate}}</span> | ||||
|               <span i18n>Modified: {{ document.modified | customDate}}</span> | ||||
|               <span i18n>Created: {{ document.created | customDate }}</span> | ||||
|               <span i18n>Added: {{ document.added | customDate }}</span> | ||||
|               <span i18n>Modified: {{ document.modified | customDate }}</span> | ||||
|             </div> | ||||
|           </ng-template> | ||||
|  | ||||
|           <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip"> | ||||
|             <svg class="metadata-icon me-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor"> | ||||
|               <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> | ||||
| @@ -63,7 +69,7 @@ | ||||
|       </div> | ||||
|       <div class="d-flex justify-content-between align-items-center"> | ||||
|         <div class="btn-group w-100"> | ||||
|           <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title> | ||||
|           <a (click)="openDocumentsService.openDocument(document)" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> | ||||
|             </svg> | ||||
| @@ -79,7 +85,7 @@ | ||||
|           <ng-template #previewContent> | ||||
|             <object [data]="previewUrl | safeUrl" class="preview" width="100%"></object> | ||||
|           </ng-template> | ||||
|           <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" (click)="$event.stopPropagation()" i18n-title> | ||||
|           <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> | ||||
|               <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> | ||||
|   | ||||
| @@ -9,11 +9,10 @@ import { | ||||
| import { map } from 'rxjs/operators' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| import { | ||||
|   SettingsService, | ||||
|   SETTINGS_KEYS, | ||||
| } from 'src/app/services/settings.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-card-small', | ||||
| @@ -26,7 +25,8 @@ import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||||
| export class DocumentCardSmallComponent implements OnInit { | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private settingsService: SettingsService | ||||
|     private settingsService: SettingsService, | ||||
|     public openDocumentsService: OpenDocumentsService | ||||
|   ) {} | ||||
|  | ||||
|   @Input() | ||||
| @@ -47,6 +47,9 @@ export class DocumentCardSmallComponent implements OnInit { | ||||
|   @Output() | ||||
|   clickDocumentType = new EventEmitter<number>() | ||||
|  | ||||
|   @Output() | ||||
|   clickStoragePath = new EventEmitter<number>() | ||||
|  | ||||
|   moreTags: number = null | ||||
|  | ||||
|   @ViewChild('popover') popover: NgbPopover | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| <app-page-header [title]="getTitle()"> | ||||
|  | ||||
|   <div ngbDropdown class="me-2 flex-fill d-flex"> | ||||
|     <button class="btn btn-sm btn-outline-primary flex-fill" id="dropdownSelect" ngbDropdownToggle> | ||||
|   <div ngbDropdown class="me-2 d-flex"> | ||||
|     <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#text-indent-left" /> | ||||
|       </svg> <ng-container i18n>Select</ng-container> | ||||
|       </svg> | ||||
|       <div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div> | ||||
|     </button> | ||||
|     <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||
|       <button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button> | ||||
| @@ -38,7 +39,7 @@ | ||||
|   <div ngbDropdown class="btn-group ms-2 flex-fill"> | ||||
|     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button> | ||||
|     <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right"> | ||||
|       <div class="w-100 d-flex btn-group-toggle pb-2 mb-1 border-bottom" ngbRadioGroup [(ngModel)]="list.sortReverse"> | ||||
|       <div class="w-100 d-flex btn-group-toggle pb-2 mb-1 border-bottom" ngbRadioGroup [(ngModel)]="listSort"> | ||||
|         <label ngbButtonLabel class="btn-outline-primary btn-sm mx-2 flex-fill"> | ||||
|           <input ngbButton type="radio" class="btn btn-check btn-sm" [value]="false"> | ||||
|           <svg class="toolbaricon" fill="currentColor"> | ||||
| @@ -53,7 +54,7 @@ | ||||
|         </label> | ||||
|       </div> | ||||
|       <div> | ||||
|         <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field" | ||||
|         <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)" | ||||
|           [class.active]="list.sortField == f.field">{{f.name}} | ||||
|         </button> | ||||
|       </div> | ||||
| @@ -64,7 +65,7 @@ | ||||
|     <button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" ngbDropdownToggle i18n>Views</button> | ||||
|     <div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu> | ||||
|       <ng-container *ngIf="!list.activeSavedViewId"> | ||||
|         <button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button> | ||||
|         <button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button> | ||||
|         <div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div> | ||||
|       </ng-container> | ||||
|  | ||||
| @@ -75,7 +76,7 @@ | ||||
|  | ||||
| </app-page-header> | ||||
|  | ||||
| <div class="row sticky-top pt-4 pb-2 pb-lg-4 bg-body"> | ||||
| <div class="row sticky-top pt-3 pt-sm-4 pb-2 pb-lg-4 bg-body"> | ||||
|   <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" #filterEditor></app-filter-editor> | ||||
|   <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor> | ||||
| </div> | ||||
| @@ -106,7 +107,7 @@ | ||||
| <ng-template #documentListNoError> | ||||
|  | ||||
|   <div *ngIf="displayMode == 'largeCards'"> | ||||
|     <app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|     <app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|     </app-document-card-large> | ||||
|   </div> | ||||
|  | ||||
| @@ -137,6 +138,12 @@ | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Document type</th> | ||||
|       <th class="d-none d-xl-table-cell" | ||||
|         sortable="storage_path__name" | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Storage path</th> | ||||
|       <th | ||||
|         sortable="created" | ||||
|         [currentSortField]="list.sortField" | ||||
| @@ -163,16 +170,21 @@ | ||||
|         </td> | ||||
|         <td class="d-none d-md-table-cell"> | ||||
|           <ng-container *ngIf="d.correspondent"> | ||||
|             <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> | ||||
|             <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> | ||||
|           </ng-container> | ||||
|         </td> | ||||
|         <td> | ||||
|           <a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|           <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id);$event.stopPropagation()"></app-tag> | ||||
|           <a (click)="openDocumentsService.openDocument(d)" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|           <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag> | ||||
|         </td> | ||||
|         <td class="d-none d-xl-table-cell"> | ||||
|           <ng-container *ngIf="d.document_type"> | ||||
|             <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> | ||||
|             <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a> | ||||
|           </ng-container> | ||||
|         </td> | ||||
|         <td class="d-none d-xl-table-cell"> | ||||
|           <ng-container *ngIf="d.storage_path"> | ||||
|             <a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a> | ||||
|           </ng-container> | ||||
|         </td> | ||||
|         <td> | ||||
| @@ -186,7 +198,7 @@ | ||||
|   </table> | ||||
|  | ||||
|   <div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> | ||||
|     <app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small> | ||||
|     <app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small> | ||||
|   </div> | ||||
|   <div *ngIf="list.documents?.length > 15" class="mt-3"> | ||||
|     <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|   | ||||
| @@ -9,20 +9,9 @@ import { | ||||
| } from '@angular/core' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { | ||||
|   filter, | ||||
|   first, | ||||
|   map, | ||||
|   Subject, | ||||
|   Subscription, | ||||
|   switchMap, | ||||
|   takeUntil, | ||||
| } from 'rxjs' | ||||
| import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs' | ||||
| import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule' | ||||
| import { | ||||
|   FILTER_FULLTEXT_MORELIKE, | ||||
|   FILTER_RULE_TYPES, | ||||
| } from 'src/app/data/filter-rule-type' | ||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||
| import { | ||||
| @@ -31,8 +20,9 @@ import { | ||||
| } from 'src/app/directives/sortable.directive' | ||||
| import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { | ||||
|   DocumentService, | ||||
|   DOCUMENT_SORT_FIELDS, | ||||
|   DOCUMENT_SORT_FIELDS_FULLTEXT, | ||||
| } from 'src/app/services/rest/document.service' | ||||
| @@ -49,13 +39,14 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi | ||||
| export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|   constructor( | ||||
|     public list: DocumentListViewService, | ||||
|     private documentService: DocumentService, | ||||
|     public savedViewService: SavedViewService, | ||||
|     public route: ActivatedRoute, | ||||
|     private router: Router, | ||||
|     private toastService: ToastService, | ||||
|     private modalService: NgbModal, | ||||
|     private consumerStatusService: ConsumerStatusService | ||||
|     private consumerStatusService: ConsumerStatusService, | ||||
|     private queryParamsService: QueryParamsService, | ||||
|     public openDocumentsService: OpenDocumentsService | ||||
|   ) {} | ||||
|  | ||||
|   @ViewChild('filterEditor') | ||||
| @@ -83,8 +74,26 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|       : DOCUMENT_SORT_FIELDS | ||||
|   } | ||||
|  | ||||
|   set listSort(reverse: boolean) { | ||||
|     this.list.sortReverse = reverse | ||||
|     this.queryParamsService.sortField = this.list.sortField | ||||
|     this.queryParamsService.sortReverse = reverse | ||||
|   } | ||||
|  | ||||
|   get listSort(): boolean { | ||||
|     return this.list.sortReverse | ||||
|   } | ||||
|  | ||||
|   setSortField(field: string) { | ||||
|     this.list.sortField = field | ||||
|     this.queryParamsService.sortField = field | ||||
|     this.queryParamsService.sortReverse = this.listSort | ||||
|   } | ||||
|  | ||||
|   onSort(event: SortEvent) { | ||||
|     this.list.setSort(event.column, event.reverse) | ||||
|     this.queryParamsService.sortField = event.column | ||||
|     this.queryParamsService.sortReverse = event.reverse | ||||
|   } | ||||
|  | ||||
|   get isBulkEditing(): boolean { | ||||
| @@ -109,60 +118,39 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|  | ||||
|     this.route.paramMap | ||||
|       .pipe( | ||||
|         filter((params) => params.has('id')), // only on saved view | ||||
|         filter((params) => params.has('id')), // only on saved view e.g. /view/id | ||||
|         switchMap((params) => { | ||||
|           return this.savedViewService | ||||
|             .getCached(+params.get('id')) | ||||
|             .pipe(map((view) => ({ params, view }))) | ||||
|             .pipe(map((view) => ({ view }))) | ||||
|         }) | ||||
|       ) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(({ view, params }) => { | ||||
|       .subscribe(({ view }) => { | ||||
|         if (!view) { | ||||
|           this.router.navigate(['404']) | ||||
|           return | ||||
|         } | ||||
|         this.list.activateSavedView(view) | ||||
|         this.list.reload() | ||||
|         this.queryParamsService.updateFromView(view) | ||||
|         this.unmodifiedFilterRules = view.filter_rules | ||||
|       }) | ||||
|  | ||||
|     const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map( | ||||
|       (rt) => rt.filtervar | ||||
|     ) | ||||
|  | ||||
|     this.route.queryParamMap | ||||
|       .pipe( | ||||
|         filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on saved view | ||||
|         filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on /view/id | ||||
|         takeUntil(this.unsubscribeNotifier) | ||||
|       ) | ||||
|       .subscribe((queryParams) => { | ||||
|         // transform query params to filter rules | ||||
|         let filterRulesFromQueryParams: FilterRule[] = [] | ||||
|         allFilterRuleQueryParams | ||||
|           .filter((frqp) => queryParams.has(frqp)) | ||||
|           .forEach((filterQueryParamName) => { | ||||
|             const filterQueryParamValues: string[] = queryParams | ||||
|               .get(filterQueryParamName) | ||||
|               .split(',') | ||||
|  | ||||
|             filterRulesFromQueryParams = filterRulesFromQueryParams.concat( | ||||
|               // map all values to filter rules | ||||
|               filterQueryParamValues.map((val) => { | ||||
|                 return { | ||||
|                   rule_type: FILTER_RULE_TYPES.find( | ||||
|                     (rt) => rt.filtervar == filterQueryParamName | ||||
|                   ).id, | ||||
|                   value: val, | ||||
|                 } | ||||
|               }) | ||||
|             ) | ||||
|           }) | ||||
|  | ||||
|         this.list.activateSavedView(null) | ||||
|         this.list.filterRules = filterRulesFromQueryParams | ||||
|         this.list.reload() | ||||
|         this.unmodifiedFilterRules = [] | ||||
|         if (queryParams.has('view')) { | ||||
|           // loading a saved view on /documents | ||||
|           this.loadViewConfig(parseInt(queryParams.get('view'))) | ||||
|         } else { | ||||
|           this.list.activateSavedView(null) | ||||
|           this.queryParamsService.parseQueryParams(queryParams) | ||||
|           this.unmodifiedFilterRules = [] | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
| @@ -171,17 +159,7 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: (filterRules) => { | ||||
|           const params = | ||||
|             this.documentService.filterRulesToQueryParams(filterRules) | ||||
|  | ||||
|           // if we were on a saved view we navigate 'away' to /documents | ||||
|           let base = [] | ||||
|           if (this.route.snapshot.paramMap.has('id')) base = ['/documents'] | ||||
|  | ||||
|           this.router.navigate(base, { | ||||
|             relativeTo: this.route, | ||||
|             queryParams: params, | ||||
|           }) | ||||
|           this.queryParamsService.updateFilterRules(filterRules) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
| @@ -192,9 +170,15 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|     this.unsubscribeNotifier.complete() | ||||
|   } | ||||
|  | ||||
|   loadViewConfig(view: PaperlessSavedView) { | ||||
|     this.list.loadSavedView(view) | ||||
|     this.list.reload() | ||||
|   loadViewConfig(viewId: number) { | ||||
|     this.savedViewService | ||||
|       .getCached(viewId) | ||||
|       .pipe(first()) | ||||
|       .subscribe((view) => { | ||||
|         this.list.loadSavedView(view) | ||||
|         this.list.reload() | ||||
|         this.queryParamsService.updateFromView(view) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   saveViewConfig() { | ||||
| @@ -281,8 +265,15 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   clickStoragePath(storagePathID: number) { | ||||
|     this.list.selectNone() | ||||
|     setTimeout(() => { | ||||
|       this.filterEditor.addStoragePath(storagePathID) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   clickMoreLike(documentID: number) { | ||||
|     this.list.quickFilter([ | ||||
|     this.queryParamsService.navigateWithFilterRules([ | ||||
|       { rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() }, | ||||
|     ]) | ||||
|   } | ||||
|   | ||||
| @@ -1,57 +1,71 @@ | ||||
| <div class="row"> | ||||
| <div class="row flex-wrap"> | ||||
|    <div class="col mb-2 mb-xl-0"> | ||||
|      <div class="form-inline d-flex align-items-center"> | ||||
|          <div class="input-group input-group-sm flex-fill w-auto"> | ||||
|          <div class="input-group input-group-sm flex-fill w-auto flex-nowrap"> | ||||
|            <div ngbDropdown> | ||||
|             <button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button> | ||||
|             <div class="dropdown-menu shadow" ngbDropdownMenu> | ||||
|               <button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget == t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button> | ||||
|             </div> | ||||
|           </div> | ||||
|           <input #textFilterInput class="form-control form-control-sm" type="text" [(ngModel)]="textFilter" (keyup.enter)="textFilterEnter()" [readonly]="textFilterTarget == 'fulltext-morelike'"> | ||||
|           <select *ngIf="textFilterTarget == 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()"> | ||||
|             <option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option> | ||||
|           </select> | ||||
|           <input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup.enter)="textFilterEnter()" [readonly]="textFilterTarget == 'fulltext-morelike'"> | ||||
|          </div> | ||||
|      </div> | ||||
|   </div> | ||||
|   <div class="w-100 d-xl-none"></div> | ||||
|     <div class="col col-xl-auto mb-2 mb-xl-0"> | ||||
|       <div class="d-flex"> | ||||
|         <app-filterable-dropdown class="me-2 flex-fill" title="Tags" icon="tag-fill" i18n-title | ||||
|           filterPlaceholder="Filter tags" i18n-filterPlaceholder | ||||
|           [items]="tags" | ||||
|           [(selectionModel)]="tagSelectionModel" | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           [multiple]="true" | ||||
|           (open)="onTagsDropdownOpen()" | ||||
|           [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|         <app-filterable-dropdown class="me-2 flex-fill" title="Correspondent" icon="person-fill" i18n-title | ||||
|           filterPlaceholder="Filter correspondents" i18n-filterPlaceholder | ||||
|           [items]="correspondents" | ||||
|           [(selectionModel)]="correspondentSelectionModel" | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           (open)="onCorrespondentDropdownOpen()" | ||||
|           [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|         <app-filterable-dropdown class="me-2 flex-fill" title="Document type" icon="file-earmark-fill" i18n-title | ||||
|           filterPlaceholder="Filter document types" i18n-filterPlaceholder | ||||
|           [items]="documentTypes" | ||||
|           [(selectionModel)]="documentTypeSelectionModel" | ||||
|           (open)="onDocumentTypeDropdownOpen()" | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|         <app-date-dropdown class="me-2" | ||||
|           title="Created" i18n-title | ||||
|           (datesSet)="updateRules()" | ||||
|           [(dateBefore)]="dateCreatedBefore" | ||||
|           [(dateAfter)]="dateCreatedAfter"></app-date-dropdown> | ||||
|         <app-date-dropdown | ||||
|           [(dateBefore)]="dateAddedBefore" | ||||
|           [(dateAfter)]="dateAddedAfter" | ||||
|           title="Added" i18n-title | ||||
|           (datesSet)="updateRules()"></app-date-dropdown> | ||||
|     <div class="col col-xl-auto"> | ||||
|       <div class="d-flex flex-wrap"> | ||||
|         <div class="d-flex flex-wrap mb-2 mb-lg-0"> | ||||
|           <app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title | ||||
|             filterPlaceholder="Filter tags" i18n-filterPlaceholder | ||||
|             [items]="tags" | ||||
|             [(selectionModel)]="tagSelectionModel" | ||||
|             (selectionModelChange)="updateRules()" | ||||
|             [multiple]="true" | ||||
|             (open)="onTagsDropdownOpen()" | ||||
|             [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|           <app-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title | ||||
|             filterPlaceholder="Filter correspondents" i18n-filterPlaceholder | ||||
|             [items]="correspondents" | ||||
|             [(selectionModel)]="correspondentSelectionModel" | ||||
|             (selectionModelChange)="updateRules()" | ||||
|             (open)="onCorrespondentDropdownOpen()" | ||||
|             [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|           <app-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title | ||||
|             filterPlaceholder="Filter document types" i18n-filterPlaceholder | ||||
|             [items]="documentTypes" | ||||
|             [(selectionModel)]="documentTypeSelectionModel" | ||||
|             (open)="onDocumentTypeDropdownOpen()" | ||||
|             (selectionModelChange)="updateRules()" | ||||
|             [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|           <app-filterable-dropdown class="me-2 flex-fill" title="Storage path" icon="folder-fill" i18n-title | ||||
|             filterPlaceholder="Filter storage paths" i18n-filterPlaceholder | ||||
|             [items]="storagePaths" | ||||
|             [(selectionModel)]="storagePathSelectionModel" | ||||
|             (open)="onStoragePathDropdownOpen()" | ||||
|             (selectionModelChange)="updateRules()" | ||||
|             [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|         </div> | ||||
|         <div class="d-flex flex-wrap"> | ||||
|           <app-date-dropdown class="mb-2 mb-xl-0" | ||||
|             title="Created" i18n-title | ||||
|             (datesSet)="updateRules()" | ||||
|             [(dateBefore)]="dateCreatedBefore" | ||||
|             [(dateAfter)]="dateCreatedAfter"></app-date-dropdown> | ||||
|           <app-date-dropdown class="mb-2 mb-xl-0" | ||||
|             [(dateBefore)]="dateAddedBefore" | ||||
|             [(dateAfter)]="dateAddedAfter" | ||||
|             title="Added" i18n-title | ||||
|             (datesSet)="updateRules()"></app-date-dropdown> | ||||
|         </div> | ||||
|      </div> | ||||
|    </div> | ||||
|    <div class="w-100 d-xl-none"></div> | ||||
|    <div class="col col-xl-auto"> | ||||
|      <button class="btn btn-link btn-sm px-0 mx-0 ms-xl-n3" [disabled]="!rulesModified" (click)="resetSelected()"> | ||||
|    <div class="col col-xl-auto ps-0"> | ||||
|      <button class="btn btn-link btn-sm px-0" [disabled]="!rulesModified" (click)="resetSelected()"> | ||||
|        <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|          <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> | ||||
|        </svg><ng-container i18n>Reset filters</ng-container> | ||||
|   | ||||
| @@ -14,6 +14,6 @@ | ||||
|   border-bottom-right-radius: 0; | ||||
| } | ||||
|  | ||||
| .me-2 { | ||||
|   margin-right: 0.7rem !important; // tweak to make room for badges | ||||
| .d-flex.flex-wrap { | ||||
|   column-gap: 0.7rem; | ||||
| } | ||||
|   | ||||
| @@ -33,11 +33,17 @@ import { | ||||
|   FILTER_DOES_NOT_HAVE_TAG, | ||||
|   FILTER_TITLE, | ||||
|   FILTER_TITLE_CONTENT, | ||||
|   FILTER_STORAGE_PATH, | ||||
|   FILTER_ASN_ISNULL, | ||||
|   FILTER_ASN_GT, | ||||
|   FILTER_ASN_LT, | ||||
| } from 'src/app/data/filter-rule-type' | ||||
| import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component' | ||||
| import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
|  | ||||
| const TEXT_FILTER_TARGET_TITLE = 'title' | ||||
| const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content' | ||||
| @@ -45,6 +51,12 @@ const TEXT_FILTER_TARGET_ASN = 'asn' | ||||
| const TEXT_FILTER_TARGET_FULLTEXT_QUERY = 'fulltext-query' | ||||
| const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' | ||||
|  | ||||
| const TEXT_FILTER_MODIFIER_EQUALS = 'equals' | ||||
| const TEXT_FILTER_MODIFIER_NULL = 'is null' | ||||
| const TEXT_FILTER_MODIFIER_NOTNULL = 'not null' | ||||
| const TEXT_FILTER_MODIFIER_GT = 'greater' | ||||
| const TEXT_FILTER_MODIFIER_LT = 'less' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-filter-editor', | ||||
|   templateUrl: './filter-editor.component.html', | ||||
| @@ -98,7 +110,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     private documentTypeService: DocumentTypeService, | ||||
|     private tagService: TagService, | ||||
|     private correspondentService: CorrespondentService, | ||||
|     private documentService: DocumentService | ||||
|     private documentService: DocumentService, | ||||
|     private storagePathService: StoragePathService | ||||
|   ) {} | ||||
|  | ||||
|   @ViewChild('textFilterInput') | ||||
| @@ -107,6 +120,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   tags: PaperlessTag[] = [] | ||||
|   correspondents: PaperlessCorrespondent[] = [] | ||||
|   documentTypes: PaperlessDocumentType[] = [] | ||||
|   storagePaths: PaperlessStoragePath[] = [] | ||||
|  | ||||
|   _textFilter = '' | ||||
|   _moreLikeId: number | ||||
| @@ -141,9 +155,43 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|       ?.name | ||||
|   } | ||||
|  | ||||
|   public textFilterModifier: string | ||||
|  | ||||
|   get textFilterModifiers() { | ||||
|     return [ | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_EQUALS, | ||||
|         label: $localize`equals`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_NULL, | ||||
|         label: $localize`is empty`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_NOTNULL, | ||||
|         label: $localize`is not empty`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_GT, | ||||
|         label: $localize`greater than`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_LT, | ||||
|         label: $localize`less than`, | ||||
|       }, | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   get textFilterModifierIsNull(): boolean { | ||||
|     return [TEXT_FILTER_MODIFIER_NULL, TEXT_FILTER_MODIFIER_NOTNULL].includes( | ||||
|       this.textFilterModifier | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   tagSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   correspondentSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   documentTypeSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   storagePathSelectionModel = new FilterableDropdownSelectionModel() | ||||
|  | ||||
|   dateCreatedBefore: string | ||||
|   dateCreatedAfter: string | ||||
| @@ -168,6 +216,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this._filterRules = value | ||||
|  | ||||
|     this.documentTypeSelectionModel.clear(false) | ||||
|     this.storagePathSelectionModel.clear(false) | ||||
|     this.tagSelectionModel.clear(false) | ||||
|     this.correspondentSelectionModel.clear(false) | ||||
|     this._textFilter = null | ||||
| @@ -176,6 +225,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this.dateAddedAfter = null | ||||
|     this.dateCreatedBefore = null | ||||
|     this.dateCreatedAfter = null | ||||
|     this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS | ||||
|  | ||||
|     value.forEach((rule) => { | ||||
|       switch (rule.rule_type) { | ||||
| @@ -254,6 +304,27 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_STORAGE_PATH: | ||||
|           this.storagePathSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Selected, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_ASN_ISNULL: | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||
|           this.textFilterModifier = TEXT_FILTER_MODIFIER_NULL | ||||
|           break | ||||
|         case FILTER_ASN_GT: | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||
|           this.textFilterModifier = TEXT_FILTER_MODIFIER_GT | ||||
|           this._textFilter = rule.value | ||||
|           break | ||||
|         case FILTER_ASN_LT: | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||
|           this.textFilterModifier = TEXT_FILTER_MODIFIER_LT | ||||
|           this._textFilter = rule.value | ||||
|           break | ||||
|       } | ||||
|     }) | ||||
|     this.checkIfRulesHaveChanged() | ||||
| @@ -273,8 +344,33 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_TITLE) { | ||||
|       filterRules.push({ rule_type: FILTER_TITLE, value: this._textFilter }) | ||||
|     } | ||||
|     if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_ASN) { | ||||
|       filterRules.push({ rule_type: FILTER_ASN, value: this._textFilter }) | ||||
|     if (this.textFilterTarget == TEXT_FILTER_TARGET_ASN) { | ||||
|       if ( | ||||
|         this.textFilterModifier == TEXT_FILTER_MODIFIER_EQUALS && | ||||
|         this._textFilter | ||||
|       ) { | ||||
|         filterRules.push({ rule_type: FILTER_ASN, value: this._textFilter }) | ||||
|       } else if (this.textFilterModifierIsNull) { | ||||
|         filterRules.push({ | ||||
|           rule_type: FILTER_ASN_ISNULL, | ||||
|           value: ( | ||||
|             this.textFilterModifier == TEXT_FILTER_MODIFIER_NULL | ||||
|           ).toString(), | ||||
|         }) | ||||
|       } else if ( | ||||
|         [TEXT_FILTER_MODIFIER_GT, TEXT_FILTER_MODIFIER_LT].includes( | ||||
|           this.textFilterModifier | ||||
|         ) && | ||||
|         this._textFilter | ||||
|       ) { | ||||
|         filterRules.push({ | ||||
|           rule_type: | ||||
|             this.textFilterModifier == TEXT_FILTER_MODIFIER_GT | ||||
|               ? FILTER_ASN_GT | ||||
|               : FILTER_ASN_LT, | ||||
|           value: this._textFilter, | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|     if ( | ||||
|       this._textFilter && | ||||
| @@ -336,6 +432,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           value: documentType.id?.toString(), | ||||
|         }) | ||||
|       }) | ||||
|     this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_STORAGE_PATH, | ||||
|         value: storagePath.id?.toString(), | ||||
|       }) | ||||
|     }) | ||||
|     if (this.dateCreatedBefore) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_CREATED_BEFORE, | ||||
| @@ -398,7 +500,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
|  | ||||
|   get textFilter() { | ||||
|     return this._textFilter | ||||
|     return this.textFilterModifierIsNull ? '' : this._textFilter | ||||
|   } | ||||
|  | ||||
|   set textFilter(value) { | ||||
| @@ -418,6 +520,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this.documentTypeService | ||||
|       .listAll() | ||||
|       .subscribe((result) => (this.documentTypes = result.results)) | ||||
|     this.storagePathService | ||||
|       .listAll() | ||||
|       .subscribe((result) => (this.storagePaths = result.results)) | ||||
|  | ||||
|     this.textFilterDebounce = new Subject<string>() | ||||
|  | ||||
| @@ -460,6 +565,13 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   addStoragePath(storagePathID: number) { | ||||
|     this.storagePathSelectionModel.set( | ||||
|       storagePathID, | ||||
|       ToggleableItemState.Selected | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   onTagsDropdownOpen() { | ||||
|     this.tagSelectionModel.apply() | ||||
|   } | ||||
| @@ -472,6 +584,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this.documentTypeSelectionModel.apply() | ||||
|   } | ||||
|  | ||||
|   onStoragePathDropdownOpen() { | ||||
|     this.storagePathSelectionModel.apply() | ||||
|   } | ||||
|  | ||||
|   updateTextFilter(text) { | ||||
|     this._textFilter = text | ||||
|     this.documentService.searchQuery = text | ||||
| @@ -498,4 +614,18 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this.textFilterInput.nativeElement.focus() | ||||
|     this.updateRules() | ||||
|   } | ||||
|  | ||||
|   textFilterModifierChange() { | ||||
|     if ( | ||||
|       this.textFilterModifierIsNull || | ||||
|       ([ | ||||
|         TEXT_FILTER_MODIFIER_EQUALS, | ||||
|         TEXT_FILTER_MODIFIER_GT, | ||||
|         TEXT_FILTER_MODIFIER_LT, | ||||
|       ].includes(this.textFilterModifier) && | ||||
|         this._textFilter) | ||||
|     ) { | ||||
|       this.updateRules() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' | ||||
| @@ -20,7 +20,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles | ||||
|     correspondentsService: CorrespondentService, | ||||
|     modalService: NgbModal, | ||||
|     toastService: ToastService, | ||||
|     list: DocumentListViewService, | ||||
|     queryParamsService: QueryParamsService, | ||||
|     private datePipe: CustomDatePipe | ||||
|   ) { | ||||
|     super( | ||||
| @@ -28,13 +28,14 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles | ||||
|       modalService, | ||||
|       CorrespondentEditDialogComponent, | ||||
|       toastService, | ||||
|       list, | ||||
|       queryParamsService, | ||||
|       FILTER_CORRESPONDENT, | ||||
|       $localize`correspondent`, | ||||
|       $localize`correspondents`, | ||||
|       [ | ||||
|         { | ||||
|           key: 'last_correspondence', | ||||
|           name: $localize`Last correspondence`, | ||||
|           name: $localize`Last used`, | ||||
|           valueFn: (c: PaperlessCorrespondent) => { | ||||
|             return this.datePipe.transform(c.last_correspondence) | ||||
|           }, | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { Component } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' | ||||
| @@ -18,16 +18,17 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless | ||||
|     documentTypeService: DocumentTypeService, | ||||
|     modalService: NgbModal, | ||||
|     toastService: ToastService, | ||||
|     list: DocumentListViewService | ||||
|     queryParamsService: QueryParamsService | ||||
|   ) { | ||||
|     super( | ||||
|       documentTypeService, | ||||
|       modalService, | ||||
|       DocumentTypeEditDialogComponent, | ||||
|       toastService, | ||||
|       list, | ||||
|       queryParamsService, | ||||
|       FILTER_DOCUMENT_TYPE, | ||||
|       $localize`document type`, | ||||
|       $localize`document types`, | ||||
|       [] | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <app-page-header title="{{ typeName | titlecase }}s"> | ||||
| <app-page-header title="{{ typeNamePlural | titlecase }}"> | ||||
|   <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button> | ||||
| </app-page-header> | ||||
|  | ||||
| @@ -17,7 +17,7 @@ | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> | ||||
|       <th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th> | ||||
|       <th scope="col" class="d-none d-sm-table-cell" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th> | ||||
|       <th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th> | ||||
|       <th scope="col" *ngFor="let column of extraColumns" sortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th> | ||||
|       <th scope="col" i18n>Actions</th> | ||||
| @@ -26,14 +26,28 @@ | ||||
|   <tbody> | ||||
|     <tr *ngFor="let object of data"> | ||||
|       <td scope="row">{{ object.name }}</td> | ||||
|       <td scope="row">{{ getMatching(object) }}</td> | ||||
|       <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> | ||||
|       <td scope="row">{{ object.document_count }}</td> | ||||
|       <td scope="row" *ngFor="let column of extraColumns"> | ||||
|         <div *ngIf="column.rendersHtml; else colValue" [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div> | ||||
|         <ng-template #colValue>{{ column.valueFn.call(null, object) }}</ng-template> | ||||
|       </td> | ||||
|       <td scope="row"> | ||||
|         <div class="btn-group"> | ||||
|         <div class="btn-group d-block d-sm-none"> | ||||
|           <div ngbDropdown class="d-inline-block"> | ||||
|             <button type="button" class="btn btn-link" id="actionsMenuMobile" ngbDropdownToggle> | ||||
|               <svg class="toolbaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#three-dots-vertical" /> | ||||
|               </svg> | ||||
|             </button> | ||||
|             <div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> | ||||
|               <button (click)="filterDocuments(object)" ngbDropdownItem i18n>Filter Documents</button> | ||||
|               <button (click)="openEditDialog(object)" ngbDropdownItem i18n>Edit</button> | ||||
|               <button class="text-danger" (click)="openDeleteDialog(object)" ngbDropdownItem i18n>Delete</button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="btn-group d-none d-sm-block"> | ||||
|           <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)"> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16"> | ||||
|               <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/> | ||||
| @@ -57,6 +71,6 @@ | ||||
| </table> | ||||
|  | ||||
| <div class="d-flex"> | ||||
|   <div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeName}}s}}</div> | ||||
|   <div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div> | ||||
|   <ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination> | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,4 @@ | ||||
| // hide caret on mobile dropdown | ||||
| .d-block.d-sm-none .dropdown-toggle::after { | ||||
|     display: none; | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import { | ||||
|   SortableDirective, | ||||
|   SortEvent, | ||||
| } from 'src/app/directives/sortable.directive' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||
| @@ -42,9 +42,10 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|     private modalService: NgbModal, | ||||
|     private editDialogComponent: any, | ||||
|     private toastService: ToastService, | ||||
|     private list: DocumentListViewService, | ||||
|     private queryParamsService: QueryParamsService, | ||||
|     protected filterRuleType: number, | ||||
|     public typeName: string, | ||||
|     public typeNamePlural: string, | ||||
|     public extraColumns: ManagementListColumn[] | ||||
|   ) {} | ||||
|  | ||||
| @@ -140,7 +141,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|   } | ||||
|  | ||||
|   filterDocuments(object: ObjectWithId) { | ||||
|     this.list.quickFilter([ | ||||
|     this.queryParamsService.navigateWithFilterRules([ | ||||
|       { rule_type: this.filterRuleType, value: object.id.toString() }, | ||||
|     ]) | ||||
|   } | ||||
|   | ||||
| @@ -13,11 +13,11 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service' | ||||
| import { | ||||
|   LanguageOption, | ||||
|   SettingsService, | ||||
|   SETTINGS_KEYS, | ||||
| } from 'src/app/services/settings.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' | ||||
| import { Observable, Subscription, BehaviorSubject } from 'rxjs' | ||||
| import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-settings', | ||||
| @@ -227,10 +227,23 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|       this.settingsForm.value.notificationsConsumerSuppressOnDashboard | ||||
|     ) | ||||
|     this.settings.setLanguage(this.settingsForm.value.displayLanguage) | ||||
|     this.store.next(this.settingsForm.value) | ||||
|     this.documentListViewService.updatePageSize() | ||||
|     this.settings.updateAppearanceSettings() | ||||
|     this.toastService.showInfo($localize`Settings saved successfully.`) | ||||
|     this.settings | ||||
|       .storeSettings() | ||||
|       .pipe(first()) | ||||
|       .subscribe({ | ||||
|         next: () => { | ||||
|           this.store.next(this.settingsForm.value) | ||||
|           this.documentListViewService.updatePageSize() | ||||
|           this.settings.updateAppearanceSettings() | ||||
|           this.toastService.showInfo($localize`Settings saved successfully.`) | ||||
|         }, | ||||
|         error: (error) => { | ||||
|           this.toastService.showError( | ||||
|             $localize`An error occurred while saving settings.` | ||||
|           ) | ||||
|           console.log(error) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   get displayLanguageOptions(): LanguageOption[] { | ||||
|   | ||||
| @@ -0,0 +1,48 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' | ||||
| import { ManagementListComponent } from '../management-list/management-list.component' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-storage-path-list', | ||||
|   templateUrl: './../management-list/management-list.component.html', | ||||
|   styleUrls: ['./../management-list/management-list.component.scss'], | ||||
| }) | ||||
| export class StoragePathListComponent extends ManagementListComponent<PaperlessStoragePath> { | ||||
|   constructor( | ||||
|     directoryService: StoragePathService, | ||||
|     modalService: NgbModal, | ||||
|     toastService: ToastService, | ||||
|     queryParamsService: QueryParamsService | ||||
|   ) { | ||||
|     super( | ||||
|       directoryService, | ||||
|       modalService, | ||||
|       StoragePathEditDialogComponent, | ||||
|       toastService, | ||||
|       queryParamsService, | ||||
|       FILTER_STORAGE_PATH, | ||||
|       $localize`storage path`, | ||||
|       $localize`storage paths`, | ||||
|       [ | ||||
|         { | ||||
|           key: 'path', | ||||
|           name: $localize`Path`, | ||||
|           valueFn: (c: PaperlessStoragePath) => { | ||||
|             return c.path | ||||
|           }, | ||||
|         }, | ||||
|       ] | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   getDeleteMessage(object: PaperlessStoragePath) { | ||||
|     return $localize`Do you really want to delete the storage path "${object.name}"?` | ||||
|   } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ import { Component } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | ||||
| import { TagService } from 'src/app/services/rest/tag.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' | ||||
| @@ -18,16 +18,17 @@ export class TagListComponent extends ManagementListComponent<PaperlessTag> { | ||||
|     tagService: TagService, | ||||
|     modalService: NgbModal, | ||||
|     toastService: ToastService, | ||||
|     list: DocumentListViewService | ||||
|     queryParamsService: QueryParamsService | ||||
|   ) { | ||||
|     super( | ||||
|       tagService, | ||||
|       modalService, | ||||
|       TagEditDialogComponent, | ||||
|       toastService, | ||||
|       list, | ||||
|       queryParamsService, | ||||
|       FILTER_HAS_TAGS_ALL, | ||||
|       $localize`tag`, | ||||
|       $localize`tags`, | ||||
|       [ | ||||
|         { | ||||
|           key: 'color', | ||||
|   | ||||
| @@ -20,11 +20,15 @@ export const FILTER_MODIFIED_AFTER = 16 | ||||
| export const FILTER_DOES_NOT_HAVE_TAG = 17 | ||||
|  | ||||
| export const FILTER_ASN_ISNULL = 18 | ||||
| export const FILTER_ASN_GT = 19 | ||||
| export const FILTER_ASN_LT = 20 | ||||
|  | ||||
| export const FILTER_TITLE_CONTENT = 19 | ||||
| export const FILTER_TITLE_CONTENT = 21 | ||||
|  | ||||
| export const FILTER_FULLTEXT_QUERY = 20 | ||||
| export const FILTER_FULLTEXT_MORELIKE = 21 | ||||
| export const FILTER_FULLTEXT_QUERY = 22 | ||||
| export const FILTER_FULLTEXT_MORELIKE = 23 | ||||
|  | ||||
| export const FILTER_STORAGE_PATH = 30 | ||||
|  | ||||
| export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|   { | ||||
| @@ -41,14 +45,12 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     multi: false, | ||||
|     default: '', | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_ASN, | ||||
|     filtervar: 'archive_serial_number', | ||||
|     datatype: 'number', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_CORRESPONDENT, | ||||
|     filtervar: 'correspondent__id', | ||||
| @@ -56,6 +58,13 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'correspondent', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_STORAGE_PATH, | ||||
|     filtervar: 'storage_path__id', | ||||
|     isnull_filtervar: 'storage_path__isnull', | ||||
|     datatype: 'storage_path', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_DOCUMENT_TYPE, | ||||
|     filtervar: 'document_type__id', | ||||
| @@ -63,7 +72,6 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'document_type', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_IS_IN_INBOX, | ||||
|     filtervar: 'is_in_inbox', | ||||
| @@ -96,7 +104,6 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     multi: false, | ||||
|     default: true, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_CREATED_BEFORE, | ||||
|     filtervar: 'created__date__lt', | ||||
| @@ -109,7 +116,6 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'date', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_CREATED_YEAR, | ||||
|     filtervar: 'created__year', | ||||
| @@ -141,7 +147,6 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'date', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_MODIFIED_BEFORE, | ||||
|     filtervar: 'modified__date__lt', | ||||
| @@ -160,21 +165,30 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'boolean', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_ASN_GT, | ||||
|     filtervar: 'archive_serial_number__gt', | ||||
|     datatype: 'number', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_ASN_LT, | ||||
|     filtervar: 'archive_serial_number__lt', | ||||
|     datatype: 'number', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_TITLE_CONTENT, | ||||
|     filtervar: 'title_content', | ||||
|     datatype: 'string', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_FULLTEXT_QUERY, | ||||
|     filtervar: 'query', | ||||
|     datatype: 'string', | ||||
|     multi: false, | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     id: FILTER_FULLTEXT_MORELIKE, | ||||
|     filtervar: 'more_like_id', | ||||
|   | ||||
| @@ -4,4 +4,6 @@ export interface PaperlessDocumentSuggestions { | ||||
|   correspondents?: number[] | ||||
|  | ||||
|   document_types?: number[] | ||||
|  | ||||
|   storage_paths?: number[] | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { ObjectWithId } from './object-with-id' | ||||
| import { PaperlessTag } from './paperless-tag' | ||||
| import { PaperlessDocumentType } from './paperless-document-type' | ||||
| import { Observable } from 'rxjs' | ||||
| import { PaperlessStoragePath } from './paperless-storage-path' | ||||
|  | ||||
| export interface SearchHit { | ||||
|   score?: number | ||||
| @@ -20,6 +21,10 @@ export interface PaperlessDocument extends ObjectWithId { | ||||
|  | ||||
|   document_type?: number | ||||
|  | ||||
|   storage_path$?: Observable<PaperlessStoragePath> | ||||
|  | ||||
|   storage_path?: number | ||||
|  | ||||
|   title?: string | ||||
|  | ||||
|   content?: string | ||||
|   | ||||
							
								
								
									
										5
									
								
								src-ui/src/app/data/paperless-storage-path.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src-ui/src/app/data/paperless-storage-path.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import { MatchingModel } from './matching-model' | ||||
|  | ||||
| export interface PaperlessStoragePath extends MatchingModel { | ||||
|   path?: string | ||||
| } | ||||
							
								
								
									
										117
									
								
								src-ui/src/app/data/paperless-uisettings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src-ui/src/app/data/paperless-uisettings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| export interface PaperlessUiSettings { | ||||
|   user_id: number | ||||
|  | ||||
|   username: string | ||||
|  | ||||
|   display_name: string | ||||
|  | ||||
|   settings: Object | ||||
| } | ||||
|  | ||||
| export interface PaperlessUiSetting { | ||||
|   key: string | ||||
|   type: string | ||||
|   default: any | ||||
| } | ||||
|  | ||||
| export const SETTINGS_KEYS = { | ||||
|   LANGUAGE: 'language', | ||||
|   // maintain old general-settings: for backwards compatibility | ||||
|   BULK_EDIT_CONFIRMATION_DIALOGS: | ||||
|     'general-settings:bulk-edit:confirmation-dialogs', | ||||
|   BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', | ||||
|   DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', | ||||
|   DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system', | ||||
|   DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled', | ||||
|   DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted', | ||||
|   THEME_COLOR: 'general-settings:theme:color', | ||||
|   USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer', | ||||
|   DATE_LOCALE: 'general-settings:date-display:date-locale', | ||||
|   DATE_FORMAT: 'general-settings:date-display:date-format', | ||||
|   NOTIFICATIONS_CONSUMER_NEW_DOCUMENT: | ||||
|     'general-settings:notifications:consumer-new-documents', | ||||
|   NOTIFICATIONS_CONSUMER_SUCCESS: | ||||
|     'general-settings:notifications:consumer-success', | ||||
|   NOTIFICATIONS_CONSUMER_FAILED: | ||||
|     'general-settings:notifications:consumer-failed', | ||||
|   NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: | ||||
|     'general-settings:notifications:consumer-suppress-on-dashboard', | ||||
| } | ||||
|  | ||||
| export const SETTINGS: PaperlessUiSetting[] = [ | ||||
|   { | ||||
|     key: SETTINGS_KEYS.LANGUAGE, | ||||
|     type: 'string', | ||||
|     default: '', | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, | ||||
|     type: 'boolean', | ||||
|     default: false, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, | ||||
|     type: 'number', | ||||
|     default: 50, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DARK_MODE_ENABLED, | ||||
|     type: 'boolean', | ||||
|     default: false, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.THEME_COLOR, | ||||
|     type: 'string', | ||||
|     default: '', | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, | ||||
|     type: 'boolean', | ||||
|     default: false, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DATE_LOCALE, | ||||
|     type: 'string', | ||||
|     default: '', | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DATE_FORMAT, | ||||
|     type: 'string', | ||||
|     default: 'mediumDate', | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
| ] | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { DatePipe } from '@angular/common' | ||||
| import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core' | ||||
| import { SettingsService, SETTINGS_KEYS } from '../services/settings.service' | ||||
| import { SETTINGS_KEYS } from '../data/paperless-uisettings' | ||||
| import { SettingsService } from '../services/settings.service' | ||||
| import { normalizeDateStr } from '../utils/date' | ||||
|  | ||||
| const FORMAT_TO_ISO_FORMAT = { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { Injectable } from '@angular/core' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { ActivatedRoute, Params, Router } from '@angular/router' | ||||
| import { Observable } from 'rxjs' | ||||
| import { | ||||
|   cloneFilterRules, | ||||
| @@ -8,9 +8,10 @@ import { | ||||
| } from '../data/filter-rule' | ||||
| import { PaperlessDocument } from '../data/paperless-document' | ||||
| import { PaperlessSavedView } from '../data/paperless-saved-view' | ||||
| import { SETTINGS_KEYS } from '../data/paperless-uisettings' | ||||
| import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys' | ||||
| import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service' | ||||
| import { SettingsService, SETTINGS_KEYS } from './settings.service' | ||||
| import { SettingsService } from './settings.service' | ||||
|  | ||||
| /** | ||||
|  * Captures the current state of the list view. | ||||
| @@ -220,6 +221,13 @@ export class DocumentListViewService { | ||||
|     return this.activeListViewState.sortReverse | ||||
|   } | ||||
|  | ||||
|   get sortParams(): Params { | ||||
|     return { | ||||
|       sortField: this.sortField, | ||||
|       sortReverse: this.sortReverse, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get collectionSize(): number { | ||||
|     return this.activeListViewState.collectionSize | ||||
|   } | ||||
| @@ -265,14 +273,6 @@ export class DocumentListViewService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   quickFilter(filterRules: FilterRule[]) { | ||||
|     const params = this.documentService.filterRulesToQueryParams(filterRules) | ||||
|     this.router.navigate(['/documents'], { | ||||
|       relativeTo: this.route, | ||||
|       queryParams: params, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   getLastPage(): number { | ||||
|     return Math.ceil(this.collectionSize / this.currentPageSize) | ||||
|   } | ||||
| @@ -434,9 +434,7 @@ export class DocumentListViewService { | ||||
|  | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private settings: SettingsService, | ||||
|     private router: Router, | ||||
|     private route: ActivatedRoute | ||||
|     private settings: SettingsService | ||||
|   ) { | ||||
|     let documentListViewConfigJson = localStorage.getItem( | ||||
|       DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' | ||||
| import { Observable, Subject, of } from 'rxjs' | ||||
| import { first } from 'rxjs/operators' | ||||
| import { Router } from '@angular/router' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| @@ -15,7 +16,8 @@ export class OpenDocumentsService { | ||||
|  | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private modalService: NgbModal | ||||
|     private modalService: NgbModal, | ||||
|     private router: Router | ||||
|   ) { | ||||
|     if (sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)) { | ||||
|       try { | ||||
| @@ -55,13 +57,38 @@ export class OpenDocumentsService { | ||||
|     return this.openDocuments.find((d) => d.id == id) | ||||
|   } | ||||
|  | ||||
|   openDocument(doc: PaperlessDocument) { | ||||
|   openDocument( | ||||
|     doc: PaperlessDocument, | ||||
|     navigate: boolean = true | ||||
|   ): Observable<boolean> { | ||||
|     if (this.openDocuments.find((d) => d.id == doc.id) == null) { | ||||
|       this.openDocuments.unshift(doc) | ||||
|       if (this.openDocuments.length > this.MAX_OPEN_DOCUMENTS) { | ||||
|         this.openDocuments.pop() | ||||
|       if (this.openDocuments.length == this.MAX_OPEN_DOCUMENTS) { | ||||
|         // at max, ensure changes arent lost | ||||
|         const docToRemove = this.openDocuments[this.MAX_OPEN_DOCUMENTS - 1] | ||||
|         const closeObservable = this.closeDocument(docToRemove) | ||||
|         closeObservable.pipe(first()).subscribe((closed) => { | ||||
|           if (closed) this.finishOpenDocument(doc, navigate) | ||||
|         }) | ||||
|         return closeObservable | ||||
|       } else { | ||||
|         // not at max | ||||
|         this.finishOpenDocument(doc, navigate) | ||||
|       } | ||||
|       this.save() | ||||
|     } else { | ||||
|       // doc is open, just maybe navigate | ||||
|       if (navigate) { | ||||
|         this.router.navigate(['documents', doc.id]) | ||||
|       } | ||||
|     } | ||||
|     return of(true) | ||||
|   } | ||||
|  | ||||
|   private finishOpenDocument(doc: PaperlessDocument, navigate: boolean) { | ||||
|     this.openDocuments.unshift(doc) | ||||
|     this.dirtyDocuments.delete(doc.id) | ||||
|     this.save() | ||||
|     if (navigate) { | ||||
|       this.router.navigate(['documents', doc.id]) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -82,7 +109,11 @@ export class OpenDocumentsService { | ||||
|         backdrop: 'static', | ||||
|       }) | ||||
|       modal.componentInstance.title = $localize`Unsaved Changes` | ||||
|       modal.componentInstance.messageBold = $localize`You have unsaved changes.` | ||||
|       modal.componentInstance.messageBold = | ||||
|         $localize`You have unsaved changes to the document` + | ||||
|         ' "' + | ||||
|         doc.title + | ||||
|         '".' | ||||
|       modal.componentInstance.message = $localize`Are you sure you want to close this document?` | ||||
|       modal.componentInstance.btnClass = 'btn-warning' | ||||
|       modal.componentInstance.btnCaption = $localize`Close document` | ||||
|   | ||||
							
								
								
									
										163
									
								
								src-ui/src/app/services/query-params.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src-ui/src/app/services/query-params.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| import { Injectable } from '@angular/core' | ||||
| import { ParamMap, Params, Router } from '@angular/router' | ||||
| import { FilterRule } from '../data/filter-rule' | ||||
| import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type' | ||||
| import { PaperlessSavedView } from '../data/paperless-saved-view' | ||||
| import { DocumentListViewService } from './document-list-view.service' | ||||
|  | ||||
| const SORT_FIELD_PARAMETER = 'sort' | ||||
| const SORT_REVERSE_PARAMETER = 'reverse' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class QueryParamsService { | ||||
|   constructor(private router: Router, private list: DocumentListViewService) {} | ||||
|  | ||||
|   private filterParams: Params = {} | ||||
|   private sortParams: Params = {} | ||||
|  | ||||
|   updateFilterRules( | ||||
|     filterRules: FilterRule[], | ||||
|     updateQueryParams: boolean = true | ||||
|   ) { | ||||
|     this.filterParams = filterRulesToQueryParams(filterRules) | ||||
|     if (updateQueryParams) this.updateQueryParams() | ||||
|   } | ||||
|  | ||||
|   set sortField(field: string) { | ||||
|     this.sortParams[SORT_FIELD_PARAMETER] = field | ||||
|     this.updateQueryParams() | ||||
|   } | ||||
|  | ||||
|   set sortReverse(reverse: boolean) { | ||||
|     if (!reverse) this.sortParams[SORT_REVERSE_PARAMETER] = undefined | ||||
|     else this.sortParams[SORT_REVERSE_PARAMETER] = reverse | ||||
|     this.updateQueryParams() | ||||
|   } | ||||
|  | ||||
|   get params(): Params { | ||||
|     return { | ||||
|       ...this.sortParams, | ||||
|       ...this.filterParams, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private updateQueryParams() { | ||||
|     // if we were on a saved view we navigate 'away' to /documents | ||||
|     let base = [] | ||||
|     if (this.router.routerState.snapshot.url.includes('/view/')) | ||||
|       base = ['/documents'] | ||||
|  | ||||
|     this.router.navigate(base, { | ||||
|       queryParams: this.params, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   public parseQueryParams(queryParams: ParamMap) { | ||||
|     let filterRules = filterRulesFromQueryParams(queryParams) | ||||
|     if ( | ||||
|       filterRules.length || | ||||
|       queryParams.has(SORT_FIELD_PARAMETER) || | ||||
|       queryParams.has(SORT_REVERSE_PARAMETER) | ||||
|     ) { | ||||
|       this.list.filterRules = filterRules | ||||
|       this.list.sortField = queryParams.get(SORT_FIELD_PARAMETER) | ||||
|       this.list.sortReverse = | ||||
|         queryParams.has(SORT_REVERSE_PARAMETER) || | ||||
|         (!queryParams.has(SORT_FIELD_PARAMETER) && | ||||
|           !queryParams.has(SORT_REVERSE_PARAMETER)) | ||||
|       this.list.reload() | ||||
|     } else if ( | ||||
|       filterRules.length == 0 && | ||||
|       !queryParams.has(SORT_FIELD_PARAMETER) | ||||
|     ) { | ||||
|       // this is navigating to /documents so we need to update the params from the list | ||||
|       this.updateFilterRules(this.list.filterRules, false) | ||||
|       this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField | ||||
|       this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse | ||||
|       this.router.navigate([], { | ||||
|         queryParams: this.params, | ||||
|         replaceUrl: true, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   updateFromView(view: PaperlessSavedView) { | ||||
|     if (!this.router.routerState.snapshot.url.includes('/view/')) { | ||||
|       // navigation for /documents?view= | ||||
|       this.router.navigate([], { | ||||
|         queryParams: { view: view.id }, | ||||
|       }) | ||||
|     } | ||||
|     // make sure params are up-to-date | ||||
|     this.updateFilterRules(view.filter_rules, false) | ||||
|     this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField | ||||
|     this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse | ||||
|   } | ||||
|  | ||||
|   navigateWithFilterRules(filterRules: FilterRule[]) { | ||||
|     this.updateFilterRules(filterRules) | ||||
|     this.router.navigate(['/documents'], { | ||||
|       queryParams: this.params, | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function filterRulesToQueryParams(filterRules: FilterRule[]): Object { | ||||
|   if (filterRules) { | ||||
|     let params = {} | ||||
|     for (let rule of filterRules) { | ||||
|       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) | ||||
|       if (ruleType.multi) { | ||||
|         params[ruleType.filtervar] = params[ruleType.filtervar] | ||||
|           ? params[ruleType.filtervar] + ',' + rule.value | ||||
|           : rule.value | ||||
|       } else if (ruleType.isnull_filtervar && rule.value == null) { | ||||
|         params[ruleType.isnull_filtervar] = true | ||||
|       } else { | ||||
|         params[ruleType.filtervar] = rule.value | ||||
|       } | ||||
|     } | ||||
|     return params | ||||
|   } else { | ||||
|     return null | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function filterRulesFromQueryParams(queryParams: ParamMap) { | ||||
|   const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map( | ||||
|     (rt) => rt.filtervar | ||||
|   ) | ||||
|     .concat(FILTER_RULE_TYPES.map((rt) => rt.isnull_filtervar)) | ||||
|     .filter((rt) => rt !== undefined) | ||||
|  | ||||
|   // transform query params to filter rules | ||||
|   let filterRulesFromQueryParams: FilterRule[] = [] | ||||
|   allFilterRuleQueryParams | ||||
|     .filter((frqp) => queryParams.has(frqp)) | ||||
|     .forEach((filterQueryParamName) => { | ||||
|       const rule_type: FilterRuleType = FILTER_RULE_TYPES.find( | ||||
|         (rt) => | ||||
|           rt.filtervar == filterQueryParamName || | ||||
|           rt.isnull_filtervar == filterQueryParamName | ||||
|       ) | ||||
|       const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName | ||||
|       const valueURIComponent: string = queryParams.get(filterQueryParamName) | ||||
|       const filterQueryParamValues: string[] = rule_type.multi | ||||
|         ? valueURIComponent.split(',') | ||||
|         : [valueURIComponent] | ||||
|  | ||||
|       filterRulesFromQueryParams = filterRulesFromQueryParams.concat( | ||||
|         // map all values to filter rules | ||||
|         filterQueryParamValues.map((val) => { | ||||
|           return { | ||||
|             rule_type: rule_type.id, | ||||
|             value: isNullRuleType ? null : val, | ||||
|           } | ||||
|         }) | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|   return filterRulesFromQueryParams | ||||
| } | ||||
| @@ -10,8 +10,9 @@ import { map } from 'rxjs/operators' | ||||
| import { CorrespondentService } from './correspondent.service' | ||||
| import { DocumentTypeService } from './document-type.service' | ||||
| import { TagService } from './tag.service' | ||||
| import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions' | ||||
| import { filterRulesToQueryParams } from '../query-params.service' | ||||
| import { StoragePathService } from './storage-path.service' | ||||
|  | ||||
| export const DOCUMENT_SORT_FIELDS = [ | ||||
|   { field: 'archive_serial_number', name: $localize`ASN` }, | ||||
| @@ -37,6 +38,7 @@ export interface SelectionDataItem { | ||||
| } | ||||
|  | ||||
| export interface SelectionData { | ||||
|   selected_storage_paths: SelectionDataItem[] | ||||
|   selected_correspondents: SelectionDataItem[] | ||||
|   selected_tags: SelectionDataItem[] | ||||
|   selected_document_types: SelectionDataItem[] | ||||
| @@ -52,32 +54,12 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | ||||
|     http: HttpClient, | ||||
|     private correspondentService: CorrespondentService, | ||||
|     private documentTypeService: DocumentTypeService, | ||||
|     private tagService: TagService | ||||
|     private tagService: TagService, | ||||
|     private storagePathService: StoragePathService | ||||
|   ) { | ||||
|     super(http, 'documents') | ||||
|   } | ||||
|  | ||||
|   public filterRulesToQueryParams(filterRules: FilterRule[]): Object { | ||||
|     if (filterRules) { | ||||
|       let params = {} | ||||
|       for (let rule of filterRules) { | ||||
|         let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) | ||||
|         if (ruleType.multi) { | ||||
|           params[ruleType.filtervar] = params[ruleType.filtervar] | ||||
|             ? params[ruleType.filtervar] + ',' + rule.value | ||||
|             : rule.value | ||||
|         } else if (ruleType.isnull_filtervar && rule.value == null) { | ||||
|           params[ruleType.isnull_filtervar] = true | ||||
|         } else { | ||||
|           params[ruleType.filtervar] = rule.value | ||||
|         } | ||||
|       } | ||||
|       return params | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   addObservablesToDocument(doc: PaperlessDocument) { | ||||
|     if (doc.correspondent) { | ||||
|       doc.correspondent$ = this.correspondentService.getCached( | ||||
| @@ -90,6 +72,9 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | ||||
|     if (doc.tags) { | ||||
|       doc.tags$ = this.tagService.getCachedMany(doc.tags) | ||||
|     } | ||||
|     if (doc.storage_path) { | ||||
|       doc.storage_path$ = this.storagePathService.getCached(doc.storage_path) | ||||
|     } | ||||
|     return doc | ||||
|   } | ||||
|  | ||||
| @@ -106,7 +91,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | ||||
|       pageSize, | ||||
|       sortField, | ||||
|       sortReverse, | ||||
|       Object.assign(extraParams, this.filterRulesToQueryParams(filterRules)) | ||||
|       Object.assign(extraParams, filterRulesToQueryParams(filterRules)) | ||||
|     ).pipe( | ||||
|       map((results) => { | ||||
|         results.results.forEach((doc) => this.addObservablesToDocument(doc)) | ||||
|   | ||||
| @@ -9,13 +9,19 @@ import { AbstractPaperlessService } from './abstract-paperless-service' | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class SavedViewService extends AbstractPaperlessService<PaperlessSavedView> { | ||||
|   loading: boolean | ||||
|  | ||||
|   constructor(http: HttpClient) { | ||||
|     super(http, 'saved_views') | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   private reload() { | ||||
|     this.listAll().subscribe((r) => (this.savedViews = r.results)) | ||||
|     this.loading = true | ||||
|     this.listAll().subscribe((r) => { | ||||
|       this.savedViews = r.results | ||||
|       this.loading = false | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   private savedViews: PaperlessSavedView[] = [] | ||||
|   | ||||
							
								
								
									
										13
									
								
								src-ui/src/app/services/rest/storage-path.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src-ui/src/app/services/rest/storage-path.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Injectable } from '@angular/core' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { AbstractNameFilterService } from './abstract-name-filter-service' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class StoragePathService extends AbstractNameFilterService<PaperlessStoragePath> { | ||||
|   constructor(http: HttpClient) { | ||||
|     super(http, 'storage_paths') | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { DOCUMENT } from '@angular/common' | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { | ||||
|   Inject, | ||||
|   Injectable, | ||||
| @@ -9,17 +10,19 @@ import { | ||||
| } from '@angular/core' | ||||
| import { Meta } from '@angular/platform-browser' | ||||
| import { CookieService } from 'ngx-cookie-service' | ||||
| import { first, Observable, tap } from 'rxjs' | ||||
| import { | ||||
|   BRIGHTNESS, | ||||
|   estimateBrightnessForColor, | ||||
|   hexToHsl, | ||||
| } from 'src/app/utils/color' | ||||
|  | ||||
| export interface PaperlessSettings { | ||||
|   key: string | ||||
|   type: string | ||||
|   default: any | ||||
| } | ||||
| import { environment } from 'src/environments/environment' | ||||
| import { | ||||
|   PaperlessUiSettings, | ||||
|   SETTINGS, | ||||
|   SETTINGS_KEYS, | ||||
| } from '../data/paperless-uisettings' | ||||
| import { ToastService } from './toast.service' | ||||
|  | ||||
| export interface LanguageOption { | ||||
|   code: string | ||||
| @@ -32,89 +35,42 @@ export interface LanguageOption { | ||||
|   dateInputFormat?: string | ||||
| } | ||||
|  | ||||
| export const SETTINGS_KEYS = { | ||||
|   BULK_EDIT_CONFIRMATION_DIALOGS: | ||||
|     'general-settings:bulk-edit:confirmation-dialogs', | ||||
|   BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', | ||||
|   DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', | ||||
|   DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system', | ||||
|   DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled', | ||||
|   DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted', | ||||
|   THEME_COLOR: 'general-settings:theme:color', | ||||
|   USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer', | ||||
|   DATE_LOCALE: 'general-settings:date-display:date-locale', | ||||
|   DATE_FORMAT: 'general-settings:date-display:date-format', | ||||
|   NOTIFICATIONS_CONSUMER_NEW_DOCUMENT: | ||||
|     'general-settings:notifications:consumer-new-documents', | ||||
|   NOTIFICATIONS_CONSUMER_SUCCESS: | ||||
|     'general-settings:notifications:consumer-success', | ||||
|   NOTIFICATIONS_CONSUMER_FAILED: | ||||
|     'general-settings:notifications:consumer-failed', | ||||
|   NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: | ||||
|     'general-settings:notifications:consumer-suppress-on-dashboard', | ||||
| } | ||||
|  | ||||
| const SETTINGS: PaperlessSettings[] = [ | ||||
|   { | ||||
|     key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, | ||||
|     type: 'boolean', | ||||
|     default: false, | ||||
|   }, | ||||
|   { key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: 'number', default: 50 }, | ||||
|   { key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: 'boolean', default: true }, | ||||
|   { key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: 'boolean', default: false }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { key: SETTINGS_KEYS.THEME_COLOR, type: 'string', default: '' }, | ||||
|   { key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: 'boolean', default: false }, | ||||
|   { key: SETTINGS_KEYS.DATE_LOCALE, type: 'string', default: '' }, | ||||
|   { key: SETTINGS_KEYS.DATE_FORMAT, type: 'string', default: 'mediumDate' }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD, | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class SettingsService { | ||||
|   private renderer: Renderer2 | ||||
|   protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/' | ||||
|  | ||||
|   private settings: Object = {} | ||||
|  | ||||
|   public displayName: string | ||||
|  | ||||
|   constructor( | ||||
|     private rendererFactory: RendererFactory2, | ||||
|     rendererFactory: RendererFactory2, | ||||
|     @Inject(DOCUMENT) private document, | ||||
|     private cookieService: CookieService, | ||||
|     private meta: Meta, | ||||
|     @Inject(LOCALE_ID) private localeId: string | ||||
|     @Inject(LOCALE_ID) private localeId: string, | ||||
|     protected http: HttpClient, | ||||
|     private toastService: ToastService | ||||
|   ) { | ||||
|     this.renderer = rendererFactory.createRenderer(null, null) | ||||
|   } | ||||
|  | ||||
|     this.updateAppearanceSettings() | ||||
|   // this is called by the app initializer in app.module | ||||
|   public initializeSettings(): Observable<PaperlessUiSettings> { | ||||
|     return this.http.get<PaperlessUiSettings>(this.baseUrl).pipe( | ||||
|       first(), | ||||
|       tap((uisettings) => { | ||||
|         Object.assign(this.settings, uisettings.settings) | ||||
|         this.maybeMigrateSettings() | ||||
|         // to update lang cookie | ||||
|         if (this.settings['language']?.length) | ||||
|           this.setLanguage(this.settings['language']) | ||||
|         this.displayName = uisettings.display_name.trim() | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   public updateAppearanceSettings( | ||||
| @@ -333,11 +289,13 @@ export class SettingsService { | ||||
|   } | ||||
|  | ||||
|   getLanguage(): string { | ||||
|     return this.cookieService.get(this.getLanguageCookieName()) | ||||
|     return this.get(SETTINGS_KEYS.LANGUAGE) | ||||
|   } | ||||
|  | ||||
|   setLanguage(language: string) { | ||||
|     if (language) { | ||||
|     this.set(SETTINGS_KEYS.LANGUAGE, language) | ||||
|     if (language?.length) { | ||||
|       // for Django | ||||
|       this.cookieService.set(this.getLanguageCookieName(), language) | ||||
|     } else { | ||||
|       this.cookieService.delete(this.getLanguageCookieName()) | ||||
| @@ -362,7 +320,16 @@ export class SettingsService { | ||||
|       return null | ||||
|     } | ||||
|  | ||||
|     let value = localStorage.getItem(key) | ||||
|     let value = null | ||||
|     // parse key:key:key into nested object | ||||
|     const keys = key.replace('general-settings:', '').split(':') | ||||
|     let settingObj = this.settings | ||||
|     keys.forEach((keyPart, index) => { | ||||
|       keyPart = keyPart.replace(/-/g, '_') | ||||
|       if (!settingObj.hasOwnProperty(keyPart)) return | ||||
|       if (index == keys.length - 1) value = settingObj[keyPart] | ||||
|       else settingObj = settingObj[keyPart] | ||||
|     }) | ||||
|  | ||||
|     if (value != null) { | ||||
|       switch (setting.type) { | ||||
| @@ -381,10 +348,57 @@ export class SettingsService { | ||||
|   } | ||||
|  | ||||
|   set(key: string, value: any) { | ||||
|     localStorage.setItem(key, value.toString()) | ||||
|     // parse key:key:key into nested object | ||||
|     let settingObj = this.settings | ||||
|     const keys = key.replace('general-settings:', '').split(':') | ||||
|     keys.forEach((keyPart, index) => { | ||||
|       keyPart = keyPart.replace(/-/g, '_') | ||||
|       if (!settingObj.hasOwnProperty(keyPart)) settingObj[keyPart] = {} | ||||
|       if (index == keys.length - 1) settingObj[keyPart] = value | ||||
|       else settingObj = settingObj[keyPart] | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   unset(key: string) { | ||||
|     localStorage.removeItem(key) | ||||
|   storeSettings(): Observable<any> { | ||||
|     return this.http.post(this.baseUrl, { settings: this.settings }) | ||||
|   } | ||||
|  | ||||
|   maybeMigrateSettings() { | ||||
|     if ( | ||||
|       !this.settings.hasOwnProperty('documentListSize') && | ||||
|       localStorage.getItem(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) | ||||
|     ) { | ||||
|       // lets migrate | ||||
|       const successMessage = $localize`Successfully completed one-time migratration of settings to the database!` | ||||
|       const errorMessage = $localize`Unable to migrate settings to the database, please try saving manually.` | ||||
|  | ||||
|       try { | ||||
|         for (const setting in SETTINGS_KEYS) { | ||||
|           const key = SETTINGS_KEYS[setting] | ||||
|           const value = localStorage.getItem(key) | ||||
|           this.set(key, value) | ||||
|         } | ||||
|         this.set( | ||||
|           SETTINGS_KEYS.LANGUAGE, | ||||
|           this.cookieService.get(this.getLanguageCookieName()) | ||||
|         ) | ||||
|       } catch (error) { | ||||
|         this.toastService.showError(errorMessage) | ||||
|         console.log(error) | ||||
|       } | ||||
|  | ||||
|       this.storeSettings() | ||||
|         .pipe(first()) | ||||
|         .subscribe({ | ||||
|           next: () => { | ||||
|             this.updateAppearanceSettings() | ||||
|             this.toastService.showInfo(successMessage) | ||||
|           }, | ||||
|           error: (e) => { | ||||
|             this.toastService.showError(errorMessage) | ||||
|             console.log(e) | ||||
|           }, | ||||
|         }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 phail
					phail