mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'dev' into feature-consume-eml
This commit is contained in:
commit
d0a0ae91c4
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
Loading…
x
Reference in New Issue
Block a user