mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'dev'
This commit is contained in:
commit
8c9a74ee0c
@ -1,6 +1,6 @@
|
||||
{
|
||||
"qpdf": {
|
||||
"version": "11.1.1"
|
||||
"version": "11.2.0"
|
||||
},
|
||||
"jbig2enc": {
|
||||
"version": "0.29",
|
||||
|
12
.github/release-drafter.yml
vendored
12
.github/release-drafter.yml
vendored
@ -4,6 +4,7 @@ autolabeler:
|
||||
- '/^fix/'
|
||||
title:
|
||||
- "/^fix/i"
|
||||
- "/^Bugfix/i"
|
||||
- label: "enhancement"
|
||||
branch:
|
||||
- '/^feature/'
|
||||
@ -13,6 +14,9 @@ categories:
|
||||
- title: 'Breaking Changes'
|
||||
labels:
|
||||
- 'breaking-change'
|
||||
- title: 'Notable Changes'
|
||||
labels:
|
||||
- 'notable'
|
||||
- title: 'Features'
|
||||
labels:
|
||||
- 'enhancement'
|
||||
@ -20,7 +24,8 @@ categories:
|
||||
labels:
|
||||
- 'bug'
|
||||
- title: 'Documentation'
|
||||
label: 'documentation'
|
||||
labels:
|
||||
- 'documentation'
|
||||
- title: 'Maintenance'
|
||||
labels:
|
||||
- 'chore'
|
||||
@ -29,7 +34,8 @@ categories:
|
||||
- 'ci-cd'
|
||||
- title: 'Dependencies'
|
||||
collapse-after: 3
|
||||
label: 'dependencies'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- title: 'All App Changes'
|
||||
labels:
|
||||
- 'frontend'
|
||||
@ -46,6 +52,8 @@ include-labels:
|
||||
- 'frontend'
|
||||
- 'backend'
|
||||
- 'ci-cd'
|
||||
- 'breaking-change'
|
||||
- 'notable'
|
||||
category-template: '### $TITLE'
|
||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||
change-title-escapes: '\<*_&#@'
|
||||
|
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@ -16,7 +16,7 @@ on:
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Linting Checks
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
documentation:
|
||||
name: "Build Documentation"
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
steps:
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
-
|
||||
name: Install pipenv
|
||||
run: |
|
||||
pipx install pipenv==2022.10.12
|
||||
pipx install pipenv==2022.11.30
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
|
||||
documentation-deploy:
|
||||
name: "Deploy Documentation"
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- documentation
|
||||
@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
tests-backend:
|
||||
name: "Tests (${{ matrix.python-version }})"
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
strategy:
|
||||
@ -106,6 +106,10 @@ jobs:
|
||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
# Skip Tests which require convert
|
||||
PAPERLESS_TEST_SKIP_CONVERT: 1
|
||||
# Enable Gotenberg end to end testing
|
||||
GOTENBERG_LIVE: 1
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -120,7 +124,7 @@ jobs:
|
||||
-
|
||||
name: Install pipenv
|
||||
run: |
|
||||
pipx install pipenv==2022.10.12
|
||||
pipx install pipenv==2022.11.30
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
@ -177,7 +181,7 @@ jobs:
|
||||
|
||||
tests-frontend:
|
||||
name: "Tests Frontend"
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
strategy:
|
||||
@ -191,13 +195,14 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: cd src-ui && npm ci
|
||||
- run: cd src-ui && npm run lint
|
||||
- run: cd src-ui && npm run test
|
||||
- run: cd src-ui && npm run e2e:ci
|
||||
|
||||
prepare-docker-build:
|
||||
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
|
||||
runs-on: ubuntu-22.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
|
||||
@ -274,7 +279,7 @@ jobs:
|
||||
|
||||
# build and push image to docker hub.
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
@ -379,7 +384,7 @@ jobs:
|
||||
build-release:
|
||||
needs:
|
||||
- build-docker-image
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -458,7 +463,7 @@ jobs:
|
||||
path: dist/paperless-ngx.tar.xz
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||
changelog: ${{ steps.create-release.outputs.body }}
|
||||
@ -507,7 +512,7 @@ jobs:
|
||||
asset_content_type: application/x-xz
|
||||
|
||||
append-changelog:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- publish-release
|
||||
if: needs.publish-release.outputs.prerelease == 'false'
|
||||
|
12
.github/workflows/cleanup-tags.yml
vendored
12
.github/workflows/cleanup-tags.yml
vendored
@ -1,17 +1,14 @@
|
||||
# This workflow runs on certain conditions to check for and potentially
|
||||
# delete container images from the GHCR which no longer have an associated
|
||||
# code branch.
|
||||
# Requires a PAT with the correct scope set in the secrets
|
||||
# Requires a PAT with the correct scope set in the secrets.
|
||||
#
|
||||
# This workflow will not trigger runs on forked repos.
|
||||
|
||||
name: Cleanup Image Tags
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * SAT'
|
||||
delete:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/cleanup-tags.yml"
|
||||
@ -26,7 +23,8 @@ concurrency:
|
||||
jobs:
|
||||
cleanup-images:
|
||||
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'paperless-ngx'
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
3
.github/workflows/installer-library.yml
vendored
3
.github/workflows/installer-library.yml
vendored
@ -34,7 +34,7 @@ concurrency:
|
||||
jobs:
|
||||
prepare-docker-build:
|
||||
name: Prepare Docker Image Version Data
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Set ghcr repository name
|
||||
@ -127,6 +127,7 @@ jobs:
|
||||
uses: ./.github/workflows/reusable-workflow-builder.yml
|
||||
with:
|
||||
dockerfile: ./docker-builders/Dockerfile.qpdf
|
||||
build-platforms: linux/amd64
|
||||
build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }}
|
||||
build-args: |
|
||||
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
|
4
.github/workflows/project-actions.yml
vendored
4
.github/workflows/project-actions.yml
vendored
@ -24,7 +24,7 @@ env:
|
||||
jobs:
|
||||
issue_opened_or_reopened:
|
||||
name: issue_opened_or_reopened
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||
steps:
|
||||
- name: Add issue to project and set status to ${{ env.todo }}
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
status_value: ${{ env.todo }} # Target status
|
||||
pr_opened_or_reopened:
|
||||
name: pr_opened_or_reopened
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
# write permission is required for autolabeler
|
||||
pull-requests: write
|
||||
|
2
.github/workflows/release-chart.yml
vendored
2
.github/workflows/release-chart.yml
vendored
@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
release_chart:
|
||||
name: "Release Chart"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
@ -13,6 +13,10 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
build-platforms:
|
||||
required: false
|
||||
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ fromJSON(inputs.build-json).name }}-${{ fromJSON(inputs.build-json).version }}
|
||||
@ -21,7 +25,7 @@ concurrency:
|
||||
jobs:
|
||||
build-image:
|
||||
name: Build ${{ fromJSON(inputs.build-json).name }} @ ${{ fromJSON(inputs.build-json).version }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -46,7 +50,7 @@ jobs:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
tags: ${{ fromJSON(inputs.build-json).image_tag }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: ${{ inputs.build-platforms }}
|
||||
build-args: ${{ inputs.build-args }}
|
||||
push: true
|
||||
cache-from: type=registry,ref=${{ fromJSON(inputs.build-json).cache_tag }}
|
||||
|
@ -5,7 +5,7 @@
|
||||
repos:
|
||||
# General hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
@ -48,23 +48,23 @@ repos:
|
||||
- id: yesqa
|
||||
exclude: "(migrations)"
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
rev: "v2.3.0"
|
||||
rev: "v2.4.0"
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
exclude: "(migrations)"
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: ^src/
|
||||
args:
|
||||
- "--config=./src/setup.cfg"
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.2.2
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
exclude: "(migrations)"
|
||||
@ -83,6 +83,6 @@ repos:
|
||||
args:
|
||||
- "--tab"
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: "v0.8.0.4"
|
||||
rev: "v0.9.0.2"
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
22
Dockerfile
22
Dockerfile
@ -45,7 +45,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2022.11.30 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@ -58,6 +58,12 @@ LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-n
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# Buildx provided
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
# Workflow provided
|
||||
ARG QPDF_VERSION
|
||||
|
||||
#
|
||||
# Begin installation and configuration
|
||||
@ -194,14 +200,10 @@ RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \
|
||||
--mount=type=bind,from=pikepdf-builder,target=/pikepdf \
|
||||
set -eux \
|
||||
&& echo "Installing qpdf" \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf29_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/libqpdf29_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/qpdf_*.deb \
|
||||
&& echo "Installing pikepdf and dependencies" \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pyparsing*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/packaging*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/lxml*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/Pillow*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pikepdf*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/*.whl \
|
||||
&& python3 -m pip list \
|
||||
&& echo "Installing psycopg2" \
|
||||
&& python3 -m pip install --no-cache-dir /psycopg2/usr/src/wheels/psycopg2*.whl \
|
||||
@ -228,6 +230,10 @@ RUN set -eux \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/local/share/nltk_data" snowball_data \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/local/share/nltk_data" stopwords \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/local/share/nltk_data" punkt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
|
21
Pipfile
21
Pipfile
@ -30,8 +30,6 @@ psycopg2 = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.1"
|
||||
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
||||
scipy = "==1.8.1"
|
||||
numpy = "*"
|
||||
whitenoise = "~=6.2"
|
||||
watchdog = "~=2.1"
|
||||
@ -43,9 +41,6 @@ tika = "*"
|
||||
# TODO: This will sadly also install daphne+dependencies,
|
||||
# which an ASGI server we don't need. Adds about 15MB image size.
|
||||
channels = "~=3.0"
|
||||
# Locked version until https://github.com/django/channels_redis/issues/332
|
||||
# is resolved
|
||||
channels-redis = "==3.4.1"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
concurrent-log-handler = "*"
|
||||
"pdfminer.six" = "*"
|
||||
@ -60,6 +55,21 @@ setproctitle = "*"
|
||||
nltk = "*"
|
||||
pdf2image = "*"
|
||||
flower = "*"
|
||||
bleach = "*"
|
||||
|
||||
#
|
||||
# Packages locked due to issues (try to check if these are fixed in a release every so often)
|
||||
#
|
||||
|
||||
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
||||
scipy = "==1.8.1"
|
||||
|
||||
# Newer versions aren't builting yet (see https://www.piwheels.org/project/cryptography/)
|
||||
cryptography = "==38.0.1"
|
||||
|
||||
# Locked version until https://github.com/django/channels_redis/issues/332
|
||||
# is resolved
|
||||
channels-redis = "==3.4.1"
|
||||
|
||||
[dev-packages]
|
||||
coveralls = "*"
|
||||
@ -76,4 +86,5 @@ black = "*"
|
||||
pre-commit = "*"
|
||||
sphinx-autobuild = "*"
|
||||
myst-parser = "*"
|
||||
imagehash = "*"
|
||||
mkdocs-material = "*"
|
||||
|
1508
Pipfile.lock
generated
1508
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,9 +10,9 @@
|
||||
# Example Usage:
|
||||
# ./build-docker-image.sh Dockerfile -t paperless-ngx:my-awesome-feature
|
||||
|
||||
set -eux
|
||||
set -eu
|
||||
|
||||
if ! command -v jq; then
|
||||
if ! command -v jq &> /dev/null ; then
|
||||
echo "jq required"
|
||||
exit 1
|
||||
elif [ ! -f "$1" ]; then
|
||||
@ -20,28 +20,62 @@ elif [ ! -f "$1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse what we can from Pipfile.lock
|
||||
pikepdf_version=$(jq ".default.pikepdf.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
psycopg2_version=$(jq ".default.psycopg2.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
pillow_version=$(jq ".default.pillow.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
lxml_version=$(jq ".default.lxml.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
# Read this from the other config file
|
||||
qpdf_version=$(jq ".qpdf.version" .build-config.json | sed 's/"//g')
|
||||
jbig2enc_version=$(jq ".jbig2enc.version" .build-config.json | sed 's/"//g')
|
||||
# Get the branch name (used for caching)
|
||||
branch_name=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
# Required to use cache-from
|
||||
export DOCKER_BUILDKIT=1
|
||||
# Parse eithe Pipfile.lock or the .build-config.json
|
||||
jbig2enc_version=$(jq ".jbig2enc.version" .build-config.json | sed 's/"//g')
|
||||
qpdf_version=$(jq ".qpdf.version" .build-config.json | sed 's/"//g')
|
||||
psycopg2_version=$(jq ".default.psycopg2.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
pikepdf_version=$(jq ".default.pikepdf.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
pillow_version=$(jq ".default.pillow.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
lxml_version=$(jq ".default.lxml.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||
|
||||
docker build --file "$1" \
|
||||
base_filename="$(basename -- "${1}")"
|
||||
build_args_str=""
|
||||
cache_from_str=""
|
||||
|
||||
case "${base_filename}" in
|
||||
|
||||
*.jbig2enc)
|
||||
build_args_str="--build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/jbig2enc:${jbig2enc_version}"
|
||||
;;
|
||||
|
||||
*.psycopg2)
|
||||
build_args_str="--build-arg PSYCOPG2_VERSION=${psycopg2_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/psycopg2:${psycopg2_version}"
|
||||
;;
|
||||
|
||||
*.qpdf)
|
||||
build_args_str="--build-arg QPDF_VERSION=${qpdf_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/qpdf:${qpdf_version}"
|
||||
;;
|
||||
|
||||
*.pikepdf)
|
||||
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PILLOW_VERSION=${pillow_version} --build-arg LXML_VERSION=${lxml_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/pikepdf:${pikepdf_version}"
|
||||
;;
|
||||
|
||||
Dockerfile)
|
||||
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PSYCOPG2_VERSION=${psycopg2_version} --build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:${branch_name} --cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unable to match ${base_filename}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
read -r -a build_args_arr <<< "${build_args_str}"
|
||||
read -r -a cache_from_arr <<< "${cache_from_str}"
|
||||
|
||||
set -eux
|
||||
|
||||
docker buildx build --file "${1}" \
|
||||
--progress=plain \
|
||||
--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:"${branch_name}" \
|
||||
--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev \
|
||||
--build-arg JBIG2ENC_VERSION="${jbig2enc_version}" \
|
||||
--build-arg QPDF_VERSION="${qpdf_version}" \
|
||||
--build-arg PIKEPDF_VERSION="${pikepdf_version}" \
|
||||
--build-arg PILLOW_VERSION="${pillow_version}" \
|
||||
--build-arg LXML_VERSION="${lxml_version}" \
|
||||
--build-arg PSYCOPG2_VERSION="${psycopg2_version}" "${@:2}" .
|
||||
--output=type=docker \
|
||||
"${cache_from_arr[@]}" \
|
||||
"${build_args_arr[@]}" \
|
||||
"${@:2}" .
|
||||
|
@ -16,7 +16,13 @@ FROM python:3.9-slim-bullseye as main
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with pikepdf wheel built"
|
||||
|
||||
# Buildx provided
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# Workflow provided
|
||||
ARG QPDF_VERSION
|
||||
ARG PIKEPDF_VERSION
|
||||
# These are not used, but will still bust the cache if one changes
|
||||
# Otherwise, the main image will try to build thing (and fail)
|
||||
@ -54,7 +60,7 @@ ARG BUILD_PACKAGES="\
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
COPY --from=qpdf-builder /usr/src/qpdf/*.deb ./
|
||||
COPY --from=qpdf-builder /usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.deb ./
|
||||
|
||||
# As this is an base image for a multi-stage final image
|
||||
# the added size of the install is basically irrelevant
|
||||
@ -77,6 +83,8 @@ RUN set -eux \
|
||||
&& python3 -m pip wheel \
|
||||
# Build the package at the required version
|
||||
pikepdf==${PIKEPDF_VERSION} \
|
||||
# Look to piwheels for additional pre-built wheels
|
||||
--extra-index-url https://www.piwheels.org/simple \
|
||||
# Output the *.whl into this directory
|
||||
--wheel-dir wheels \
|
||||
# Do not use a binary packge for the package being built
|
||||
@ -86,6 +94,8 @@ RUN set -eux \
|
||||
# Don't cache build files
|
||||
--no-cache-dir \
|
||||
&& ls -ahl wheels \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
|
@ -42,6 +42,8 @@ RUN set -eux \
|
||||
# Don't cache build files
|
||||
--no-cache-dir \
|
||||
&& ls -ahl wheels/ \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
|
@ -1,48 +1,156 @@
|
||||
# This Dockerfile compiles the jbig2enc library
|
||||
# Inputs:
|
||||
# - QPDF_VERSION - the version of qpdf to build a .deb.
|
||||
# Must be present as a deb-src in bookworm
|
||||
#
|
||||
# Stage: pre-build
|
||||
# Purpose:
|
||||
# - Installs common packages
|
||||
# - Sets common environment variables related to dpkg
|
||||
# - Aquires the qpdf source from bookwork
|
||||
# Useful Links:
|
||||
# - https://qpdf.readthedocs.io/en/stable/installation.html#system-requirements
|
||||
# - https://wiki.debian.org/Multiarch/HOWTO
|
||||
# - https://wiki.debian.org/CrossCompiling
|
||||
#
|
||||
|
||||
FROM debian:bullseye-slim as main
|
||||
FROM debian:bullseye-slim as pre-build
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with qpdf built"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# This must match to pikepdf's minimum at least
|
||||
ARG QPDF_VERSION
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
debhelper \
|
||||
ARG COMMON_BUILD_PACKAGES="\
|
||||
cmake \
|
||||
debhelper\
|
||||
debian-keyring \
|
||||
devscripts \
|
||||
equivs \
|
||||
libtool \
|
||||
# https://qpdf.readthedocs.io/en/stable/installation.html#system-requirements
|
||||
libjpeg62-turbo-dev \
|
||||
libgnutls28-dev \
|
||||
dpkg-dev \
|
||||
equivs \
|
||||
packaging-dev \
|
||||
cmake \
|
||||
zlib1g-dev"
|
||||
libtool"
|
||||
|
||||
ENV DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2"
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing build tools" \
|
||||
&& echo "Installing common packages" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends $BUILD_PACKAGES \
|
||||
&& echo "Getting qpdf src" \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${COMMON_BUILD_PACKAGES} \
|
||||
&& echo "Getting qpdf source" \
|
||||
&& echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
|
||||
&& apt-get update \
|
||||
&& mkdir qpdf \
|
||||
&& cd qpdf \
|
||||
&& apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm \
|
||||
&& echo "Building qpdf" \
|
||||
&& cd qpdf-$QPDF_VERSION \
|
||||
&& export DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
|
||||
&& ls -ahl ../*.deb \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm
|
||||
|
||||
#
|
||||
# Stage: amd64-builder
|
||||
# Purpose: Builds qpdf for x86_64 (native build)
|
||||
#
|
||||
FROM pre-build as amd64-builder
|
||||
|
||||
ARG AMD64_BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
libjpeg62-turbo-dev:amd64 \
|
||||
libgnutls28-dev:amd64 \
|
||||
zlib1g-dev:amd64"
|
||||
|
||||
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Beginning amd64" \
|
||||
&& echo "Install amd64 packages" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${AMD64_BUILD_PACKAGES} \
|
||||
&& echo "Building amd64" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
|
||||
&& echo "Removing debug files" \
|
||||
&& rm -f ../libqpdf29-dbgsym* \
|
||||
&& rm -f ../qpdf-dbgsym* \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||
#
|
||||
# Stage: armhf-builder
|
||||
# Purpose:
|
||||
# - Sets armhf specific environment
|
||||
# - Builds qpdf for armhf (cross compile)
|
||||
#
|
||||
FROM pre-build as armhf-builder
|
||||
|
||||
ARG ARMHF_PACKAGES="\
|
||||
crossbuild-essential-armhf \
|
||||
libjpeg62-turbo-dev:armhf \
|
||||
libgnutls28-dev:armhf \
|
||||
zlib1g-dev:armhf"
|
||||
|
||||
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||
|
||||
ENV CXX="/usr/bin/arm-linux-gnueabihf-g++" \
|
||||
CC="/usr/bin/arm-linux-gnueabihf-gcc"
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Beginning armhf" \
|
||||
&& echo "Install armhf packages" \
|
||||
&& dpkg --add-architecture armhf \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${ARMHF_PACKAGES} \
|
||||
&& echo "Building armhf" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch armhf \
|
||||
&& echo "Removing debug files" \
|
||||
&& rm -f ../libqpdf29-dbgsym* \
|
||||
&& rm -f ../qpdf-dbgsym* \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||
|
||||
#
|
||||
# Stage: aarch64-builder
|
||||
# Purpose:
|
||||
# - Sets aarch64 specific environment
|
||||
# - Builds qpdf for aarch64 (cross compile)
|
||||
#
|
||||
FROM pre-build as aarch64-builder
|
||||
|
||||
ARG ARM64_PACKAGES="\
|
||||
crossbuild-essential-arm64 \
|
||||
libjpeg62-turbo-dev:arm64 \
|
||||
libgnutls28-dev:arm64 \
|
||||
zlib1g-dev:arm64"
|
||||
|
||||
ENV CXX="/usr/bin/aarch64-linux-gnu-g++" \
|
||||
CC="/usr/bin/aarch64-linux-gnu-gcc"
|
||||
|
||||
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Beginning arm64" \
|
||||
&& echo "Install arm64 packages" \
|
||||
&& dpkg --add-architecture arm64 \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${ARM64_PACKAGES} \
|
||||
&& echo "Building arm64" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch arm64 \
|
||||
&& echo "Removing debug files" \
|
||||
&& rm -f ../libqpdf29-dbgsym* \
|
||||
&& rm -f ../qpdf-dbgsym* \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||
|
||||
#
|
||||
# Stage: package
|
||||
# Purpose: Holds the compiled .deb files in arch/variant specific folders
|
||||
#
|
||||
FROM alpine:3.17 as package
|
||||
|
||||
LABEL org.opencontainers.image.description="A image with qpdf installers stored in architecture & version specific folders"
|
||||
|
||||
ARG QPDF_VERSION
|
||||
|
||||
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/amd64
|
||||
|
||||
COPY --from=amd64-builder /usr/src/*.deb ./
|
||||
COPY --from=amd64-builder /usr/src/pkg-list.txt ./
|
||||
|
||||
# Note this is ${TARGETARCH}${TARGETVARIANT} for armv7
|
||||
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/armv7
|
||||
|
||||
COPY --from=armhf-builder /usr/src/*.deb ./
|
||||
COPY --from=armhf-builder /usr/src/pkg-list.txt ./
|
||||
|
||||
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/arm64
|
||||
|
||||
COPY --from=aarch64-builder /usr/src/*.deb ./
|
||||
COPY --from=aarch64-builder /usr/src/pkg-list.txt ./
|
||||
|
@ -11,9 +11,12 @@ services:
|
||||
container_name: gotenberg
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-routes=true"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
hostname: tika
|
||||
|
@ -85,9 +85,12 @@ services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-routes=true"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
|
@ -79,9 +79,13 @@ services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-routes=true"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
|
@ -67,9 +67,13 @@ services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-routes=true"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
|
@ -53,30 +53,6 @@ map_folders() {
|
||||
export CONSUME_DIR="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
|
||||
}
|
||||
|
||||
nltk_data () {
|
||||
# Store the NLTK data outside the Docker container
|
||||
local -r nltk_data_dir="${DATA_DIR}/nltk"
|
||||
local -r truthy_things=("yes y 1 t true")
|
||||
|
||||
# If not set, or it looks truthy
|
||||
if [[ -z "${PAPERLESS_ENABLE_NLTK}" ]] || [[ "${truthy_things[*]}" =~ ${PAPERLESS_ENABLE_NLTK,} ]]; then
|
||||
|
||||
# Download or update the snowball stemmer data
|
||||
python3 -W ignore::RuntimeWarning -m nltk.downloader -d "${nltk_data_dir}" snowball_data
|
||||
|
||||
# Download or update the stopwords corpus
|
||||
python3 -W ignore::RuntimeWarning -m nltk.downloader -d "${nltk_data_dir}" stopwords
|
||||
|
||||
# Download or update the punkt tokenizer data
|
||||
python3 -W ignore::RuntimeWarning -m nltk.downloader -d "${nltk_data_dir}" punkt
|
||||
|
||||
else
|
||||
echo "Skipping NLTK data download"
|
||||
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
custom_container_init() {
|
||||
# Mostly borrowed from the LinuxServer.io base image
|
||||
# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d
|
||||
@ -157,8 +133,6 @@ initialize() {
|
||||
echo "Creating directory ${tmp_dir}"
|
||||
mkdir -p "${tmp_dir}"
|
||||
|
||||
nltk_data
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
chown -R paperless:paperless ${tmp_dir}
|
||||
@ -191,10 +165,6 @@ install_languages() {
|
||||
|
||||
for lang in "${langs[@]}"; do
|
||||
pkg="tesseract-ocr-$lang"
|
||||
# English is installed by default
|
||||
#if [[ "$lang" == "eng" ]]; then
|
||||
# continue
|
||||
#fi
|
||||
|
||||
if dpkg -s "$pkg" &>/dev/null; then
|
||||
echo "Package $pkg already installed!"
|
||||
|
@ -20,7 +20,6 @@ wait_for_postgres() {
|
||||
exit 1
|
||||
else
|
||||
echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
|
||||
|
||||
fi
|
||||
|
||||
attempt_num=$(("$attempt_num" + 1))
|
||||
@ -37,6 +36,8 @@ wait_for_mariadb() {
|
||||
local attempt_num=1
|
||||
local -r max_attempts=5
|
||||
|
||||
# Disable warning, host and port can't have spaces
|
||||
# shellcheck disable=SC2086
|
||||
while ! true > /dev/tcp/$host/$port; do
|
||||
|
||||
if [ $attempt_num -eq $max_attempts ]; then
|
||||
@ -67,10 +68,16 @@ migrations() {
|
||||
# of the current container starts.
|
||||
flock 200
|
||||
echo "Apply database migrations..."
|
||||
python3 manage.py migrate
|
||||
python3 manage.py migrate --skip-checks --no-input
|
||||
) 200>"${DATA_DIR}/migration_lock"
|
||||
}
|
||||
|
||||
django_checks() {
|
||||
# Explicitly run the Django system checks
|
||||
echo "Running Django checks"
|
||||
python3 manage.py check
|
||||
}
|
||||
|
||||
search_index() {
|
||||
|
||||
local -r index_version=1
|
||||
@ -100,6 +107,8 @@ do_work() {
|
||||
|
||||
migrations
|
||||
|
||||
django_checks
|
||||
|
||||
search_index
|
||||
|
||||
superuser
|
||||
|
@ -233,6 +233,7 @@ optional arguments:
|
||||
-c, --compare-checksums
|
||||
-f, --use-filename-format
|
||||
-d, --delete
|
||||
-z --zip
|
||||
```
|
||||
|
||||
`target` is a folder to which the data gets written. This includes
|
||||
@ -258,6 +259,9 @@ current export such as files from deleted documents, specify `--delete`.
|
||||
Be careful when pointing paperless to a directory that already contains
|
||||
other files.
|
||||
|
||||
If `-z` or `--zip` is provided, the export will be a zipfile
|
||||
in the target directory, named according to the current date.
|
||||
|
||||
The filenames generated by this command follow the format
|
||||
`[date created] [correspondent] [title].[extension]`. If you want
|
||||
paperless to use `PAPERLESS_FILENAME_FORMAT` for exported filenames
|
||||
|
@ -10,12 +10,10 @@ run paperless, these settings have to be defined in different places.
|
||||
- If you are running paperless on anything else, paperless will search
|
||||
for the configuration file in these locations and use the first one
|
||||
it finds:
|
||||
|
||||
```
|
||||
/path/to/paperless/paperless.conf
|
||||
/etc/paperless.conf
|
||||
/usr/local/etc/paperless.conf
|
||||
```
|
||||
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
|
||||
- `/path/to/paperless/paperless.conf`
|
||||
- `/etc/paperless.conf`
|
||||
- `/usr/local/etc/paperless.conf`
|
||||
|
||||
## Required services
|
||||
|
||||
@ -170,6 +168,19 @@ details.
|
||||
|
||||
Defaults to `PAPERLESS_DATA_DIR/log/`.
|
||||
|
||||
`PAPERLESS_NLTK_DIR=<path>`
|
||||
|
||||
: This is where paperless will search for the data required for NLTK
|
||||
processing, if you are using it. If you are using the Docker image,
|
||||
this should not be changed, as the data is included in the image
|
||||
already.
|
||||
|
||||
Previously, the location defaulted to `PAPERLESS_DATA_DIR/nltk`.
|
||||
Unless you are using this in a bare metal install or other setup,
|
||||
this folder is no longer needed and can be removed manually.
|
||||
|
||||
Defaults to `/usr/local/share/nltk_data`
|
||||
|
||||
## Logging
|
||||
|
||||
`PAPERLESS_LOGROTATE_MAX_SIZE=<num>`
|
||||
@ -564,8 +575,10 @@ they use underscores instead of dashes.
|
||||
|
||||
Paperless can make use of [Tika](https://tika.apache.org/) and
|
||||
[Gotenberg](https://gotenberg.dev/) for parsing and converting
|
||||
"Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
|
||||
wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||
"Office" documents (such as ".doc", ".xlsx" and ".odt").
|
||||
Tika and Gotenberg are also needed to allow parsing of E-Mails (.eml).
|
||||
|
||||
If you wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||
configure their endpoints, and enable the feature.
|
||||
|
||||
`PAPERLESS_TIKA_ENABLED=<bool>`
|
||||
@ -604,14 +617,17 @@ services:
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
# ...
|
||||
# ...
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- 'gotenberg'
|
||||
- '--chromium-disable-routes=true'
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- 'gotenberg'
|
||||
- '--chromium-disable-javascript=true'
|
||||
- '--chromium-allow-list=file:///tmp/.*'
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
@ -658,7 +674,7 @@ paperless will process in parallel on a single document.
|
||||
count, with a slight favor towards threads per worker:
|
||||
|
||||
| CPU core count | Workers | Threads |
|
||||
|----------------|---------|---------|
|
||||
| -------------- | ------- | ------- |
|
||||
| > 1 | > 1 | > 1 |
|
||||
| > 2 | > 2 | > 1 |
|
||||
| > 4 | > 2 | > 2 |
|
||||
@ -691,6 +707,16 @@ for details on how to set it.
|
||||
|
||||
Defaults to UTC.
|
||||
|
||||
`PAPERLESS_ENABLE_NLTK=<bool>`
|
||||
|
||||
: Enables or disables the advanced natural language processing
|
||||
used during automatic classification. If disabled, paperless will
|
||||
still preform some basic text pre-processing before matching.
|
||||
|
||||
See also `PAPERLESS_NLTK_DIR`.
|
||||
|
||||
Defaults to 1.
|
||||
|
||||
## Polling {#polling}
|
||||
|
||||
`PAPERLESS_CONSUMER_POLLING=<num>`
|
||||
|
@ -125,13 +125,13 @@ using docker-compose, this is achieved by the following configuration
|
||||
change in the `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- 'gotenberg'
|
||||
- '--chromium-disable-routes=true'
|
||||
- '--api-timeout=60'
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- 'gotenberg'
|
||||
- '--chromium-disable-javascript=true'
|
||||
- '--chromium-allow-list=file:///tmp/.*'
|
||||
- '--api-timeout=60'
|
||||
```
|
||||
|
||||
## Permission denied errors in the consumption directory
|
||||
|
@ -2,5 +2,5 @@
|
||||
|
||||
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||
docker run -d -p 6379:6379 redis:latest
|
||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7.6
|
||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7.6 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
||||
|
51
src-ui/.eslintrc.json
Normal file
51
src-ui/.eslintrc.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,179 +1,196 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"paperless-ui": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"be-BY": "src/locale/messages.be_BY.xlf",
|
||||
"cs-CZ": "src/locale/messages.cs_CZ.xlf",
|
||||
"da-DK": "src/locale/messages.da_DK.xlf",
|
||||
"de-DE": "src/locale/messages.de_DE.xlf",
|
||||
"en-GB": "src/locale/messages.en_GB.xlf",
|
||||
"es-ES": "src/locale/messages.es_ES.xlf",
|
||||
"fr-FR": "src/locale/messages.fr_FR.xlf",
|
||||
"it-IT": "src/locale/messages.it_IT.xlf",
|
||||
"lb-LU": "src/locale/messages.lb_LU.xlf",
|
||||
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||
"pl-PL": "src/locale/messages.pl_PL.xlf",
|
||||
"pt-BR": "src/locale/messages.pt_BR.xlf",
|
||||
"pt-PT": "src/locale/messages.pt_PT.xlf",
|
||||
"ro-RO": "src/locale/messages.ro_RO.xlf",
|
||||
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
||||
"sl-SI": "src/locale/messages.sl_SI.xlf",
|
||||
"sr-CS": "src/locale/messages.sr_CS.xlf",
|
||||
"sv-SE": "src/locale/messages.sv_SE.xlf",
|
||||
"tr-TR": "src/locale/messages.tr_TR.xlf",
|
||||
"zh-CN": "src/locale/messages.zh_CN.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/paperless-ui",
|
||||
"outputHashing": "none",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"localize": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/apple-touch-icon.png",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest", {
|
||||
"glob": "pdf.worker.min.js",
|
||||
"input": "node_modules/pdfjs-dist/build/",
|
||||
"output": "/assets/js/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ng2-pdf-viewer"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputPath": "../src/documents/static/frontend/",
|
||||
"optimization": true,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"localize": ["en-US"]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": ""
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "paperless-ui:build:en-US"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "paperless-ui:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "paperless-ui:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/jest:run",
|
||||
"options": {
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/apple-touch-icon.png",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "paperless-ui:serve",
|
||||
"watch": true,
|
||||
"headless": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "paperless-ui:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "paperless-ui:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "paperless-ui:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-open": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"watch": true,
|
||||
"headless": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "paperless-ui"
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"paperless-ui": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"be-BY": "src/locale/messages.be_BY.xlf",
|
||||
"cs-CZ": "src/locale/messages.cs_CZ.xlf",
|
||||
"da-DK": "src/locale/messages.da_DK.xlf",
|
||||
"de-DE": "src/locale/messages.de_DE.xlf",
|
||||
"en-GB": "src/locale/messages.en_GB.xlf",
|
||||
"es-ES": "src/locale/messages.es_ES.xlf",
|
||||
"fr-FR": "src/locale/messages.fr_FR.xlf",
|
||||
"it-IT": "src/locale/messages.it_IT.xlf",
|
||||
"lb-LU": "src/locale/messages.lb_LU.xlf",
|
||||
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||
"pl-PL": "src/locale/messages.pl_PL.xlf",
|
||||
"pt-BR": "src/locale/messages.pt_BR.xlf",
|
||||
"pt-PT": "src/locale/messages.pt_PT.xlf",
|
||||
"ro-RO": "src/locale/messages.ro_RO.xlf",
|
||||
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
||||
"sl-SI": "src/locale/messages.sl_SI.xlf",
|
||||
"sr-CS": "src/locale/messages.sr_CS.xlf",
|
||||
"sv-SE": "src/locale/messages.sv_SE.xlf",
|
||||
"tr-TR": "src/locale/messages.tr_TR.xlf",
|
||||
"zh-CN": "src/locale/messages.zh_CN.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/paperless-ui",
|
||||
"outputHashing": "none",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"localize": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/apple-touch-icon.png",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "pdf.worker.min.js",
|
||||
"input": "node_modules/pdfjs-dist/build/",
|
||||
"output": "/assets/js/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ng2-pdf-viewer"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputPath": "../src/documents/static/frontend/",
|
||||
"optimization": true,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"localize": [
|
||||
"en-US"
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": ""
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "paperless-ui:build:en-US"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "paperless-ui:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "paperless-ui:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/jest:run",
|
||||
"options": {
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/apple-touch-icon.png",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "paperless-ui:serve",
|
||||
"watch": true,
|
||||
"headless": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "paperless-ui:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "paperless-ui:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "paperless-ui:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-open": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"watch": true,
|
||||
"headless": false
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "paperless-ui",
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,16 @@ describe('settings', () => {
|
||||
req.reply(response)
|
||||
}
|
||||
).as('savedViews')
|
||||
|
||||
cy.intercept('http://localhost:8000/api/mail_accounts/*', {
|
||||
fixture: 'mail_accounts/mail_accounts.json',
|
||||
})
|
||||
cy.intercept('http://localhost:8000/api/mail_rules/*', {
|
||||
fixture: 'mail_rules/mail_rules.json',
|
||||
}).as('mailRules')
|
||||
cy.intercept('http://localhost:8000/api/tasks/', {
|
||||
fixture: 'tasks/tasks.json',
|
||||
})
|
||||
})
|
||||
|
||||
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||
@ -48,7 +58,6 @@ describe('settings', () => {
|
||||
|
||||
cy.viewport(1024, 1600)
|
||||
cy.visit('/settings')
|
||||
cy.wait('@savedViews')
|
||||
})
|
||||
|
||||
it('should activate / deactivate save button when settings change and are saved', () => {
|
||||
@ -64,7 +73,7 @@ describe('settings', () => {
|
||||
cy.contains('a', 'Dashboard').click()
|
||||
cy.contains('You have unsaved changes')
|
||||
cy.contains('button', 'Cancel').click()
|
||||
cy.contains('button', 'Save').click().wait('@savedViews')
|
||||
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||
cy.contains('a', 'Dashboard').click()
|
||||
cy.contains('You have unsaved changes').should('not.exist')
|
||||
})
|
||||
@ -77,16 +86,16 @@ describe('settings', () => {
|
||||
})
|
||||
|
||||
it('should remove saved view from sidebar when unset', () => {
|
||||
cy.contains('a', 'Saved views').click()
|
||||
cy.contains('a', 'Saved views').click().wait(2000)
|
||||
cy.get('#show_in_sidebar_1').click()
|
||||
cy.contains('button', 'Save').click().wait('@savedViews')
|
||||
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||
cy.contains('li', 'Inbox').should('not.exist')
|
||||
})
|
||||
|
||||
it('should remove saved view from dashboard when unset', () => {
|
||||
cy.contains('a', 'Saved views').click()
|
||||
cy.get('#show_on_dashboard_1').click()
|
||||
cy.contains('button', 'Save').click().wait('@savedViews')
|
||||
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||
cy.visit('/dashboard')
|
||||
cy.get('app-saved-view-widget').contains('Inbox').should('not.exist')
|
||||
})
|
||||
|
27
src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
Normal file
27
src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "IMAP Server",
|
||||
"imap_server": "imap.example.com",
|
||||
"imap_port": 993,
|
||||
"imap_security": 2,
|
||||
"username": "inbox@example.com",
|
||||
"password": "pass",
|
||||
"character_set": "UTF-8"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Gmail",
|
||||
"imap_server": "imap.gmail.com",
|
||||
"imap_port": 993,
|
||||
"imap_security": 2,
|
||||
"username": "user@gmail.com",
|
||||
"password": "pass",
|
||||
"character_set": "UTF-8"
|
||||
}
|
||||
]
|
||||
}
|
29
src-ui/cypress/fixtures/mail_rules/mail_rules.json
Normal file
29
src-ui/cypress/fixtures/mail_rules/mail_rules.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Gmail",
|
||||
"account": 2,
|
||||
"folder": "INBOX",
|
||||
"filter_from": null,
|
||||
"filter_subject": "[paperless]",
|
||||
"filter_body": null,
|
||||
"filter_attachment_filename": null,
|
||||
"maximum_age": 30,
|
||||
"action": 3,
|
||||
"action_parameter": null,
|
||||
"assign_title_from": 1,
|
||||
"assign_tags": [
|
||||
9
|
||||
],
|
||||
"assign_correspondent_from": 1,
|
||||
"assign_correspondent": 2,
|
||||
"assign_document_type": null,
|
||||
"order": 0,
|
||||
"attachment_type": 2
|
||||
}
|
||||
]
|
||||
}
|
@ -1 +1,44 @@
|
||||
{"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]}
|
||||
{
|
||||
"count": 3,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Inbox",
|
||||
"show_on_dashboard": true,
|
||||
"show_in_sidebar": true,
|
||||
"sort_field": "created",
|
||||
"sort_reverse": true,
|
||||
"filter_rules": [
|
||||
{
|
||||
"rule_type": 6,
|
||||
"value": "18"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Recently Added",
|
||||
"show_on_dashboard": true,
|
||||
"show_in_sidebar": false,
|
||||
"sort_field": "created",
|
||||
"sort_reverse": true,
|
||||
"filter_rules": []
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Taxes",
|
||||
"show_on_dashboard": false,
|
||||
"show_in_sidebar": true,
|
||||
"sort_field": "created",
|
||||
"sort_reverse": true,
|
||||
"filter_rules": [
|
||||
{
|
||||
"rule_type": 6,
|
||||
"value": "39"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
1018
src-ui/messages.xlf
1018
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
3239
src-ui/package-lock.json
generated
3239
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -40,17 +40,23 @@
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "14.1.0",
|
||||
"@angular-devkit/build-angular": "~14.2.7",
|
||||
"@angular-eslint/builder": "14.4.0",
|
||||
"@angular-eslint/eslint-plugin": "14.4.0",
|
||||
"@angular-eslint/eslint-plugin-template": "14.4.0",
|
||||
"@angular-eslint/schematics": "14.4.0",
|
||||
"@angular-eslint/template-parser": "14.4.0",
|
||||
"@angular/cli": "~14.2.7",
|
||||
"@angular/compiler-cli": "~14.2.8",
|
||||
"@types/jest": "28.1.6",
|
||||
"@types/node": "^18.7.23",
|
||||
"codelyzer": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"concurrently": "7.4.0",
|
||||
"eslint": "^8.28.0",
|
||||
"jest": "28.1.3",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"jest-preset-angular": "^12.2.3",
|
||||
"ts-node": "~10.9.1",
|
||||
"tslint": "~6.1.3",
|
||||
"typescript": "~4.8.4",
|
||||
"wait-on": "~6.0.1"
|
||||
},
|
||||
|
@ -47,6 +47,11 @@ const routes: Routes = [
|
||||
component: SettingsComponent,
|
||||
canDeactivate: [DirtyFormGuard],
|
||||
},
|
||||
{
|
||||
path: 'settings/:section',
|
||||
component: SettingsComponent,
|
||||
canDeactivate: [DirtyFormGuard],
|
||||
},
|
||||
{ path: 'tasks', component: TasksComponent },
|
||||
],
|
||||
},
|
||||
|
@ -191,21 +191,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.settings',
|
||||
content: $localize`Check out the settings for various tweaks to the web app or to toggle settings for saved views.`,
|
||||
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
|
||||
route: '/settings',
|
||||
enableBackdrop: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.admin',
|
||||
content: $localize`The Admin area contains more advanced controls as well as the settings for automatic e-mail fetching.`,
|
||||
enableBackdrop: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.outro',
|
||||
title: $localize`Thank you! 🙏`,
|
||||
|
@ -24,7 +24,7 @@ import { CorrespondentEditDialogComponent } from './components/common/edit-dialo
|
||||
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
import { TagComponent } from './components/common/tag/tag.component'
|
||||
import { ClearableBadge } from './components/common/clearable-badge/clearable-badge.component'
|
||||
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
||||
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
||||
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
@ -39,6 +39,7 @@ import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { TextComponent } from './components/common/input/text/text.component'
|
||||
import { SelectComponent } from './components/common/input/select/select.component'
|
||||
import { CheckComponent } from './components/common/input/check/check.component'
|
||||
import { PasswordComponent } from './components/common/input/password/password.component'
|
||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
||||
import { TagsComponent } from './components/common/input/tags/tags.component'
|
||||
import { SortableDirective } from './directives/sortable.directive'
|
||||
@ -76,6 +77,8 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
|
||||
import localeBe from '@angular/common/locales/be'
|
||||
import localeCs from '@angular/common/locales/cs'
|
||||
@ -143,7 +146,7 @@ function initializeApp(settings: SettingsService) {
|
||||
DocumentTypeEditDialogComponent,
|
||||
StoragePathEditDialogComponent,
|
||||
TagComponent,
|
||||
ClearableBadge,
|
||||
ClearableBadgeComponent,
|
||||
PageHeaderComponent,
|
||||
AppFrameComponent,
|
||||
ToastsComponent,
|
||||
@ -157,6 +160,7 @@ function initializeApp(settings: SettingsService) {
|
||||
TextComponent,
|
||||
SelectComponent,
|
||||
CheckComponent,
|
||||
PasswordComponent,
|
||||
SaveViewConfigDialogComponent,
|
||||
TagsComponent,
|
||||
SortableDirective,
|
||||
@ -180,6 +184,8 @@ function initializeApp(settings: SettingsService) {
|
||||
DocumentAsnComponent,
|
||||
DocumentCommentsComponent,
|
||||
TasksComponent,
|
||||
MailAccountEditDialogComponent,
|
||||
MailRuleEditDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -174,13 +174,6 @@
|
||||
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.admin">
|
||||
<a class="nav-link" href="admin/" ngbPopover="Admin" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
||||
</svg><span> <ng-container i18n>Admin</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
|
||||
|
@ -220,6 +220,12 @@ main {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.navbar-brand.slim {
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown.show .dropdown-toggle,
|
||||
.dropdown-toggle:hover {
|
||||
opacity: 0.7;
|
||||
|
@ -5,7 +5,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
templateUrl: './clearable-badge.component.html',
|
||||
styleUrls: ['./clearable-badge.component.scss'],
|
||||
})
|
||||
export class ClearableBadge {
|
||||
export class ClearableBadgeComponent {
|
||||
constructor() {}
|
||||
|
||||
@Input()
|
||||
|
@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-correspondent-edit-dialog',
|
||||
@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||
})
|
||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||
constructor(
|
||||
service: CorrespondentService,
|
||||
activeModal: NgbActiveModal,
|
||||
toastService: ToastService
|
||||
) {
|
||||
super(service, activeModal, toastService)
|
||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-type-edit-dialog',
|
||||
@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||
})
|
||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||
constructor(
|
||||
service: DocumentTypeService,
|
||||
activeModal: NgbActiveModal,
|
||||
toastService: ToastService
|
||||
) {
|
||||
super(service, activeModal, toastService)
|
||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -2,11 +2,9 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
|
||||
@Directive()
|
||||
export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
@ -14,8 +12,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
{
|
||||
constructor(
|
||||
private service: AbstractPaperlessService<T>,
|
||||
private activeModal: NgbActiveModal,
|
||||
private toastService: ToastService
|
||||
private activeModal: NgbActiveModal
|
||||
) {}
|
||||
|
||||
@Input()
|
||||
@ -25,7 +22,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
object: T
|
||||
|
||||
@Output()
|
||||
success = new EventEmitter()
|
||||
succeeded = new EventEmitter()
|
||||
|
||||
networkActive = false
|
||||
|
||||
@ -95,16 +92,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
break
|
||||
}
|
||||
this.networkActive = true
|
||||
serverResponse.subscribe(
|
||||
(result) => {
|
||||
serverResponse.subscribe({
|
||||
next: (result) => {
|
||||
this.activeModal.close()
|
||||
this.success.emit(result)
|
||||
this.succeeded.emit(result)
|
||||
},
|
||||
(error) => {
|
||||
error: (error) => {
|
||||
this.error = error.error
|
||||
this.networkActive = false
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
|
@ -0,0 +1,26 @@
|
||||
<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">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></app-input-text>
|
||||
<app-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></app-input-text>
|
||||
<app-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></app-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||
<app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">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 {
|
||||
IMAPSecurity,
|
||||
PaperlessMailAccount,
|
||||
} from 'src/app/data/paperless-mail-account'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
|
||||
const IMAP_SECURITY_OPTIONS = [
|
||||
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
||||
{ id: IMAPSecurity.SSL, name: $localize`SSL` },
|
||||
{ id: IMAPSecurity.STARTTLS, name: $localize`STARTTLS` },
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'app-mail-account-edit-dialog',
|
||||
templateUrl: './mail-account-edit-dialog.component.html',
|
||||
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||
})
|
||||
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||
constructor(service: MailAccountService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new mail account`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit mail account`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(null),
|
||||
imap_server: new FormControl(null),
|
||||
imap_port: new FormControl(null),
|
||||
imap_security: new FormControl(IMAPSecurity.SSL),
|
||||
username: new FormControl(null),
|
||||
password: new FormControl(null),
|
||||
character_set: new FormControl('UTF-8'),
|
||||
})
|
||||
}
|
||||
|
||||
get imapSecurityOptions() {
|
||||
return IMAP_SECURITY_OPTIONS
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<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">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></app-input-select>
|
||||
<app-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></app-input-text>
|
||||
<app-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></app-input-number>
|
||||
<app-input-select i18n-title title="Attachment type" [items]="attachmentTypeOptions" formControlName="attachment_type"></app-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p>
|
||||
<app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></app-input-select>
|
||||
<app-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></app-input-text>
|
||||
<app-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></app-input-select>
|
||||
<app-input-tags [allowCreate]="false" formControlName="assign_tags"></app-input-tags>
|
||||
<app-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></app-input-select>
|
||||
<app-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></app-input-select>
|
||||
<app-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></app-input-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,180 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import {
|
||||
MailAction,
|
||||
MailFilterAttachmentType,
|
||||
MailMetadataCorrespondentOption,
|
||||
MailMetadataTitleOption,
|
||||
PaperlessMailRule,
|
||||
} from 'src/app/data/paperless-mail-rule'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
|
||||
const ATTACHMENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: MailFilterAttachmentType.Attachments,
|
||||
name: $localize`Only process attachments.`,
|
||||
},
|
||||
{
|
||||
id: MailFilterAttachmentType.Everything,
|
||||
name: $localize`Process all files, including 'inline' attachments.`,
|
||||
},
|
||||
]
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{
|
||||
id: MailAction.Delete,
|
||||
name: $localize`Delete`,
|
||||
},
|
||||
{
|
||||
id: MailAction.Move,
|
||||
name: $localize`Move to specified folder`,
|
||||
},
|
||||
{
|
||||
id: MailAction.MarkRead,
|
||||
name: $localize`Mark as read, don't process read mails`,
|
||||
},
|
||||
{
|
||||
id: MailAction.Flag,
|
||||
name: $localize`Flag the mail, don't process flagged mails`,
|
||||
},
|
||||
{
|
||||
id: MailAction.Tag,
|
||||
name: $localize`Tag the mail with specified tag, don't process tagged mails`,
|
||||
},
|
||||
]
|
||||
|
||||
const METADATA_TITLE_OPTIONS = [
|
||||
{
|
||||
id: MailMetadataTitleOption.FromSubject,
|
||||
name: $localize`Use subject as title`,
|
||||
},
|
||||
{
|
||||
id: MailMetadataTitleOption.FromFilename,
|
||||
name: $localize`Use attachment filename as title`,
|
||||
},
|
||||
]
|
||||
|
||||
const METADATA_CORRESPONDENT_OPTIONS = [
|
||||
{
|
||||
id: MailMetadataCorrespondentOption.FromNothing,
|
||||
name: $localize`Do not assign a correspondent`,
|
||||
},
|
||||
{
|
||||
id: MailMetadataCorrespondentOption.FromEmail,
|
||||
name: $localize`Use mail address`,
|
||||
},
|
||||
{
|
||||
id: MailMetadataCorrespondentOption.FromName,
|
||||
name: $localize`Use name (or mail address if not available)`,
|
||||
},
|
||||
{
|
||||
id: MailMetadataCorrespondentOption.FromCustom,
|
||||
name: $localize`Use correspondent selected below`,
|
||||
},
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'app-mail-rule-edit-dialog',
|
||||
templateUrl: './mail-rule-edit-dialog.component.html',
|
||||
styleUrls: ['./mail-rule-edit-dialog.component.scss'],
|
||||
})
|
||||
export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> {
|
||||
accounts: PaperlessMailAccount[]
|
||||
correspondents: PaperlessCorrespondent[]
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
|
||||
constructor(
|
||||
service: MailRuleService,
|
||||
activeModal: NgbActiveModal,
|
||||
accountService: MailAccountService,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService
|
||||
) {
|
||||
super(service, activeModal)
|
||||
|
||||
accountService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.accounts = result.results))
|
||||
|
||||
correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new mail rule`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit mail rule`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(null),
|
||||
account: new FormControl(null),
|
||||
folder: new FormControl('INBOX'),
|
||||
filter_from: new FormControl(null),
|
||||
filter_subject: new FormControl(null),
|
||||
filter_body: new FormControl(null),
|
||||
filter_attachment_filename: new FormControl(null),
|
||||
maximum_age: new FormControl(null),
|
||||
attachment_type: new FormControl(MailFilterAttachmentType.Attachments),
|
||||
action: new FormControl(MailAction.MarkRead),
|
||||
action_parameter: new FormControl(null),
|
||||
assign_title_from: new FormControl(MailMetadataTitleOption.FromSubject),
|
||||
assign_tags: new FormControl([]),
|
||||
assign_document_type: new FormControl(null),
|
||||
assign_correspondent_from: new FormControl(
|
||||
MailMetadataCorrespondentOption.FromNothing
|
||||
),
|
||||
assign_correspondent: new FormControl(null),
|
||||
})
|
||||
}
|
||||
|
||||
get showCorrespondentField(): boolean {
|
||||
return (
|
||||
this.objectForm?.get('assign_correspondent_from')?.value ==
|
||||
MailMetadataCorrespondentOption.FromCustom
|
||||
)
|
||||
}
|
||||
|
||||
get showActionParamField(): boolean {
|
||||
return (
|
||||
this.objectForm?.get('action')?.value == MailAction.Move ||
|
||||
this.objectForm?.get('action')?.value == MailAction.Tag
|
||||
)
|
||||
}
|
||||
|
||||
get attachmentTypeOptions() {
|
||||
return ATTACHMENT_TYPE_OPTIONS
|
||||
}
|
||||
|
||||
get actionOptions() {
|
||||
return ACTION_OPTIONS
|
||||
}
|
||||
|
||||
get metadataTitleOptions() {
|
||||
return METADATA_TITLE_OPTIONS
|
||||
}
|
||||
|
||||
get metadataCorrespondentOptions() {
|
||||
return METADATA_CORRESPONDENT_OPTIONS
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p *ngIf="this.dialogMode == 'edit'" i18n>
|
||||
<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://docs.paperless-ngx.com/administration/#renamer">documentation</a>.</em>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
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',
|
||||
@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||
})
|
||||
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||
constructor(
|
||||
service: StoragePathService,
|
||||
activeModal: NgbActiveModal,
|
||||
toastService: ToastService
|
||||
) {
|
||||
super(service, activeModal, toastService)
|
||||
constructor(service: StoragePathService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
}
|
||||
|
||||
get pathHint() {
|
||||
|
@ -4,7 +4,6 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { randomColor } from 'src/app/utils/color'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
|
||||
@ -14,12 +13,8 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||
})
|
||||
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
toastService: ToastService
|
||||
) {
|
||||
super(service, activeModal, toastService)
|
||||
constructor(service: TagService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -4,7 +4,7 @@
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
||||
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
||||
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||
</ng-container>
|
||||
</button>
|
||||
|
@ -321,7 +321,7 @@ export class FilterableDropdownComponent {
|
||||
apply = new EventEmitter<ChangedItems>()
|
||||
|
||||
@Output()
|
||||
open = new EventEmitter()
|
||||
opened = new EventEmitter()
|
||||
|
||||
get operatorToggleEnabled(): boolean {
|
||||
return (
|
||||
@ -356,7 +356,7 @@ export class FilterableDropdownComponent {
|
||||
if (this.editing) {
|
||||
this.selectionModel.reset()
|
||||
}
|
||||
this.open.next(this)
|
||||
this.opened.next(this)
|
||||
} else {
|
||||
this.filterText = ''
|
||||
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
|
||||
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
||||
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, forwardRef, Input } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
@ -17,6 +17,9 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
styleUrls: ['./number.component.scss'],
|
||||
})
|
||||
export class NumberComponent extends AbstractInputComponent<number> {
|
||||
@Input()
|
||||
showAdd: boolean = true
|
||||
|
||||
constructor(private documentService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,21 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => PasswordComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'app-input-password',
|
||||
templateUrl: './password.component.html',
|
||||
styleUrls: ['./password.component.scss'],
|
||||
})
|
||||
export class PasswordComponent extends AbstractInputComponent<string> {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
[closeOnSelect]="false"
|
||||
[clearSearchOnAdd]="true"
|
||||
[hideSelected]="true"
|
||||
[addTag]="createTagRef"
|
||||
[addTag]="allowCreate ? createTagRef : false"
|
||||
addTagText="Add tag"
|
||||
i18n-addTagText
|
||||
(change)="onChange(value)"
|
||||
@ -31,7 +31,7 @@
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
||||
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
|
@ -54,6 +54,9 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
@Input()
|
||||
suggestions: number[]
|
||||
|
||||
@Input()
|
||||
allowCreate: boolean = true
|
||||
|
||||
value: number[]
|
||||
|
||||
tags: PaperlessTag[]
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
|
||||
@ -7,7 +7,7 @@ import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
templateUrl: './select-dialog.component.html',
|
||||
styleUrls: ['./select-dialog.component.scss'],
|
||||
})
|
||||
export class SelectDialogComponent implements OnInit {
|
||||
export class SelectDialogComponent {
|
||||
constructor(public activeModal: NgbActiveModal) {}
|
||||
|
||||
@Output()
|
||||
@ -24,8 +24,6 @@ export class SelectDialogComponent implements OnInit {
|
||||
|
||||
selected: number
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
|
||||
@Component({
|
||||
@ -6,7 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
templateUrl: './tag.component.html',
|
||||
styleUrls: ['./tag.component.scss'],
|
||||
})
|
||||
export class TagComponent implements OnInit {
|
||||
export class TagComponent {
|
||||
constructor() {}
|
||||
|
||||
@Input()
|
||||
@ -17,6 +17,4 @@ export class TagComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
clickable: boolean = false
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let doc of documents" (click)="openDocumentsService.openDocument(doc)">
|
||||
<td>{{doc.created_date | 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 *ngFor="let doc of documents">
|
||||
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
|
||||
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></app-tag></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -7,6 +7,6 @@ th:first-child {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
tbody app-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -72,7 +72,9 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
clickTag(tag: PaperlessTag) {
|
||||
clickTag(tag: PaperlessTag, event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
||||
])
|
||||
|
@ -1,6 +1,6 @@
|
||||
<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 *ngIf="statistics?.documents_inbox !== null">Documents in inbox: {{statistics?.documents_inbox}}</p>
|
||||
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
|
||||
</ng-container>
|
||||
</app-widget-frame>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { HttpEventType } from '@angular/common/http'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { Component } from '@angular/core'
|
||||
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
@ -15,7 +14,7 @@ const MAX_ALERTS = 5
|
||||
templateUrl: './upload-file-widget.component.html',
|
||||
styleUrls: ['./upload-file-widget.component.scss'],
|
||||
})
|
||||
export class UploadFileWidgetComponent implements OnInit {
|
||||
export class UploadFileWidgetComponent {
|
||||
alertsExpanded = false
|
||||
|
||||
constructor(
|
||||
@ -109,8 +108,6 @@ export class UploadFileWidgetComponent implements OnInit {
|
||||
this.consumerStatusService.dismissCompleted()
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
public fileOver(event) {}
|
||||
|
||||
public fileLeave(event) {}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component } from '@angular/core'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
@ -6,8 +6,6 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
templateUrl: './welcome-widget.component.html',
|
||||
styleUrls: ['./welcome-widget.component.scss'],
|
||||
})
|
||||
export class WelcomeWidgetComponent implements OnInit {
|
||||
export class WelcomeWidgetComponent {
|
||||
constructor(public readonly tourService: TourService) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-widget-frame',
|
||||
templateUrl: './widget-frame.component.html',
|
||||
styleUrls: ['./widget-frame.component.scss'],
|
||||
})
|
||||
export class WidgetFrameComponent implements OnInit {
|
||||
export class WidgetFrameComponent {
|
||||
constructor() {}
|
||||
|
||||
@Input()
|
||||
@ -13,6 +13,4 @@ export class WidgetFrameComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
loading: boolean = false
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<app-page-header [(title)]="title">
|
||||
<div class="input-group input-group-sm me-5 d-none d-md-flex" *ngIf="getContentType() == 'application/pdf' && !useNativePdfViewer">
|
||||
<div class="input-group input-group-sm me-5 d-none d-md-flex" *ngIf="getContentType() === 'application/pdf' && !useNativePdfViewer">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||
@ -149,9 +149,9 @@
|
||||
|
||||
<li [ngbNavItem]="4" class="d-md-none">
|
||||
<a ngbNavLink>Preview</a>
|
||||
<ng-template ngbNavContent *ngIf="pdfPreview.offsetParent == undefined">
|
||||
<ng-template ngbNavContent *ngIf="pdfPreview.offsetParent === undefined">
|
||||
<div class="position-relative">
|
||||
<ng-container *ngIf="getContentType() == 'application/pdf'">
|
||||
<ng-container *ngIf="getContentType() === 'application/pdf'">
|
||||
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
|
||||
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||
</div>
|
||||
@ -159,7 +159,7 @@
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="getContentType() == 'text/plain'">
|
||||
<ng-container *ngIf="getContentType() === 'text/plain'">
|
||||
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
|
||||
</ng-container>
|
||||
<div *ngIf="requiresPassword" class="password-prompt">
|
||||
@ -180,14 +180,14 @@
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || !(isDirty$ | async)">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save & next</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || (isDirty$ | async) === false">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || (isDirty$ | async) === false || error">Save & next</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || (isDirty$ | async) === false || error">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
|
||||
<ng-container *ngIf="getContentType() == 'application/pdf'">
|
||||
<ng-container *ngIf="getContentType() === 'application/pdf'">
|
||||
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
|
||||
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||
</div>
|
||||
@ -195,7 +195,7 @@
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="getContentType() == 'text/plain'">
|
||||
<ng-container *ngIf="getContentType() === 'text/plain'">
|
||||
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
|
||||
</ng-container>
|
||||
<div *ngIf="requiresPassword" class="password-prompt">
|
||||
|
@ -184,7 +184,7 @@ export class DocumentDetailComponent
|
||||
this.openDocumentService.getOpenDocument(this.documentId)
|
||||
)
|
||||
} else {
|
||||
this.openDocumentService.openDocument(doc, false)
|
||||
this.openDocumentService.openDocument(doc)
|
||||
this.updateComponent(doc)
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-collapse',
|
||||
templateUrl: './metadata-collapse.component.html',
|
||||
styleUrls: ['./metadata-collapse.component.scss'],
|
||||
})
|
||||
export class MetadataCollapseComponent implements OnInit {
|
||||
export class MetadataCollapseComponent {
|
||||
constructor() {}
|
||||
|
||||
expand = false
|
||||
@ -15,6 +15,4 @@ export class MetadataCollapseComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
title = $localize`Metadata`
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
@ -66,7 +66,6 @@
|
||||
</div>
|
||||
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
|
||||
<div ngbDropdown class="me-2 d-flex">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
@ -75,26 +74,57 @@
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected()" i18n>
|
||||
Download
|
||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Preparing download...</span>
|
||||
</div>
|
||||
</button>
|
||||
<button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected('originals')" i18n>
|
||||
Download originals
|
||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Preparing download...</span>
|
||||
</div>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
|
||||
<svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
|
||||
</svg>
|
||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Preparing download...</span>
|
||||
</div>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Download</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<form [formGroup]="downloadForm" class="px-3 py-1">
|
||||
<p class="mb-1" i18n>Include:</p>
|
||||
<div class="form-group ps-3 mb-2">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
|
||||
<label class="form-check-label" for="downloadFileType_archive" i18n>
|
||||
Archived files
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
|
||||
<label class="form-check-label" for="downloadFileType_originals" i18n>
|
||||
Original files
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
|
||||
<label class="form-check-label" for="downloadUseFormatting" i18n>
|
||||
Use formatted filename
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,7 @@
|
||||
.dropdown-toggle-split {
|
||||
--bs-border-radius: .25rem;
|
||||
}
|
||||
|
||||
.dropdown-menu{
|
||||
--bs-dropdown-min-width: 12rem;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
@ -25,13 +25,15 @@ 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'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { first, Subject, takeUntil } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-editor',
|
||||
templateUrl: './bulk-editor.component.html',
|
||||
styleUrls: ['./bulk-editor.component.scss'],
|
||||
})
|
||||
export class BulkEditorComponent {
|
||||
export class BulkEditorComponent implements OnInit, OnDestroy {
|
||||
tags: PaperlessTag[]
|
||||
correspondents: PaperlessCorrespondent[]
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
@ -43,6 +45,14 @@ export class BulkEditorComponent {
|
||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
awaitingDownload: boolean
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
downloadForm = new FormGroup({
|
||||
downloadFileTypeArchive: new FormControl(true),
|
||||
downloadFileTypeOriginals: new FormControl(false),
|
||||
downloadUseFormatting: new FormControl(false),
|
||||
})
|
||||
|
||||
constructor(
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService,
|
||||
@ -66,16 +76,46 @@ export class BulkEditorComponent {
|
||||
ngOnInit() {
|
||||
this.tagService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.tags = result.results))
|
||||
this.correspondentService
|
||||
.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.downloadForm
|
||||
.get('downloadFileTypeArchive')
|
||||
.valueChanges.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((newValue) => {
|
||||
if (!newValue) {
|
||||
this.downloadForm
|
||||
.get('downloadFileTypeOriginals')
|
||||
.patchValue(true, { emitEvent: false })
|
||||
}
|
||||
})
|
||||
this.downloadForm
|
||||
.get('downloadFileTypeOriginals')
|
||||
.valueChanges.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((newValue) => {
|
||||
if (!newValue) {
|
||||
this.downloadForm
|
||||
.get('downloadFileTypeArchive')
|
||||
.patchValue(true, { emitEvent: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(this)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
private executeBulkOperation(modal, method: string, args) {
|
||||
@ -84,8 +124,9 @@ export class BulkEditorComponent {
|
||||
}
|
||||
this.documentService
|
||||
.bulkEdit(Array.from(this.list.selected), method, args)
|
||||
.subscribe(
|
||||
(response) => {
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.list.reload()
|
||||
this.list.reduceSelectionToFilter()
|
||||
this.list.selected.forEach((id) => {
|
||||
@ -95,7 +136,7 @@ export class BulkEditorComponent {
|
||||
modal.close()
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
error: (error) => {
|
||||
if (modal) {
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
}
|
||||
@ -104,8 +145,8 @@ export class BulkEditorComponent {
|
||||
error.error
|
||||
)}`
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private applySelectionData(
|
||||
@ -126,6 +167,7 @@ export class BulkEditorComponent {
|
||||
openTagsDropdown() {
|
||||
this.documentService
|
||||
.getSelectionData(Array.from(this.list.selected))
|
||||
.pipe(first())
|
||||
.subscribe((s) => {
|
||||
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
||||
})
|
||||
@ -134,6 +176,7 @@ export class BulkEditorComponent {
|
||||
openDocumentTypeDropdown() {
|
||||
this.documentService
|
||||
.getSelectionData(Array.from(this.list.selected))
|
||||
.pipe(first())
|
||||
.subscribe((s) => {
|
||||
this.applySelectionData(
|
||||
s.selected_document_types,
|
||||
@ -145,6 +188,7 @@ export class BulkEditorComponent {
|
||||
openCorrespondentDropdown() {
|
||||
this.documentService
|
||||
.getSelectionData(Array.from(this.list.selected))
|
||||
.pipe(first())
|
||||
.subscribe((s) => {
|
||||
this.applySelectionData(
|
||||
s.selected_correspondents,
|
||||
@ -156,6 +200,7 @@ export class BulkEditorComponent {
|
||||
openStoragePathDropdown() {
|
||||
this.documentService
|
||||
.getSelectionData(Array.from(this.list.selected))
|
||||
.pipe(first())
|
||||
.subscribe((s) => {
|
||||
this.applySelectionData(
|
||||
s.selected_storage_paths,
|
||||
@ -232,12 +277,14 @@ export class BulkEditorComponent {
|
||||
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'modify_tags', {
|
||||
add_tags: changedTags.itemsToAdd.map((t) => t.id),
|
||||
remove_tags: changedTags.itemsToRemove.map((t) => t.id),
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'modify_tags', {
|
||||
add_tags: changedTags.itemsToAdd.map((t) => t.id),
|
||||
remove_tags: changedTags.itemsToRemove.map((t) => t.id),
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'modify_tags', {
|
||||
add_tags: changedTags.itemsToAdd.map((t) => t.id),
|
||||
@ -270,11 +317,13 @@ export class BulkEditorComponent {
|
||||
}
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_correspondent', {
|
||||
correspondent: correspondent ? correspondent.id : null,
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_correspondent', {
|
||||
correspondent: correspondent ? correspondent.id : null,
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'set_correspondent', {
|
||||
correspondent: correspondent ? correspondent.id : null,
|
||||
@ -306,11 +355,13 @@ export class BulkEditorComponent {
|
||||
}
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_document_type', {
|
||||
document_type: documentType ? documentType.id : null,
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_document_type', {
|
||||
document_type: documentType ? documentType.id : null,
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'set_document_type', {
|
||||
document_type: documentType ? documentType.id : null,
|
||||
@ -342,11 +393,13 @@ export class BulkEditorComponent {
|
||||
}
|
||||
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,
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.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,
|
||||
@ -364,16 +417,30 @@ export class BulkEditorComponent {
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Delete document(s)`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'delete', {})
|
||||
})
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'delete', {})
|
||||
})
|
||||
}
|
||||
|
||||
downloadSelected(content = 'archive') {
|
||||
downloadSelected() {
|
||||
this.awaitingDownload = true
|
||||
let downloadFileType: string =
|
||||
this.downloadForm.get('downloadFileTypeArchive').value &&
|
||||
this.downloadForm.get('downloadFileTypeOriginals').value
|
||||
? 'both'
|
||||
: this.downloadForm.get('downloadFileTypeArchive').value
|
||||
? 'archive'
|
||||
: 'originals'
|
||||
this.documentService
|
||||
.bulkDownload(Array.from(this.list.selected), content)
|
||||
.bulkDownload(
|
||||
Array.from(this.list.selected),
|
||||
downloadFileType,
|
||||
this.downloadForm.get('downloadUseFormatting').value
|
||||
)
|
||||
.pipe(first())
|
||||
.subscribe((result: any) => {
|
||||
saveAs(result, 'documents.zip')
|
||||
this.awaitingDownload = false
|
||||
@ -389,9 +456,11 @@ export class BulkEditorComponent {
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'redo_ocr', {})
|
||||
})
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'redo_ocr', {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
||||
</a>
|
||||
<a (click)="openDocumentsService.openDocument(document)" class="btn btn-sm btn-outline-secondary">
|
||||
<a routerLink="/documents/{{document.id}}" 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>
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
@ -10,9 +9,6 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.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({
|
||||
@ -23,11 +19,10 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
'../popover-preview/popover-preview.scss',
|
||||
],
|
||||
})
|
||||
export class DocumentCardLargeComponent implements OnInit {
|
||||
export class DocumentCardLargeComponent {
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private settingsService: SettingsService,
|
||||
public openDocumentsService: OpenDocumentsService
|
||||
private settingsService: SettingsService
|
||||
) {}
|
||||
|
||||
@Input()
|
||||
@ -75,8 +70,6 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
getIsThumbInverted() {
|
||||
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
||||
}
|
||||
@ -119,6 +112,9 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
}
|
||||
|
||||
get contentTrimmed() {
|
||||
return this.document.content.substr(0, 500)
|
||||
return (
|
||||
this.document.content.substr(0, 500) +
|
||||
(this.document.content.length > 500 ? '...' : '')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group w-100">
|
||||
<a (click)="openDocumentsService.openDocument(document)" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
||||
<a routerLink="/documents/{{document.id}}" 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>
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
@ -11,7 +10,6 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.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({
|
||||
@ -22,11 +20,10 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
'../popover-preview/popover-preview.scss',
|
||||
],
|
||||
})
|
||||
export class DocumentCardSmallComponent implements OnInit {
|
||||
export class DocumentCardSmallComponent {
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private settingsService: SettingsService,
|
||||
public openDocumentsService: OpenDocumentsService
|
||||
private settingsService: SettingsService
|
||||
) {}
|
||||
|
||||
@Input()
|
||||
@ -57,8 +54,6 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
mouseOnPreview = false
|
||||
popoverHidden = true
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
getIsThumbInverted() {
|
||||
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)"
|
||||
[class.active]="list.sortField == f.field">{{f.name}}
|
||||
[class.active]="list.sortField === f.field">{{f.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,7 +94,7 @@
|
||||
</ng-container>
|
||||
<span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
|
||||
<ng-container *ngIf="!list.isReloading">
|
||||
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
||||
<span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||
@ -111,52 +111,52 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-template #documentListNoError>
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<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)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode == 'details'">
|
||||
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode === 'details'">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th class="d-none d-lg-table-cell"
|
||||
sortable="archive_serial_number"
|
||||
appSortable="archive_serial_number"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>ASN</th>
|
||||
<th class="d-none d-md-table-cell"
|
||||
sortable="correspondent__name"
|
||||
appSortable="correspondent__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Correspondent</th>
|
||||
<th
|
||||
sortable="title"
|
||||
appSortable="title"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Title</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="document_type__name"
|
||||
appSortable="document_type__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Document type</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="storage_path__name"
|
||||
appSortable="storage_path__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Storage path</th>
|
||||
<th
|
||||
sortable="created"
|
||||
appSortable="created"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Created</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="added"
|
||||
appSortable="added"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
@ -179,7 +179,7 @@
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<a (click)="openDocumentsService.openDocument(d)" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
<a routerLink="/documents/{{d.id}}" 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">
|
||||
@ -202,7 +202,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<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)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
||||
</div>
|
||||
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
||||
|
@ -5,10 +5,10 @@
|
||||
<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>
|
||||
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<select *ngIf="textFilterTarget == 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()">
|
||||
<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>
|
||||
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
|
||||
@ -16,7 +16,7 @@
|
||||
<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>
|
||||
</button>
|
||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget == 'fulltext-morelike'">
|
||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,10 +16,10 @@
|
||||
<table class="table table-striped align-middle border shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</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" appSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||
<th scope="col" class="d-none d-sm-table-cell" appSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||
<th scope="col" appSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||
<th scope="col" *ngFor="let column of extraColumns" appSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||
<th scope="col" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -120,8 +120,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
backdrop: 'static',
|
||||
})
|
||||
activeModal.componentInstance.dialogMode = 'create'
|
||||
activeModal.componentInstance.success.subscribe((o) => {
|
||||
this.reloadData()
|
||||
activeModal.componentInstance.success.subscribe({
|
||||
next: () => {
|
||||
this.reloadData()
|
||||
this.toastService.showInfo(
|
||||
$localize`Successfully created ${this.typeName}.`
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Error occurred while creating ${
|
||||
this.typeName
|
||||
} : ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -131,8 +143,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
})
|
||||
activeModal.componentInstance.object = object
|
||||
activeModal.componentInstance.dialogMode = 'edit'
|
||||
activeModal.componentInstance.success.subscribe((o) => {
|
||||
this.reloadData()
|
||||
activeModal.componentInstance.success.subscribe({
|
||||
next: () => {
|
||||
this.reloadData()
|
||||
this.toastService.showInfo(
|
||||
$localize`Successfully updated ${this.typeName}.`
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Error occurred while saving ${
|
||||
this.typeName
|
||||
} : ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
<app-page-header title="Settings" i18n-title>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||
<a class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<svg class="sidebaricon ms-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
||||
</svg>
|
||||
</a>
|
||||
</app-page-header>
|
||||
|
||||
<!-- <p>items per page, documents per view type</p> -->
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
||||
|
||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||
<li [ngbNavItem]="1">
|
||||
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs">
|
||||
<li [ngbNavItem]="SettingsNavIDs.General">
|
||||
<a ngbNavLink i18n>General</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
@ -19,7 +24,7 @@
|
||||
<div class="col">
|
||||
|
||||
<select class="form-select" formControlName="displayLanguage">
|
||||
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale != 'en-US'"> - {{lang.englishName}}</span></option>
|
||||
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale !== 'en-US'"> - {{lang.englishName}}</span></option>
|
||||
</select>
|
||||
|
||||
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
||||
@ -162,7 +167,7 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="2">
|
||||
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
@ -180,7 +185,7 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="3">
|
||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews" (mouseover)="maybeInitializeTab(SettingsNavIDs.SavedViews)" (focusin)="maybeInitializeTab(SettingsNavIDs.SavedViews)">
|
||||
<a ngbNavLink i18n>Saved views</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
@ -210,8 +215,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="savedViews.length == 0" i18n>No saved views defined.</div>
|
||||
<div *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div>
|
||||
|
||||
<div *ngIf="!savedViews">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
||||
<a ngbNavLink i18n>Mail</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<ng-container *ngIf="mailAccounts && mailRules">
|
||||
<h4>
|
||||
<ng-container i18n>Mail accounts</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Account</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailAccounts">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Server</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||
</ul>
|
||||
|
||||
<h4 class="mt-4">
|
||||
<ng-container i18n>Mail rules</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Rule</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailRules">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Account</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!mailAccounts || !mailRules">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
@ -220,5 +314,5 @@
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="!(isDirty$ | async)" i18n>Save</button>
|
||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||
</form>
|
||||
|
@ -26,9 +26,26 @@ import {
|
||||
Subject,
|
||||
} from 'rxjs'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ViewportScroller } from '@angular/common'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
enum SettingsNavIDs {
|
||||
General = 1,
|
||||
Notifications = 2,
|
||||
SavedViews = 3,
|
||||
Mail = 4,
|
||||
UsersGroups = 5,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@ -38,8 +55,14 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
export class SettingsComponent
|
||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
SettingsNavIDs = SettingsNavIDs
|
||||
activeNavID: number
|
||||
|
||||
savedViewGroup = new FormGroup({})
|
||||
|
||||
mailAccountGroup = new FormGroup({})
|
||||
mailRuleGroup = new FormGroup({})
|
||||
|
||||
settingsForm = new FormGroup({
|
||||
bulkEditConfirmationDialogs: new FormControl(null),
|
||||
bulkEditApplyOnClose: new FormControl(null),
|
||||
@ -50,20 +73,28 @@ export class SettingsComponent
|
||||
darkModeInvertThumbs: new FormControl(null),
|
||||
themeColor: new FormControl(null),
|
||||
useNativePdfViewer: new FormControl(null),
|
||||
savedViews: this.savedViewGroup,
|
||||
displayLanguage: new FormControl(null),
|
||||
dateLocale: new FormControl(null),
|
||||
dateFormat: new FormControl(null),
|
||||
commentsEnabled: new FormControl(null),
|
||||
updateCheckingEnabled: new FormControl(null),
|
||||
|
||||
notificationsConsumerNewDocument: new FormControl(null),
|
||||
notificationsConsumerSuccess: new FormControl(null),
|
||||
notificationsConsumerFailed: new FormControl(null),
|
||||
notificationsConsumerSuppressOnDashboard: new FormControl(null),
|
||||
commentsEnabled: new FormControl(null),
|
||||
updateCheckingEnabled: new FormControl(null),
|
||||
|
||||
savedViews: this.savedViewGroup,
|
||||
|
||||
mailAccounts: this.mailAccountGroup,
|
||||
mailRules: this.mailRuleGroup,
|
||||
})
|
||||
|
||||
savedViews: PaperlessSavedView[]
|
||||
|
||||
mailAccounts: PaperlessMailAccount[]
|
||||
mailRules: PaperlessMailRule[]
|
||||
|
||||
store: BehaviorSubject<any>
|
||||
storeSub: Subscription
|
||||
isDirty$: Observable<boolean>
|
||||
@ -81,19 +112,40 @@ export class SettingsComponent
|
||||
|
||||
constructor(
|
||||
public savedViewService: SavedViewService,
|
||||
public mailAccountService: MailAccountService,
|
||||
public mailRuleService: MailRuleService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService,
|
||||
@Inject(LOCALE_ID) public currentLocale: string,
|
||||
private viewportScroller: ViewportScroller,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
public readonly tourService: TourService
|
||||
private router: Router,
|
||||
public readonly tourService: TourService,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
this.settings.settingsSaved.subscribe(() => {
|
||||
if (!this.savePending) this.initialize()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialize()
|
||||
|
||||
this.activatedRoute.paramMap.subscribe((paramMap) => {
|
||||
const section = paramMap.get('section')
|
||||
if (section) {
|
||||
const navIDKey: string = Object.keys(SettingsNavIDs).find(
|
||||
(navID) => navID.toLowerCase() == section
|
||||
)
|
||||
if (navIDKey) {
|
||||
this.activeNavID = SettingsNavIDs[navIDKey]
|
||||
this.maybeInitializeTab(this.activeNavID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.activatedRoute.snapshot.fragment) {
|
||||
this.viewportScroller.scrollToAnchor(
|
||||
@ -123,10 +175,13 @@ export class SettingsComponent
|
||||
useNativePdfViewer: this.settings.get(
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
|
||||
),
|
||||
savedViews: {},
|
||||
displayLanguage: this.settings.getLanguage(),
|
||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||
commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
|
||||
updateCheckingEnabled: this.settings.get(
|
||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
|
||||
),
|
||||
notificationsConsumerNewDocument: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
|
||||
),
|
||||
@ -139,41 +194,147 @@ export class SettingsComponent
|
||||
notificationsConsumerSuppressOnDashboard: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
|
||||
),
|
||||
commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
|
||||
updateCheckingEnabled: this.settings.get(
|
||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
|
||||
),
|
||||
savedViews: {},
|
||||
mailAccounts: {},
|
||||
mailRules: {},
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.savedViewService.listAll().subscribe((r) => {
|
||||
this.savedViews = r.results
|
||||
this.initialize()
|
||||
})
|
||||
onNavChange(navChangeEvent: NgbNavChangeEvent) {
|
||||
this.maybeInitializeTab(navChangeEvent.nextId)
|
||||
const [foundNavIDkey, foundNavIDValue] = Object.entries(
|
||||
SettingsNavIDs
|
||||
).find(([navIDkey, navIDValue]) => navIDValue == navChangeEvent.nextId)
|
||||
if (foundNavIDkey)
|
||||
// if its dirty we need to wait for confirmation
|
||||
this.router
|
||||
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
||||
.then((navigated) => {
|
||||
if (!navigated && this.isDirty) {
|
||||
this.activeNavID = navChangeEvent.activeId
|
||||
} else if (navigated && this.isDirty) {
|
||||
this.initialize()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Load tab contents 'on demand', either on mouseover or focusin (i.e. before click) or called from nav change event
|
||||
maybeInitializeTab(navID: number): void {
|
||||
if (navID == SettingsNavIDs.SavedViews && !this.savedViews) {
|
||||
this.savedViewService.listAll().subscribe((r) => {
|
||||
this.savedViews = r.results
|
||||
this.initialize(false)
|
||||
})
|
||||
} else if (
|
||||
navID == SettingsNavIDs.Mail &&
|
||||
(!this.mailAccounts || !this.mailRules)
|
||||
) {
|
||||
this.mailAccountService.listAll().subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
|
||||
this.mailRuleService.listAll().subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
this.initialize(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
initialize(resetSettings: boolean = true) {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
|
||||
const currentFormValue = this.settingsForm.value
|
||||
|
||||
let storeData = this.getCurrentSettings()
|
||||
|
||||
for (let view of this.savedViews) {
|
||||
storeData.savedViews[view.id.toString()] = {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
show_on_dashboard: view.show_on_dashboard,
|
||||
show_in_sidebar: view.show_in_sidebar,
|
||||
if (this.savedViews) {
|
||||
for (let view of this.savedViews) {
|
||||
storeData.savedViews[view.id.toString()] = {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
show_on_dashboard: view.show_on_dashboard,
|
||||
show_in_sidebar: view.show_in_sidebar,
|
||||
}
|
||||
this.savedViewGroup.addControl(
|
||||
view.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
name: new FormControl(null),
|
||||
show_on_dashboard: new FormControl(null),
|
||||
show_in_sidebar: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mailAccounts && this.mailRules) {
|
||||
for (let account of this.mailAccounts) {
|
||||
storeData.mailAccounts[account.id.toString()] = {
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
imap_server: account.imap_server,
|
||||
imap_port: account.imap_port,
|
||||
imap_security: account.imap_security,
|
||||
username: account.username,
|
||||
password: account.password,
|
||||
character_set: account.character_set,
|
||||
}
|
||||
this.mailAccountGroup.addControl(
|
||||
account.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
name: new FormControl(null),
|
||||
imap_server: new FormControl(null),
|
||||
imap_port: new FormControl(null),
|
||||
imap_security: new FormControl(null),
|
||||
username: new FormControl(null),
|
||||
password: new FormControl(null),
|
||||
character_set: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (let rule of this.mailRules) {
|
||||
storeData.mailRules[rule.id.toString()] = {
|
||||
name: rule.name,
|
||||
account: rule.account,
|
||||
folder: rule.folder,
|
||||
filter_from: rule.filter_from,
|
||||
filter_subject: rule.filter_subject,
|
||||
filter_body: rule.filter_body,
|
||||
filter_attachment_filename: rule.filter_attachment_filename,
|
||||
maximum_age: rule.maximum_age,
|
||||
attachment_type: rule.attachment_type,
|
||||
action: rule.action,
|
||||
action_parameter: rule.action_parameter,
|
||||
assign_title_from: rule.assign_title_from,
|
||||
assign_tags: rule.assign_tags,
|
||||
assign_document_type: rule.assign_document_type,
|
||||
assign_correspondent_from: rule.assign_correspondent_from,
|
||||
assign_correspondent: rule.assign_correspondent,
|
||||
}
|
||||
this.mailRuleGroup.addControl(
|
||||
rule.id.toString(),
|
||||
new FormGroup({
|
||||
name: new FormControl(null),
|
||||
account: new FormControl(null),
|
||||
folder: new FormControl(null),
|
||||
filter_from: new FormControl(null),
|
||||
filter_subject: new FormControl(null),
|
||||
filter_body: new FormControl(null),
|
||||
filter_attachment_filename: new FormControl(null),
|
||||
maximum_age: new FormControl(null),
|
||||
attachment_type: new FormControl(null),
|
||||
action: new FormControl(null),
|
||||
action_parameter: new FormControl(null),
|
||||
assign_title_from: new FormControl(null),
|
||||
assign_tags: new FormControl(null),
|
||||
assign_document_type: new FormControl(null),
|
||||
assign_correspondent_from: new FormControl(null),
|
||||
assign_correspondent: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
this.savedViewGroup.addControl(
|
||||
view.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
name: new FormControl(null),
|
||||
show_on_dashboard: new FormControl(null),
|
||||
show_in_sidebar: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.store = new BehaviorSubject(storeData)
|
||||
@ -202,6 +363,11 @@ export class SettingsComponent
|
||||
this.settingsForm.get('themeColor').value
|
||||
)
|
||||
})
|
||||
|
||||
if (!resetSettings && currentFormValue) {
|
||||
// prevents loss of unsaved changes
|
||||
this.settingsForm.patchValue(currentFormValue)
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -372,4 +538,121 @@ export class SettingsComponent
|
||||
clearThemeColor() {
|
||||
this.settingsForm.get('themeColor').patchValue('')
|
||||
}
|
||||
|
||||
editMailAccount(account: PaperlessMailAccount) {
|
||||
const modal = this.modalService.open(MailAccountEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = account ? 'edit' : 'create'
|
||||
modal.componentInstance.object = account
|
||||
modal.componentInstance.success
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (newMailAccount) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved account "${newMailAccount.name}".`
|
||||
)
|
||||
this.mailAccountService.clearCache()
|
||||
this.mailAccountService.listAll().subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error saving account: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deleteMailAccount(account: PaperlessMailAccount) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete mail account`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this mail account.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.mailAccountService.delete(account).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail account`)
|
||||
this.mailAccountService.clearCache()
|
||||
this.mailAccountService.listAll().subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail account: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
editMailRule(rule: PaperlessMailRule) {
|
||||
const modal = this.modalService.open(MailRuleEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = rule ? 'edit' : 'create'
|
||||
modal.componentInstance.object = rule
|
||||
modal.componentInstance.success
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (newMailRule) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved rule "${newMailRule.name}".`
|
||||
)
|
||||
this.mailRuleService.clearCache()
|
||||
this.mailRuleService.listAll().subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error saving rule: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deleteMailRule(rule: PaperlessMailRule) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete mail rule`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this mail rule.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.mailRuleService.delete(rule).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail rule`)
|
||||
this.mailRuleService.clearCache()
|
||||
this.mailRuleService.listAll().subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail rule: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<app-page-header title="File Tasks" i18n-title>
|
||||
<div class="btn-toolbar col col-md-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size == 0">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" [disabled]="tasksService.total == 0">
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" [disabled]="tasksService.total === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
||||
@ -33,13 +33,13 @@
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length == 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-tasks"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" i18n>Name</th>
|
||||
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
|
||||
<th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab != 'started' && activeTab != 'queued'" i18n>Results</th>
|
||||
<th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'" i18n>Results</th>
|
||||
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
|
||||
<th scope="col" i18n>Actions</th>
|
||||
</tr>
|
||||
@ -55,7 +55,7 @@
|
||||
</th>
|
||||
<td class="overflow-auto">{{ task.task_file_name }}</td>
|
||||
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
|
||||
<td class="d-none d-lg-table-cell" *ngIf="activeTab != 'started' && activeTab != 'queued'">
|
||||
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'">
|
||||
<div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();"
|
||||
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
|
||||
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span>
|
||||
@ -89,7 +89,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-0" [class.border-0]="expandedTask != task.id" colspan="5">
|
||||
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
|
||||
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-not-found',
|
||||
templateUrl: './not-found.component.html',
|
||||
styleUrls: ['./not-found.component.scss'],
|
||||
})
|
||||
export class NotFoundComponent implements OnInit {
|
||||
export class NotFoundComponent {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
23
src-ui/src/app/data/paperless-mail-account.ts
Normal file
23
src-ui/src/app/data/paperless-mail-account.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
export enum IMAPSecurity {
|
||||
None = 1,
|
||||
SSL = 2,
|
||||
STARTTLS = 3,
|
||||
}
|
||||
|
||||
export interface PaperlessMailAccount extends ObjectWithId {
|
||||
name: string
|
||||
|
||||
imap_server: string
|
||||
|
||||
imap_port: number
|
||||
|
||||
imap_security: IMAPSecurity
|
||||
|
||||
username: string
|
||||
|
||||
password: string
|
||||
|
||||
character_set?: string
|
||||
}
|
60
src-ui/src/app/data/paperless-mail-rule.ts
Normal file
60
src-ui/src/app/data/paperless-mail-rule.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
export enum MailFilterAttachmentType {
|
||||
Attachments = 1,
|
||||
Everything = 2,
|
||||
}
|
||||
|
||||
export enum MailAction {
|
||||
Delete = 1,
|
||||
Move = 2,
|
||||
MarkRead = 3,
|
||||
Flag = 4,
|
||||
Tag = 5,
|
||||
}
|
||||
|
||||
export enum MailMetadataTitleOption {
|
||||
FromSubject = 1,
|
||||
FromFilename = 2,
|
||||
}
|
||||
|
||||
export enum MailMetadataCorrespondentOption {
|
||||
FromNothing = 1,
|
||||
FromEmail = 2,
|
||||
FromName = 3,
|
||||
FromCustom = 4,
|
||||
}
|
||||
|
||||
export interface PaperlessMailRule extends ObjectWithId {
|
||||
name: string
|
||||
|
||||
account: number // PaperlessMailAccount.id
|
||||
|
||||
folder: string
|
||||
|
||||
filter_from: string
|
||||
|
||||
filter_subject: string
|
||||
|
||||
filter_body: string
|
||||
|
||||
filter_attachment_filename: string
|
||||
|
||||
maximum_age: number
|
||||
|
||||
attachment_type: MailFilterAttachmentType
|
||||
|
||||
action: MailAction
|
||||
|
||||
action_parameter?: string
|
||||
|
||||
assign_title_from: MailMetadataTitleOption
|
||||
|
||||
assign_tags?: number[] // PaperlessTag.id
|
||||
|
||||
assign_document_type?: number // PaperlessDocumentType.id
|
||||
|
||||
assign_correspondent_from?: MailMetadataCorrespondentOption
|
||||
|
||||
assign_correspondent?: number // PaperlessCorrespondent.id
|
||||
}
|
@ -1,4 +1,11 @@
|
||||
import { Directive, EventEmitter, Input, Output } from '@angular/core'
|
||||
import {
|
||||
Directive,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
|
||||
export interface SortEvent {
|
||||
column: string
|
||||
@ -6,18 +13,13 @@ export interface SortEvent {
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: 'th[sortable]',
|
||||
host: {
|
||||
'[class.asc]': 'currentSortField == sortable && !currentSortReverse',
|
||||
'[class.des]': 'currentSortField == sortable && currentSortReverse',
|
||||
'(click)': 'rotate()',
|
||||
},
|
||||
selector: 'th[appSortable]',
|
||||
})
|
||||
export class SortableDirective {
|
||||
constructor() {}
|
||||
|
||||
@Input()
|
||||
sortable: string = ''
|
||||
appSortable: string = ''
|
||||
|
||||
@Input()
|
||||
currentSortReverse: boolean = false
|
||||
@ -27,11 +29,20 @@ export class SortableDirective {
|
||||
|
||||
@Output() sort = new EventEmitter<SortEvent>()
|
||||
|
||||
rotate() {
|
||||
if (this.currentSortField != this.sortable) {
|
||||
this.sort.emit({ column: this.sortable, reverse: false })
|
||||
@HostBinding('class.asc') get asc() {
|
||||
return (
|
||||
this.currentSortField === this.appSortable && !this.currentSortReverse
|
||||
)
|
||||
}
|
||||
@HostBinding('class.des') get des() {
|
||||
return this.currentSortField === this.appSortable && this.currentSortReverse
|
||||
}
|
||||
|
||||
@HostListener('click') rotate() {
|
||||
if (this.currentSortField != this.appSortable) {
|
||||
this.sort.emit({ column: this.appSortable, reverse: false })
|
||||
} else if (
|
||||
this.currentSortField == this.sortable &&
|
||||
this.currentSortField == this.appSortable &&
|
||||
!this.currentSortReverse
|
||||
) {
|
||||
this.sort.emit({ column: this.currentSortField, reverse: true })
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { DirtyCheckGuard } from '@ngneat/dirty-check-forms'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||
|
||||
|
@ -213,7 +213,8 @@ export class DocumentListViewService {
|
||||
this.currentPageSize,
|
||||
activeListViewState.sortField,
|
||||
activeListViewState.sortReverse,
|
||||
activeListViewState.filterRules
|
||||
activeListViewState.filterRules,
|
||||
{ truncate_content: true }
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
|
@ -6,7 +6,6 @@ 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',
|
||||
@ -16,8 +15,7 @@ export class OpenDocumentsService {
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private modalService: NgbModal,
|
||||
private router: Router
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
if (sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)) {
|
||||
try {
|
||||
@ -57,39 +55,28 @@ export class OpenDocumentsService {
|
||||
return this.openDocuments.find((d) => d.id == id)
|
||||
}
|
||||
|
||||
openDocument(
|
||||
doc: PaperlessDocument,
|
||||
navigate: boolean = true
|
||||
): Observable<boolean> {
|
||||
openDocument(doc: PaperlessDocument): Observable<boolean> {
|
||||
if (this.openDocuments.find((d) => d.id == doc.id) == null) {
|
||||
if (this.openDocuments.length == this.MAX_OPEN_DOCUMENTS) {
|
||||
// at max, ensure changes arent lost
|
||||
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)
|
||||
if (closed) this.finishOpenDocument(doc)
|
||||
})
|
||||
return closeObservable
|
||||
} else {
|
||||
// not at max
|
||||
this.finishOpenDocument(doc, navigate)
|
||||
}
|
||||
} else {
|
||||
// doc is open, just maybe navigate
|
||||
if (navigate) {
|
||||
this.router.navigate(['documents', doc.id])
|
||||
this.finishOpenDocument(doc)
|
||||
}
|
||||
}
|
||||
return of(true)
|
||||
}
|
||||
|
||||
private finishOpenDocument(doc: PaperlessDocument, navigate: boolean) {
|
||||
private finishOpenDocument(doc: PaperlessDocument) {
|
||||
this.openDocuments.unshift(doc)
|
||||
this.dirtyDocuments.delete(doc.id)
|
||||
this.save()
|
||||
if (navigate) {
|
||||
this.router.navigate(['documents', doc.id])
|
||||
}
|
||||
}
|
||||
|
||||
setDirty(doc: PaperlessDocument, dirty: boolean) {
|
||||
|
@ -174,10 +174,18 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
)
|
||||
}
|
||||
|
||||
bulkDownload(ids: number[], content = 'both') {
|
||||
bulkDownload(
|
||||
ids: number[],
|
||||
content = 'both',
|
||||
useFilenameFormatting: boolean = false
|
||||
) {
|
||||
return this.http.post(
|
||||
this.getResourceUrl(null, 'bulk_download'),
|
||||
{ documents: ids, content: content },
|
||||
{
|
||||
documents: ids,
|
||||
content: content,
|
||||
follow_formatting: useFilenameFormatting,
|
||||
},
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
}
|
||||
|
51
src-ui/src/app/services/rest/mail-account.service.ts
Normal file
51
src-ui/src/app/services/rest/mail-account.service.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MailAccountService extends AbstractPaperlessService<PaperlessMailAccount> {
|
||||
loading: boolean
|
||||
|
||||
constructor(http: HttpClient) {
|
||||
super(http, 'mail_accounts')
|
||||
}
|
||||
|
||||
private reload() {
|
||||
this.loading = true
|
||||
this.listAll().subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
private mailAccounts: PaperlessMailAccount[] = []
|
||||
|
||||
get allAccounts() {
|
||||
return this.mailAccounts
|
||||
}
|
||||
|
||||
create(o: PaperlessMailAccount) {
|
||||
return super.create(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
update(o: PaperlessMailAccount) {
|
||||
return super.update(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
patchMany(
|
||||
objects: PaperlessMailAccount[]
|
||||
): Observable<PaperlessMailAccount[]> {
|
||||
return combineLatest(objects.map((o) => super.patch(o))).pipe(
|
||||
tap(() => this.reload())
|
||||
)
|
||||
}
|
||||
|
||||
delete(o: PaperlessMailAccount) {
|
||||
return super.delete(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
}
|
49
src-ui/src/app/services/rest/mail-rule.service.ts
Normal file
49
src-ui/src/app/services/rest/mail-rule.service.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MailRuleService extends AbstractPaperlessService<PaperlessMailRule> {
|
||||
loading: boolean
|
||||
|
||||
constructor(http: HttpClient) {
|
||||
super(http, 'mail_rules')
|
||||
}
|
||||
|
||||
private reload() {
|
||||
this.loading = true
|
||||
this.listAll().subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
private mailRules: PaperlessMailRule[] = []
|
||||
|
||||
get allRules() {
|
||||
return this.mailRules
|
||||
}
|
||||
|
||||
create(o: PaperlessMailRule) {
|
||||
return super.create(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
update(o: PaperlessMailRule) {
|
||||
return super.update(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
patchMany(objects: PaperlessMailRule[]): Observable<PaperlessMailRule[]> {
|
||||
return combineLatest(objects.map((o) => super.patch(o))).pipe(
|
||||
tap(() => this.reload())
|
||||
)
|
||||
}
|
||||
|
||||
delete(o: PaperlessMailRule) {
|
||||
return super.delete(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ export const environment = {
|
||||
apiBaseUrl: document.baseURI + 'api/',
|
||||
apiVersion: '2',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: '1.10.2',
|
||||
version: '1.10.2-dev',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
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