Compare commits

..

8 Commits

Author SHA1 Message Date
shamoon
7b75333819 custom field regex matching
[ci skip]
2025-03-20 23:17:57 -07:00
shamoon
71fdc2a36d limit the frontend algo select too 2025-03-20 23:17:44 -07:00
shamoon
dbe58672ed limit matching options in serializer
[ci skip]
2025-03-20 21:54:05 -07:00
shamoon
8a907c2868 Fix some merge stuff
[ci skip]
2025-03-20 21:54:05 -07:00
shamoon
6dc6c6c7bb Add to handler, matching, retagger 2025-03-20 21:54:05 -07:00
shamoon
a632b6b711 Add custom fields to classifier 2025-03-20 16:21:34 -07:00
shamoon
b8c618abbe Make frontend list a generic management list 2025-03-20 16:21:14 -07:00
shamoon
7a46806643 Migrate to matching model 2025-03-20 16:20:16 -07:00
255 changed files with 28346 additions and 37441 deletions

View File

@@ -21,17 +21,19 @@
# This file is intended only to be used through VSCOde devcontainers. See README.md
# in the folder .devcontainer.
services:
broker:
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- ./redisdata:/data
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
paperless-development:
image: paperless-ngx
build:
context: ../ # Dockerfile cannot access files from parent directories if context is not set.
context: ../ # Dockerfile cannot access files from parent directories if context is not set.
dockerfile: ./.devcontainer/Dockerfile
restart: unless-stopped
depends_on:
@@ -58,20 +60,25 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
PAPERLESS_STATICDIR: ./src/documents/static
PAPERLESS_DEBUG: true
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "chown -R paperless:paperless /usr/src/paperless/paperless-ngx/src/documents/static/frontend && chown -R paperless:paperless /usr/src/paperless/paperless-ngx/.ruff_cache && while sleep 1000; do :; done"
gotenberg:
image: docker.io/gotenberg/gotenberg:8.17
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: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:

View File

@@ -5,6 +5,7 @@ version: 2
# Required for uv support for now
enable-beta-ecosystems: true
updates:
# Enable version updates for pnpm
- package-ecosystem: "npm"
target-branch: "dev"
@@ -34,6 +35,7 @@ updates:
patterns:
- "@typescript-eslint*"
- "eslint"
# Enable version updates for Python
- package-ecosystem: "uv"
target-branch: "dev"
@@ -57,7 +59,6 @@ updates:
django:
patterns:
- "*django*"
- "drf-*"
major-versions:
update-types:
- "major"
@@ -69,6 +70,7 @@ updates:
patterns:
- psycopg*
- zxing-cpp
# Enable updates for GitHub Actions
- package-ecosystem: "github-actions"
target-branch: "dev"
@@ -88,6 +90,7 @@ updates:
- "major"
- "minor"
- "patch"
# Update Dockerfile in root directory
- package-ecosystem: "docker"
directory: "/"
@@ -97,10 +100,12 @@ updates:
reviewers:
- "paperless-ngx/ci-cd"
labels:
- "ci-cd"
- "dependencies"
commit-message:
prefix: "docker"
include: "scope"
# Update Docker Compose files in docker/compose directory
- package-ecosystem: "docker-compose"
directory: "/docker/compose/"
@@ -110,6 +115,7 @@ updates:
reviewers:
- "paperless-ngx/ci-cd"
labels:
- "ci-cd"
- "dependencies"
commit-message:
prefix: "docker-compose"

19
.github/labeler.yml vendored
View File

@@ -1,19 +0,0 @@
backend:
- changed-files:
- any-glob-to-any-file:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'requirements.txt'
frontend:
- changed-files:
- any-glob-to-any-file:
- 'src-ui/**'
documentation:
- changed-files:
- any-glob-to-any-file:
- 'docs/**'
ci-cd:
- changed-files:
- any-glob-to-any-file:
- '.github/**'

View File

@@ -1,4 +1,5 @@
name: ci
on:
push:
tags:
@@ -11,57 +12,72 @@ on:
pull_request:
branches-ignore:
- 'translations**'
env:
DEFAULT_UV_VERSION: "0.6.x"
# This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.11"
jobs:
pre-commit:
# We want to run on external PRs, but not on our own internal PRs as they'll be run
# by the push to the branch. Without this if check, checks are duplicated since
# internal PRs match both the push and pull_request events.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
if:
github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
github.repository
name: Linting Checks
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
-
name: Checkout repository
uses: actions/checkout@v4
- name: Install python
-
name: Install python
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Check files
-
name: Check files
uses: pre-commit/action@v3.0.1
documentation:
name: "Build & Deploy Documentation"
runs-on: ubuntu-24.04
needs:
- pre-commit
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v4
- name: Set up Python
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v6
-
name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install Python dependencies
-
name: Install Python dependencies
run: |
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
- name: Make documentation
-
name: Make documentation
run: |
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
--dev \
--frozen \
mkdocs build --config-file ./mkdocs.yml
- name: Deploy documentation
-
name: Deploy documentation
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
@@ -72,12 +88,14 @@ jobs:
--dev \
--frozen \
mkdocs gh-deploy --force --no-history
- name: Upload artifact
-
name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: documentation
path: site/
retention-days: 7
tests-backend:
name: "Backend Tests (Python ${{ matrix.python-version }})"
runs-on: ubuntu-24.04
@@ -88,40 +106,49 @@ jobs:
python-version: ['3.10', '3.11', '3.12']
fail-fast: false
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v4
- name: Start containers
-
name: Start containers
run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach
- name: Set up Python
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@v6
-
name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install system dependencies
-
name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript libzbar0 poppler-utils
- name: Configure ImageMagick
-
name: Configure ImageMagick
run: |
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
- name: Install Python dependencies
-
name: Install Python dependencies
run: |
uv sync \
--python ${{ steps.setup-python.outputs.python-version }} \
--group testing \
--frozen
- name: List installed Python dependencies
-
name: List installed Python dependencies
run: |
uv pip list
- name: Tests
-
name: Tests
env:
PAPERLESS_CI_TEST: 1
# Enable paperless_mail testing against real server
@@ -134,24 +161,28 @@ jobs:
--dev \
--frozen \
pytest
- name: Upload backend test results to Codecov
-
name: Upload backend test results to Codecov
if: always()
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }}
files: junit.xml
- name: Upload backend coverage to Codecov
-
name: Upload backend coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }}
files: coverage.xml
- name: Stop containers
-
name: Stop containers
if: always()
run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
install-frontend-dependencies:
name: "Install Frontend Dependencies"
runs-on: ubuntu-24.04
@@ -163,7 +194,8 @@ jobs:
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
@@ -177,10 +209,17 @@ jobs:
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Install dependencies
-
name: Install dependencies
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
run: cd src-ui && pnpm install
-
name: Install Playwright
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
run: cd src-ui && pnpm playwright install --with-deps
tests-frontend:
name: "Frontend Unit Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04
needs:
- install-frontend-dependencies
@@ -196,7 +235,8 @@ jobs:
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
@@ -212,90 +252,52 @@ jobs:
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Linting checks
-
name: Linting checks
run: cd src-ui && pnpm run lint
- name: Run Jest unit tests
-
name: Run Jest unit tests
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload frontend test results to Codecov
-
name: Run Playwright e2e tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
-
name: Upload frontend test results to Codecov
uses: codecov/test-results-action@v1
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/
- name: Upload frontend coverage to Codecov
-
name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/
tests-frontend-e2e:
name: "Frontend E2E Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04
needs:
- install-frontend-dependencies
strategy:
fail-fast: false
matrix:
node-version: [20.x]
shard-index: [1, 2]
shard-count: [2]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright system dependencies
run: npx playwright install-deps
- name: Install dependencies
run: cd src-ui && pnpm install --no-frozen-lockfile
- name: Install Playwright
run: cd src-ui && pnpm exec playwright install
- name: Run Playwright e2e tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
frontend-bundle-analysis:
name: "Frontend Bundle Analysis"
runs-on: ubuntu-24.04
needs:
- tests-frontend
- tests-frontend-e2e
steps:
- uses: actions/checkout@v4
- name: Install pnpm
-
name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
-
name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
@@ -303,12 +305,15 @@ jobs:
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
- name: Re-link Angular cli
-
name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Build frontend and upload analysis
-
name: Build frontend and upload analysis
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: cd src-ui && pnpm run build --configuration=production
build-docker-image:
name: Build Docker image for ${{ github.ref_name }}
runs-on: ubuntu-24.04
@@ -319,9 +324,9 @@ jobs:
needs:
- tests-backend
- tests-frontend
- tests-frontend-e2e
steps:
- name: Check pushing to Docker Hub
-
name: Check pushing to Docker Hub
id: push-other-places
# Only push to Dockerhub from the main repo AND the ref is either:
# main
@@ -337,13 +342,15 @@ jobs:
echo "Not pushing to DockerHub"
echo "enable=false" >> $GITHUB_OUTPUT
fi
- name: Set ghcr repository name
-
name: Set ghcr repository name
id: set-ghcr-repository
run: |
ghcr_name=$(echo "${{ github.repository }}" | awk '{ print tolower($0) }')
echo "Name is ${ghcr_name}"
echo "ghcr-repository=${ghcr_name}" >> $GITHUB_OUTPUT
- name: Gather Docker metadata
-
name: Gather Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
@@ -358,31 +365,37 @@ jobs:
# For a tag x.y.z or vX.Y.Z, output an x.y.z and x.y image tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Checkout
-
name: Checkout
uses: actions/checkout@v4
# If https://github.com/docker/buildx/issues/1044 is resolved,
# the append input with a native arm64 arch could be used to
# significantly speed up building
- name: Set up Docker Buildx
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Login to GitHub Container Registry
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
-
name: Login to Docker Hub
uses: docker/login-action@v3
# Don't attempt to login if not pushing to Docker Hub
if: steps.push-other-places.outputs.enable == 'true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io
-
name: Login to Quay.io
uses: docker/login-action@v3
# Don't attempt to login if not pushing to Quay.io
if: steps.push-other-places.outputs.enable == 'true'
@@ -390,7 +403,8 @@ jobs:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- name: Build and push
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
@@ -408,19 +422,23 @@ jobs:
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev
cache-to: |
type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
- name: Inspect image
-
name: Inspect image
run: |
docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
- name: Export frontend artifact from docker
-
name: Export frontend artifact from docker
run: |
docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
- name: Upload frontend artifact
-
name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: frontend-compiled
path: src/documents/static/frontend/
retention-days: 7
build-release:
name: "Build Release"
needs:
@@ -428,52 +446,63 @@ jobs:
- documentation
runs-on: ubuntu-24.04
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v4
- name: Set up Python
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v6
-
name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install Python dependencies
-
name: Install Python dependencies
run: |
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
- name: Install system dependencies
-
name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext liblept5
- name: Download frontend artifact
-
name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: frontend-compiled
path: src/documents/static/frontend/
- name: Download documentation artifact
-
name: Download documentation artifact
uses: actions/download-artifact@v4
with:
name: documentation
path: docs/_build/html/
- name: Generate requirements file
-
name: Generate requirements file
run: |
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
- name: Compile messages
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
-
name: Compile messages
run: |
cd src/
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
manage.py compilemessages
- name: Collect static files
-
name: Collect static files
run: |
cd src/
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
manage.py collectstatic --no-input
- name: Move files
-
name: Move files
run: |
echo "Making dist folders"
for directory in dist \
@@ -510,18 +539,21 @@ jobs:
cp --recursive docs/_build/html/ dist/paperless-ngx/docs
mv --verbose static dist/paperless-ngx
- name: Make release package
-
name: Make release package
run: |
echo "Creating release archive"
cd dist
sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact
-
name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: release
path: dist/paperless-ngx.tar.xz
retention-days: 7
publish-release:
name: "Publish Release"
runs-on: ubuntu-24.04
@@ -533,12 +565,14 @@ jobs:
- build-release
if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc'))
steps:
- name: Download release artifact
-
name: Download release artifact
uses: actions/download-artifact@v4
with:
name: release
path: ./
- name: Get version
-
name: Get version
id: get_version
run: |
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
@@ -547,7 +581,8 @@ jobs:
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Create Release and Changelog
-
name: Create Release and Changelog
id: create-release
uses: release-drafter/release-drafter@v6
with:
@@ -558,7 +593,8 @@ jobs:
publish: true # ensures release is not marked as draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive
-
name: Upload release archive
id: upload-release-asset
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -567,6 +603,7 @@ jobs:
asset_path: ./paperless-ngx.tar.xz
asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz
asset_content_type: application/x-xz
append-changelog:
name: "Append Changelog"
runs-on: ubuntu-24.04
@@ -574,22 +611,26 @@ jobs:
- publish-release
if: needs.publish-release.outputs.prerelease == 'false'
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v4
with:
ref: main
- name: Set up Python
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v6
-
name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Append Changelog to docs
-
name: Append Changelog to docs
id: append-Changelog
working-directory: docs
run: |
@@ -611,7 +652,8 @@ jobs:
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
git push origin ${{ needs.publish-release.outputs.version }}-changelog
- name: Create Pull Request
-
name: Create Pull Request
uses: actions/github-script@v7
with:
script: |

View File

@@ -6,14 +6,17 @@
# This workflow will not trigger runs on forked repos.
name: Cleanup Image Tags
on:
delete:
push:
paths:
- ".github/workflows/cleanup-tags.yml"
concurrency:
group: registry-tags-cleanup
cancel-in-progress: false
jobs:
cleanup-images:
name: Cleanup Image Tags for ${{ matrix.primary-name }}
@@ -27,7 +30,8 @@ jobs:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
steps:
- name: Clean temporary images
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.10.0
with:
@@ -39,6 +43,7 @@ jobs:
repo_name: "paperless-ngx"
match_regex: "(feature|fix)"
do_delete: "true"
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx'
@@ -53,7 +58,8 @@ jobs:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
steps:
- name: Clean untagged images
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.10.0
with:

View File

@@ -10,14 +10,16 @@
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [main, dev]
branches: [ main, dev ]
pull_request:
# The branches below must be a subset of the branches above
branches: [dev]
branches: [ dev ]
schedule:
- cron: '28 13 * * 5'
jobs:
analyze:
name: Analyze
@@ -26,23 +28,27 @@ jobs:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript', 'python']
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@@ -1,28 +1,35 @@
name: Crowdin Action
on:
workflow_dispatch:
schedule:
- cron: '2 */12 * * *'
push:
paths: ['src/locale/**', 'src-ui/messages.xlf', 'src-ui/src/locale/**']
branches: [dev]
paths: [
'src/locale/**',
'src-ui/messages.xlf',
'src-ui/src/locale/**'
]
branches: [ dev ]
jobs:
synchronize-with-crowdin:
name: Crowdin Sync
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v2
with:
upload_translations: false
download_translations: true
crowdin_branch_name: 'dev'
localization_branch_name: l10n_dev
pull_request_labels: 'skip-changelog, translation'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Checkout
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v2
with:
upload_translations: false
download_translations: true
crowdin_branch_name: 'dev'
localization_branch_name: l10n_dev
pull_request_labels: 'skip-changelog, translation'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -1,86 +0,0 @@
name: PR Bot
on:
pull_request_target:
types: [opened]
permissions:
contents: read
pull-requests: write
jobs:
pr-bot:
name: Automated PR Bot
runs-on: ubuntu-latest
steps:
- name: Label by file path
uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Label by size
uses: Gascon1/pr-size-labeler@v1.3.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
xs_label: 'small-change'
xs_diff: '9'
s_label: 'non-trivial'
s_diff: '99999'
fail_if_xl: 'false'
excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$
- name: Label bot-generated PRs
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const user = pr.user.login.toLowerCase();
const labels = [];
if (user.includes('dependabot')) {
labels.push('dependencies');
}
if (user.includes('crowdin-bot')) {
labels.push('translation', 'skip-changelog');
}
if (labels.length) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels,
});
}
- name: Welcome comment
if: ${{ !contains(github.actor, 'bot') }}
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const user = pr.user.login;
const { data: members } = await github.rest.orgs.listMembers({
org: 'paperless-ngx',
});
const memberLogins = members.map(m => m.login.toLowerCase());
if (memberLogins.includes(user.toLowerCase())) {
core.info('Skipping comment: user is org member');
return;
}
const body =
"Hello @" + user + ",\n\n" +
"Thank you very much for submitting this PR to us!\n\n" +
"This is what will happen next:\n\n" +
"1. CI tests will run against your PR to ensure quality and consistency.\n" +
"2. Next, human contributors from paperless-ngx review your changes.\n" +
"3. Please address any issues that come up during the review as soon as you are able to.\n" +
"4. If accepted, your pull request will be merged into the `dev` branch and changes there will be tested further.\n" +
"5. Eventually, changes from you and other contributors will be merged into `main` and a new release will be made.\n\n" +
"You'll be hearing from us soon, and thank you again for contributing to our project.";
await github.rest.issues.createComment({
issue_number: pr.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});

View File

@@ -1,4 +1,5 @@
name: Project Automations
on:
pull_request_target: #_target allows access to secrets
types:
@@ -7,8 +8,10 @@ on:
branches:
- main
- dev
permissions:
contents: read
jobs:
pr_opened_or_reopened:
name: pr_opened_or_reopened

View File

@@ -1,14 +1,18 @@
name: 'Repository Maintenance'
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
discussions: write
concurrency:
group: lock
jobs:
stale:
name: 'Stale'
@@ -23,8 +27,9 @@ jobs:
stale-issue-label: stale
stale-pr-label: stale
stale-issue-message: >
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
lock-threads:
name: 'Lock Old Threads'
if: github.repository_owner == 'paperless-ngx'
@@ -37,14 +42,20 @@ jobs:
discussion-inactive-days: '30'
log-output: true
issue-comment: >
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion or issue for related concerns. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
This issue has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns.
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
pr-comment: >
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion or issue for related concerns. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns.
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
discussion-comment: >
This discussion has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion for related concerns. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
This discussion has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion for related concerns.
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
close-answered-discussions:
name: 'Close Answered Discussions'
if: github.repository_owner == 'paperless-ngx'

View File

@@ -1,69 +0,0 @@
name: Generate Translation Strings
on:
push:
branches:
- dev
jobs:
generate-translate-strings:
name: Generate Translation Strings
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.PNGX_BOT_PAT }}
ref: ${{ github.head_ref }}
- name: Set up Python
id: setup-python
uses: actions/setup-python@v5
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Install backend python dependencies
run: |
uv sync \
--group dev \
--frozen
- name: Generate backend translation strings
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Install frontend dependencies
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
run: cd src-ui && pnpm install
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Generate frontend translation strings
run: |
cd src-ui
pnpm run ng extract-i18n
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
commit_message: "Auto translate strings"
commit_user_name: "GitHub Actions"
commit_author: "GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com>"

View File

@@ -76,8 +76,3 @@ repos:
rev: "v0.10.0.1"
hooks:
- id: shellcheck
- repo: https://github.com/google/yamlfmt
rev: v0.14.0
hooks:
- id: yamlfmt
exclude: "^src-ui/pnpm-lock.yaml"

View File

@@ -81,7 +81,7 @@ Some notes about translation:
If a language has already been added, and you would like to contribute new translations or change existing translations, please read the "Translation" section in the README.md file for further details on that.
If you would like the project to be translated to another language, first head over to https://crowdin.com/project/paperless-ngx to check if that language has already been enabled for translation.
If you would like the project to be translated to another language, first head over to https://crwd.in/paperless-ngx to check if that language has already been enabled for translation.
If not, please request the language to be added by creating an issue on GitHub. The issue should contain:
- English name of the language (the localized name can be added on Crowdin).

View File

@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.6.16-python3.12-bookworm-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.6.5-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6
@@ -47,7 +47,7 @@ ENV \
ARG TARGETARCH
ARG TARGETVARIANT
# Lock this version
ARG S6_OVERLAY_VERSION=3.2.1.0
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG S6_BUILD_TIME_PKGS="curl \
xz-utils"
@@ -239,7 +239,6 @@ COPY --from=compile-frontend --chown=1000:1000 /src/src/documents/static/fronten
# add users, setup scripts
# Mount the compiled frontend to expected location
RUN set -eux \
&& sed -i '1s|^#!/usr/bin/env python3|#!/command/with-contenv python3|' manage.py \
&& echo "Setting up user/group" \
&& addgroup --gid 1000 paperless \
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \

View File

@@ -83,7 +83,7 @@ People interested in continuing the work on paperless-ngx are encouraged to reac
## Translation
Paperless-ngx is available in many languages that are coordinated on Crowdin. If you want to help out by translating paperless-ngx into your language, please head over to https://crowdin.com/project/paperless-ngx, and thank you! More details can be found in [CONTRIBUTING.md](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md#translating-paperless-ngx).
Paperless-ngx is available in many languages that are coordinated on Crowdin. If you want to help out by translating paperless-ngx into your language, please head over to https://crwd.in/paperless-ngx, and thank you! More details can be found in [CONTRIBUTING.md](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md#translating-paperless-ngx).
## Feature Requests

View File

@@ -5,7 +5,7 @@
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.20
image: docker.io/gotenberg/gotenberg:8.17
hostname: gotenberg
container_name: gotenberg
network_mode: host

View File

@@ -24,18 +24,19 @@
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/mariadb:11
restart: unless-stopped
@@ -47,6 +48,7 @@ services:
MARIADB_USER: paperless
MARIADB_PASSWORD: paperless
MARIADB_ROOT_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -73,8 +75,9 @@ services:
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.20
image: docker.io/gotenberg/gotenberg:8.17
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.
@@ -82,9 +85,11 @@ services:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:

View File

@@ -20,6 +20,7 @@
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
@@ -27,10 +28,11 @@
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/mariadb:11
restart: unless-stopped
@@ -42,6 +44,7 @@ services:
MARIADB_USER: paperless
MARIADB_PASSWORD: paperless
MARIADB_ROOT_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -63,6 +66,7 @@ services:
PAPERLESS_DBUSER: paperless # only needed if non-default username
PAPERLESS_DBPASS: paperless # only needed if non-default password
PAPERLESS_DBPORT: 3306
volumes:
data:
media:

View File

@@ -22,16 +22,21 @@
# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file'
# - Modify the environment variables as needed
# - Click 'Deploy the stack' and wait for it to be deployed
# - Open the list of containers, select paperless_webserver_1
# - Click 'Console' and then 'Connect' to open the command line inside the container
# - Run 'python3 manage.py createsuperuser' to create a user
# - Exit the console
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:17
restart: unless-stopped
@@ -41,6 +46,7 @@ services:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -59,6 +65,7 @@ services:
PAPERLESS_DBHOST: db
env_file:
- stack.env
volumes:
data:
media:

View File

@@ -24,6 +24,7 @@
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
@@ -31,10 +32,11 @@
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:17
restart: unless-stopped
@@ -44,6 +46,7 @@ services:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -66,18 +69,22 @@ services:
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.20
image: docker.io/gotenberg/gotenberg:8.17
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: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:

View File

@@ -20,6 +20,7 @@
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
@@ -27,10 +28,11 @@
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:17
restart: unless-stopped
@@ -40,6 +42,7 @@ services:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -57,6 +60,7 @@ services:
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
data:
media:

View File

@@ -24,6 +24,7 @@
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
@@ -31,10 +32,11 @@
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -55,18 +57,22 @@ services:
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.20
image: docker.io/gotenberg/gotenberg:8.17
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: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:

View File

@@ -17,6 +17,7 @@
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
@@ -24,10 +25,11 @@
services:
broker:
image: docker.io/library/redis:8
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
@@ -43,6 +45,7 @@ services:
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
volumes:
data:
media:

179
docker/docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -e
# Source: https://github.com/sameersbn/docker-gitlab/
map_uidgid() {
local -r usermap_original_uid=$(id -u paperless)
local -r usermap_original_gid=$(id -g paperless)
local -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid}
local -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}}
if [[ ${usermap_new_uid} != "${usermap_original_uid}" || ${usermap_new_gid} != "${usermap_original_gid}" ]]; then
echo "Mapping UID and GID for paperless:paperless to $usermap_new_uid:$usermap_new_gid"
usermod --non-unique --uid "${usermap_new_uid}" paperless
groupmod --non-unique --gid "${usermap_new_gid}" paperless
fi
}
map_folders() {
# Export these so they can be used in docker-prepare.sh
export DATA_DIR="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
export MEDIA_ROOT_DIR="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
export CONSUME_DIR="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
}
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
local -r custom_script_dir="/custom-cont-init.d"
# Tamper checking.
# Don't run files which are owned by anyone except root
# Don't run files which are writeable by others
if [ -d "${custom_script_dir}" ]; then
if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 ! -user root)" ]; then
echo "**** Potential tampering with custom scripts detected ****"
echo "**** The folder '${custom_script_dir}' must be owned by root ****"
return 0
fi
if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 -perm -o+w)" ]; then
echo "**** The folder '${custom_script_dir}' or some of contents have write permissions for others, which is a security risk. ****"
echo "**** Please review the permissions and their contents to make sure they are owned by root, and can only be modified by root. ****"
return 0
fi
# Make sure custom init directory has files in it
if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then
echo "[custom-init] files found in ${custom_script_dir} executing"
# Loop over files in the directory
for SCRIPT in "${custom_script_dir}"/*; do
NAME="$(basename "${SCRIPT}")"
if [ -f "${SCRIPT}" ]; then
echo "[custom-init] ${NAME}: executing..."
/bin/bash "${SCRIPT}"
echo "[custom-init] ${NAME}: exited $?"
elif [ ! -f "${SCRIPT}" ]; then
echo "[custom-init] ${NAME}: is not a file"
fi
done
else
echo "[custom-init] no custom files found exiting..."
fi
fi
}
initialize() {
# Setup environment from secrets before anything else
# Check for a version of this var with _FILE appended
# and convert the contents to the env var value
# Source it so export is persistent
# shellcheck disable=SC1091
source /sbin/env-from-file.sh
# Change the user and group IDs if needed
map_uidgid
# Check for overrides of certain folders
map_folders
local -r export_dir="/usr/src/paperless/export"
for dir in \
"${export_dir}" \
"${DATA_DIR}" "${DATA_DIR}/index" \
"${MEDIA_ROOT_DIR}" "${MEDIA_ROOT_DIR}/documents" "${MEDIA_ROOT_DIR}/documents/originals" "${MEDIA_ROOT_DIR}/documents/thumbnails" \
"${CONSUME_DIR}"; do
if [[ ! -d "${dir}" ]]; then
echo "Creating directory ${dir}"
mkdir --parents --verbose "${dir}"
fi
done
local -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
echo "Creating directory scratch directory ${tmp_dir}"
mkdir --parents --verbose "${tmp_dir}"
set +e
echo "Adjusting permissions of paperless files. This may take a while."
chown -R paperless:paperless "${tmp_dir}"
for dir in \
"${export_dir}" \
"${DATA_DIR}" \
"${MEDIA_ROOT_DIR}" \
"${CONSUME_DIR}"; do
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
done
set -e
"${gosu_cmd[@]}" /sbin/docker-prepare.sh
# Leave this last thing
custom_container_init
}
install_languages() {
echo "Installing languages..."
read -ra langs <<<"$1"
# Check that it is not empty
if [ ${#langs[@]} -eq 0 ]; then
return
fi
# Build list of packages to install
to_install=()
for lang in "${langs[@]}"; do
pkg="tesseract-ocr-$lang"
if dpkg --status "$pkg" &>/dev/null; then
echo "Package $pkg already installed!"
continue
else
to_install+=("$pkg")
fi
done
# Use apt only when we install packages
if [ ${#to_install[@]} -gt 0 ]; then
apt-get update
for pkg in "${to_install[@]}"; do
if ! apt-cache show "$pkg" &>/dev/null; then
echo "Skipped $pkg: Package not found! :("
continue
fi
echo "Installing package $pkg..."
if ! apt-get --assume-yes install "$pkg" &>/dev/null; then
echo "Could not install $pkg"
exit 1
fi
done
fi
}
echo "Paperless-ngx docker container starting..."
gosu_cmd=(gosu paperless)
if [ "$(id --user)" == "$(id --user paperless)" ]; then
gosu_cmd=()
fi
# Install additional languages if specified
if [[ -n "$PAPERLESS_OCR_LANGUAGES" ]]; then
install_languages "$PAPERLESS_OCR_LANGUAGES"
fi
initialize
if [[ "$1" != "/"* ]]; then
echo Executing management command "$@"
exec "${gosu_cmd[@]}" python3 manage.py "$@"
else
echo Executing "$@"
exec "$@"
fi

View File

@@ -18,10 +18,9 @@ for command in decrypt_documents \
document_fuzzy_match \
manage_superuser \
convert_mariadb_uuid \
prune_audit_logs \
createsuperuser;
prune_audit_logs;
do
echo "installing $command..."
sed "s/management_command/$command/g" management_script.sh >"$PWD/rootfs/usr/local/bin/$command"
chmod u=rwx,g=rwx,o=rx "$PWD/rootfs/usr/local/bin/$command"
chmod +x "$PWD/rootfs/usr/local/bin/$command"
done

View File

@@ -9,7 +9,7 @@ if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; the
for FILENAME in /run/s6/container_environment/*; do
if [[ "${FILENAME##*/}" == PAPERLESS_*_FILE ]]; then
# This should have been named different..
if [[ "${FILENAME##*/}" == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || "${FILENAME##*/}" == "PAPERLESS_MODEL_FILE" ]]; then
if [[ ${FILENAME} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${FILENAME} == "PAPERLESS_MODEL_FILE" ]]; then
continue
fi
SECRETFILE=$(cat "${FILENAME}")
@@ -17,9 +17,6 @@ if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; the
if [[ -f ${SECRETFILE} ]]; then
# Trim off trailing _FILE
FILESTRIP=${FILENAME//_FILE/}
if [[ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]]; then
echo "${log_prefix} Your secret: ${FILENAME##*/} contains a trailing newline and may not work as expected"
fi
# Set environment variable
cat "${SECRETFILE}" > "${FILESTRIP}"
echo "${log_prefix} ${FILESTRIP##*/} set from ${FILENAME##*/}"

View File

@@ -9,57 +9,25 @@ declare -r media_root_dir="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
declare -r consume_dir="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
declare -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
declare -r main_dirs=(
"${export_dir}"
"${data_dir}"
"${media_root_dir}"
"${consume_dir}"
"${tmp_dir}"
)
echo "${log_prefix} Checking for folder existence"
declare -r extra_dirs=(
"${main_dirs[@]}"
"${data_dir}/index"
"${media_root_dir}/documents"
"${media_root_dir}/documents/originals"
"${media_root_dir}/documents/thumbnails"
)
for dir in \
"${export_dir}" \
"${data_dir}" "${data_dir}/index" \
"${media_root_dir}" "${media_root_dir}/documents" "${media_root_dir}/documents/originals" "${media_root_dir}/documents/thumbnails" \
"${consume_dir}" \
"${tmp_dir}"; do
if [[ ! -d "${dir}" ]]; then
mkdir --parents --verbose "${dir}"
fi
done
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
# Non-root mode: Create directories as current user, warn about permission issues
echo "${log_prefix} Running in non-root mode, checking directories"
current_uid=$(id --user)
current_gid=$(id --group)
for dir in "${extra_dirs[@]}"; do
if [[ ! -d "${dir}" ]]; then
mkdir --parents --verbose "${dir}" || echo "${log_prefix} WARNING: Could not create ${dir} - permission denied"
fi
# Check permissions on existing directories too
if [[ -d "${dir}" && ! -w "${dir}" ]]; then
echo "${log_prefix} WARNING: No write permission to ${dir}"
fi
done
# Warn about ownership issues
for dir in "${main_dirs[@]}"; do
if [[ -d "${dir}" ]]; then
find "${dir}" -not \( -user ${current_uid} -and -group ${current_gid} \) -exec echo "${log_prefix} WARNING: Permission issue on {}: not owned by current user (${current_uid}:${current_gid})" \; 2>/dev/null || echo "${log_prefix} WARNING: Cannot check permissions on ${dir}"
fi
done
else
# Root mode: Create and fix permissions as needed
echo "${log_prefix} Running with root privileges, adjusting directories and permissions"
# First create directories
for dir in "${extra_dirs[@]}"; do
if [[ ! -d "${dir}" ]]; then
mkdir --parents --verbose "${dir}"
fi
done
# Then fix permissions on all directories
for dir in "${main_dirs[@]}"; do
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
done
fi
echo "${log_prefix} Adjusting file and folder permissions"
for dir in \
"${export_dir}" \
"${data_dir}" \
"${media_root_dir}" \
"${consume_dir}" \
"${tmp_dir}"; do
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
done

View File

@@ -1,18 +1,20 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-migrations]"
declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
echo "${log_prefix} Apply database migrations..."
(
# flock is in place to prevent multiple containers from doing migrations
# simultaneously. This also ensures that the db is ready when the command
# of the current container starts.
flock 200
echo "${log_prefix} Apply database migrations..."
cd "${PAPERLESS_SRC_DIR}"
cd "${PAPERLESS_SRC_DIR}"
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec python3 manage.py migrate --skip-checks --no-input
else
exec s6-setuidgid paperless python3 manage.py migrate --skip-checks --no-input
fi
# The whole migrate, with flock, needs to run as the right user
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
else
exec s6-setuidgid paperless \
s6-setlock -n "${data_dir}/migration_lock" \
python3 manage.py migrate --skip-checks --no-input
fi
) 200>"${data_dir}/migration_lock"

View File

@@ -11,10 +11,9 @@ printf "/usr/src/paperless/src" > /var/run/s6/container_environment/PAPERLESS_SR
echo $(date +%s) > /var/run/s6/container_environment/PAPERLESS_START_TIME_S
# Check if we're starting as a non-root user
if [ "$(id --user)" != "0" ]; then
if [ $(id -u) == $(id -u paperless) ]; then
printf "true" > /var/run/s6/container_environment/USER_IS_NON_ROOT
echo "${log_prefix} paperless-ngx docker container running under a user ($(id --user):$(id --group))"
echo "${log_prefix} paperless-ngx docker container running under a user"
else
printf "/usr/src/paperless" > /var/run/s6/container_environment/HOME
echo "${log_prefix} paperless-ngx docker container starting init as root"
fi

View File

@@ -14,7 +14,7 @@ if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
fi
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
exec granian --interface asginl --ws "paperless.asgi:application"
else
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
exec s6-setuidgid paperless granian --interface asginl --ws "paperless.asgi:application"
fi

View File

@@ -1,14 +0,0 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
set -e
cd "${PAPERLESS_SRC_DIR}"
if [[ $(id -u) == 0 ]]; then
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
elif [[ $(id -un) == "paperless" ]]; then
python3 manage.py createsuperuser "$@"
else
echo "Unknown user."
fi

View File

@@ -372,17 +372,19 @@ currently-imported docs. This problem is common enough that there are
tools for it.
```
document_retagger [-h] [-c] [-T] [-t] [-i] [--id-range] [--use-first] [-f]
document_retagger [-h] [-c] [-T] [-t] [-cf] [-i] [--id-range] [--use-first] [-f] [--suggest]
optional arguments:
-c, --correspondent
-T, --tags
-t, --document_type
-s, --storage_path
-cf, --custom_fields
-i, --inbox-only
--id-range
--use-first
-f, --overwrite
--suggest
```
Run this after changing or adding matching rules. It'll loop over all
@@ -408,6 +410,8 @@ to override this behavior and just use the first correspondent or type
it finds. This option does not apply to tags, since any amount of tags
can be applied to a document.
If you want to suggest changes but not apply them, specify `--suggest`.
Finally, `-f` specifies that you wish to overwrite already assigned
correspondents, types and/or tags. The default behavior is to not assign
correspondents and types to documents that have this data already
@@ -565,15 +569,19 @@ document.
### Managing encryption {#encryption}
Documents can be stored in Paperless using GnuPG encryption.
!!! warning
Encryption was removed in [paperless-ng 0.9](changelog.md#paperless-ng-090)
because it did not really provide any additional security, the passphrase
was stored in a configuration file on the same system as the documents.
Furthermore, the entire text content of the documents is stored plain in
the database, even if your documents are encrypted. Filenames are not
encrypted as well. Finally, the web server provides transparent access to
your encrypted documents.
Encryption is deprecated since [paperless-ng 0.9](changelog.md#paperless-ng-090) and doesn't really
provide any additional security, since you have to store the passphrase
in a configuration file on the same system as the encrypted documents
for paperless to work. Furthermore, the entire text content of the
documents is stored plain in the database, even if your documents are
encrypted. Filenames are not encrypted as well.
Also, the web server provides transparent access to your encrypted
documents.
Consider running paperless on an encrypted filesystem instead, which
will then at least provide security against physical hardware theft.
@@ -629,11 +637,3 @@ entries created prior to this are not removed. This command allows you to prune
```shell
prune_audit_logs
```
### Create superuser {#create-superuser}
If you need to create a superuser, use the following command:
```shell
createsuperuser
```

View File

@@ -132,7 +132,7 @@ use cases:
5. Documents with a custom field "address" (text) that is empty:
`?custom_field_query=["OR", [["address", "isnull", true], ["address", "exact", ""]]]`
`?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]`
6. Documents that don't have a field called "foo":
@@ -270,7 +270,7 @@ The following methods are supported:
- `remove_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `modify_tags`
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and `{ "remove_tags": [LIST_OF_TAG_IDS] }`
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
- `delete`
- No `parameters` required
- `reprocess`
@@ -413,14 +413,3 @@ Initial API version.
list of strings. When creating or updating a custom field value of a
document for a select type custom field, the value should be the `id` of
the option whereas previously was the index of the option.
#### Version 8
- The user field of document notes now returns a simplified user object
rather than just the user ID.
#### Version 9
- The document `created` field is now a date, not a datetime. The
`created_date` field is considered deprecated and will be removed in a
future version.

View File

@@ -1,318 +1,5 @@
# Changelog
## paperless-ngx 2.15.3
### Bug Fixes
- Fix: do not try deleting original file that was moved to trash dir [@shamoon](https://github.com/shamoon) ([#9684](https://github.com/paperless-ngx/paperless-ngx/pull/9684))
- Fix: preserve non-ASCII filenames in document downloads [@shamoon](https://github.com/shamoon) ([#9702](https://github.com/paperless-ngx/paperless-ngx/pull/9702))
- Fix: fix breaking api change to document notes user field [@shamoon](https://github.com/shamoon) ([#9714](https://github.com/paperless-ngx/paperless-ngx/pull/9714))
- Fix: another doc link fix [@shamoon](https://github.com/shamoon) ([#9700](https://github.com/paperless-ngx/paperless-ngx/pull/9700))
- Fix: correctly handle dict data with webhook [@shamoon](https://github.com/shamoon) ([#9674](https://github.com/paperless-ngx/paperless-ngx/pull/9674))
### All App Changes
<details>
<summary>5 changes</summary>
- Fix: do not try deleting original file that was moved to trash dir [@shamoon](https://github.com/shamoon) ([#9684](https://github.com/paperless-ngx/paperless-ngx/pull/9684))
- Fix: preserve non-ASCII filenames in document downloads [@shamoon](https://github.com/shamoon) ([#9702](https://github.com/paperless-ngx/paperless-ngx/pull/9702))
- Fix: fix breaking api change to document notes user field [@shamoon](https://github.com/shamoon) ([#9714](https://github.com/paperless-ngx/paperless-ngx/pull/9714))
- Fix: another doc link fix [@shamoon](https://github.com/shamoon) ([#9700](https://github.com/paperless-ngx/paperless-ngx/pull/9700))
- Fix: correctly handle dict data with webhook [@shamoon](https://github.com/shamoon) ([#9674](https://github.com/paperless-ngx/paperless-ngx/pull/9674))
</details>
## paperless-ngx 2.15.2
### Bug Fixes
- Fix: Adds better handling during folder checking/creation/permissions for non-root [@stumpylog](https://github.com/stumpylog) ([#9616](https://github.com/paperless-ngx/paperless-ngx/pull/9616))
- Fix: Explicitly set the HOME environment to resolve issues running as root with database certificates [@stumpylog](https://github.com/stumpylog) ([#9643](https://github.com/paperless-ngx/paperless-ngx/pull/9643))
- Fix: prevent self-linking when bulk edit doc link [@shamoon](https://github.com/shamoon) ([#9629](https://github.com/paperless-ngx/paperless-ngx/pull/9629))
### Dependencies
- Chore: Bump celery to 5.5.1 [@hannesortmeier](https://github.com/hannesortmeier) ([#9642](https://github.com/paperless-ngx/paperless-ngx/pull/9642))
### All App Changes
<details>
<summary>4 changes</summary>
- Tweak: consistently use created date when displaying doc in list [@shamoon](https://github.com/shamoon) ([#9651](https://github.com/paperless-ngx/paperless-ngx/pull/9651))
- Fix: Adds better handling during folder checking/creation/permissions for non-root [@stumpylog](https://github.com/stumpylog) ([#9616](https://github.com/paperless-ngx/paperless-ngx/pull/9616))
- Fix: Explicitly set the HOME environment to resolve issues running as root with database certificates [@stumpylog](https://github.com/stumpylog) ([#9643](https://github.com/paperless-ngx/paperless-ngx/pull/9643))
- Fix: prevent self-linking when bulk edit doc link [@shamoon](https://github.com/shamoon) ([#9629](https://github.com/paperless-ngx/paperless-ngx/pull/9629))
</details>
## paperless-ngx 2.15.1
### Bug Fixes
- Fix: Run migration lock as the correct user [@stumpylog](https://github.com/stumpylog) ([#9604](https://github.com/paperless-ngx/paperless-ngx/pull/9604))
- Fix: Adds a warning to the user if their secret file includes a trailing newline [@stumpylog](https://github.com/stumpylog) ([#9601](https://github.com/paperless-ngx/paperless-ngx/pull/9601))
- Fix: correct download filename in 2.15.0 [@shamoon](https://github.com/shamoon) ([#9599](https://github.com/paperless-ngx/paperless-ngx/pull/9599))
- Fix: dont exclude matching check for scheduled workflows [@shamoon](https://github.com/shamoon) ([#9594](https://github.com/paperless-ngx/paperless-ngx/pull/9594))
### Maintenance
- docker(deps): Bump astral-sh/uv from 0.6.9-python3.12-bookworm-slim to 0.6.13-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9573](https://github.com/paperless-ngx/paperless-ngx/pull/9573))
### Dependencies
- docker(deps): Bump astral-sh/uv from 0.6.9-python3.12-bookworm-slim to 0.6.13-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9573](https://github.com/paperless-ngx/paperless-ngx/pull/9573))
- Chore: move to whoosh-reloaded, for now [@shamoon](https://github.com/shamoon) ([#9605](https://github.com/paperless-ngx/paperless-ngx/pull/9605))
### All App Changes
<details>
<summary>4 changes</summary>
- Fix: Run migration lock as the correct user [@stumpylog](https://github.com/stumpylog) ([#9604](https://github.com/paperless-ngx/paperless-ngx/pull/9604))
- Fix: Adds a warning to the user if their secret file includes a trailing newline [@stumpylog](https://github.com/stumpylog) ([#9601](https://github.com/paperless-ngx/paperless-ngx/pull/9601))
- Fix: correct download filename in 2.15.0 [@shamoon](https://github.com/shamoon) ([#9599](https://github.com/paperless-ngx/paperless-ngx/pull/9599))
- Fix: dont exclude matching check for scheduled workflows [@shamoon](https://github.com/shamoon) ([#9594](https://github.com/paperless-ngx/paperless-ngx/pull/9594))
</details>
## paperless-ngx 2.15.0
### Features
- Enhancement: allow webUI first account signup [@shamoon](https://github.com/shamoon) ([#9500](https://github.com/paperless-ngx/paperless-ngx/pull/9500))
- Enhancement: support more 'not assigned' filtering, refactor [@shamoon](https://github.com/shamoon) ([#9429](https://github.com/paperless-ngx/paperless-ngx/pull/9429))
- Enhancement: reorganize dates dropdown, add more relative options [@shamoon](https://github.com/shamoon) ([#9307](https://github.com/paperless-ngx/paperless-ngx/pull/9307))
- Enhancement: add switch to allow merging non-PDFs with archive version [@shamoon](https://github.com/shamoon) ([#9305](https://github.com/paperless-ngx/paperless-ngx/pull/9305))
- Enhancement: support assigning custom field values in workflows [@shamoon](https://github.com/shamoon) ([#9272](https://github.com/paperless-ngx/paperless-ngx/pull/9272))
- Enhancement: Add slugify filter in templating [@hwaterke](https://github.com/hwaterke) ([#9269](https://github.com/paperless-ngx/paperless-ngx/pull/9269))
- Feature: Switch webserver to granian [@stumpylog](https://github.com/stumpylog) ([#9218](https://github.com/paperless-ngx/paperless-ngx/pull/9218))
- Enhancement: relocate and smaller upload widget, dont limit upload list [@shamoon](https://github.com/shamoon) ([#9244](https://github.com/paperless-ngx/paperless-ngx/pull/9244))
- Enhancement: run tasks from system status, report sanity check, simpler classifier check, styling updates [@shamoon](https://github.com/shamoon) ([#9106](https://github.com/paperless-ngx/paperless-ngx/pull/9106))
- Enhancement: include celery log in logs view [@shamoon](https://github.com/shamoon) ([#9214](https://github.com/paperless-ngx/paperless-ngx/pull/9214))
- Enhancement: support default groups for regular and social account signup, syncing on login [@shamoon](https://github.com/shamoon) ([#9039](https://github.com/paperless-ngx/paperless-ngx/pull/9039))
- Enhancement: allow disabling the filesystem consumer [@shamoon](https://github.com/shamoon) ([#9199](https://github.com/paperless-ngx/paperless-ngx/pull/9199))
- Feature: email document [@shamoon](https://github.com/shamoon) ([#8950](https://github.com/paperless-ngx/paperless-ngx/pull/8950))
- Enhancement: webui workflowtrigger source option [@shamoon](https://github.com/shamoon) ([#9170](https://github.com/paperless-ngx/paperless-ngx/pull/9170))
- Enhancement: use charfield for webhook url, custom validation [@shamoon](https://github.com/shamoon) ([#9128](https://github.com/paperless-ngx/paperless-ngx/pull/9128))
- Feature: Chinese Traditional translation [@LokiHung](https://github.com/LokiHung) ([#9076](https://github.com/paperless-ngx/paperless-ngx/pull/9076))
- Enhancement: Use cached sessions for a minor performance improvement [@stumpylog](https://github.com/stumpylog) ([#9074](https://github.com/paperless-ngx/paperless-ngx/pull/9074))
- Feature: openapi spec, full api browser [@shamoon](https://github.com/shamoon) ([#8948](https://github.com/paperless-ngx/paperless-ngx/pull/8948))
- Enhancement: filter by file type [@shamoon](https://github.com/shamoon) ([#8946](https://github.com/paperless-ngx/paperless-ngx/pull/8946))
- Feature: Transition Docker to use s6 overlay [@stumpylog](https://github.com/stumpylog) ([#8886](https://github.com/paperless-ngx/paperless-ngx/pull/8886))
- Feature: better toast notifications management [@shamoon](https://github.com/shamoon) ([#8980](https://github.com/paperless-ngx/paperless-ngx/pull/8980))
- Enhancement: date picker and date filter dropdown improvements [@shamoon](https://github.com/shamoon) ([#9033](https://github.com/paperless-ngx/paperless-ngx/pull/9033))
- Tweak: more accurate classifier last trained time [@shamoon](https://github.com/shamoon) ([#9004](https://github.com/paperless-ngx/paperless-ngx/pull/9004))
- Enhancement: allow setting default pdf zoom [@shamoon](https://github.com/shamoon) ([#9017](https://github.com/paperless-ngx/paperless-ngx/pull/9017))
### Bug Fixes
- Fix: ensure only matched scheduled workflows are applied [@shamoon](https://github.com/shamoon) ([#9580](https://github.com/paperless-ngx/paperless-ngx/pull/9580))
- Fix: fix large doc thumb hidden at unexpected screen sizes [@shamoon](https://github.com/shamoon) ([#9559](https://github.com/paperless-ngx/paperless-ngx/pull/9559))
- Fix: fix potential race condition when creating new cf from doc details [@shamoon](https://github.com/shamoon) ([#9542](https://github.com/paperless-ngx/paperless-ngx/pull/9542))
- Fix: fix doc link input [@shamoon](https://github.com/shamoon) ([#9533](https://github.com/paperless-ngx/paperless-ngx/pull/9533))
- Fix: only overwrite existing cf values in workflow if set [@shamoon](https://github.com/shamoon) ([#9459](https://github.com/paperless-ngx/paperless-ngx/pull/9459))
- Fix: fix auto-close when doc update no longer has permissions [@shamoon](https://github.com/shamoon) ([#9453](https://github.com/paperless-ngx/paperless-ngx/pull/9453))
- Change: better handle permissions in patch requests [@shamoon](https://github.com/shamoon) ([#9393](https://github.com/paperless-ngx/paperless-ngx/pull/9393))
- Fix: use correct filename with webhook [@shamoon](https://github.com/shamoon) ([#9392](https://github.com/paperless-ngx/paperless-ngx/pull/9392))
- Change: sync OIDC groups on first login too [@shamoon](https://github.com/shamoon) ([#9387](https://github.com/paperless-ngx/paperless-ngx/pull/9387))
- Fix: only parse custom field queries when valid [@shamoon](https://github.com/shamoon) ([#9384](https://github.com/paperless-ngx/paperless-ngx/pull/9384))
- Fix: Allow setting of other Granian options [@stumpylog](https://github.com/stumpylog) ([#9360](https://github.com/paperless-ngx/paperless-ngx/pull/9360))
- Fix: Always clean up INotify [@stumpylog](https://github.com/stumpylog) ([#9359](https://github.com/paperless-ngx/paperless-ngx/pull/9359))
- Fix typo in inactive account template [@ocean90](https://github.com/ocean90) ([#9356](https://github.com/paperless-ngx/paperless-ngx/pull/9356))
- Fix: fix notes serializing in API document response [@shamoon](https://github.com/shamoon) ([#9336](https://github.com/paperless-ngx/paperless-ngx/pull/9336))
- Fix: correct all results with whoosh queries [@shamoon](https://github.com/shamoon) ([#9331](https://github.com/paperless-ngx/paperless-ngx/pull/9331))
- Fix: fix typo in altered migration [@gothicVI](https://github.com/gothicVI) ([#9321](https://github.com/paperless-ngx/paperless-ngx/pull/9321))
- Fix: add account_inactive template / url [@shamoon](https://github.com/shamoon) ([#9322](https://github.com/paperless-ngx/paperless-ngx/pull/9322))
- Fix: Switches data to content to upload raw bytes/text content [@stumpylog](https://github.com/stumpylog) ([#9293](https://github.com/paperless-ngx/paperless-ngx/pull/9293))
- Fix: handle null workflow body and email subject [@shamoon](https://github.com/shamoon) ([#9271](https://github.com/paperless-ngx/paperless-ngx/pull/9271))
- Fix: cleanup saved view references on custom field deletion, auto-refresh views, show error on saved view save [@shamoon](https://github.com/shamoon) ([#9225](https://github.com/paperless-ngx/paperless-ngx/pull/9225))
- Fix: revert thumbnail CSS workaround in favor of GPU workaround [@shamoon](https://github.com/shamoon) ([#9219](https://github.com/paperless-ngx/paperless-ngx/pull/9219))
- Fix: correct split confirm removal [@shamoon](https://github.com/shamoon) ([#9195](https://github.com/paperless-ngx/paperless-ngx/pull/9195))
- Fix: saved views do not return to default display fields after setting and then removing [@shamoon](https://github.com/shamoon) ([#9168](https://github.com/paperless-ngx/paperless-ngx/pull/9168))
- Fix: correct logged number of deleted documents on trash empty [@shamoon](https://github.com/shamoon) ([#9148](https://github.com/paperless-ngx/paperless-ngx/pull/9148))
- Fix: include account confirm email allauth URL [@shamoon](https://github.com/shamoon) ([#9147](https://github.com/paperless-ngx/paperless-ngx/pull/9147))
- Fix: remove additional scrollbar from popup preview [@shamoon](https://github.com/shamoon) ([#9140](https://github.com/paperless-ngx/paperless-ngx/pull/9140))
- Fix: wrap selected display fields [@shamoon](https://github.com/shamoon) ([#9139](https://github.com/paperless-ngx/paperless-ngx/pull/9139))
- Fix: reset documents sort field if user deletes the custom field [@shamoon](https://github.com/shamoon) ([#9127](https://github.com/paperless-ngx/paperless-ngx/pull/9127))
- Fix: limit document title length in workflows [@shamoon](https://github.com/shamoon) ([#9085](https://github.com/paperless-ngx/paperless-ngx/pull/9085))
- Fix: include doc link input import in custom fields query dropdown [@shamoon](https://github.com/shamoon) ([#9058](https://github.com/paperless-ngx/paperless-ngx/pull/9058))
- Fix: deselect and trigger refresh for deleted documents from bulk operations with delete originals [@shamoon](https://github.com/shamoon) ([#8996](https://github.com/paperless-ngx/paperless-ngx/pull/8996))
- Fix: allow empty email in profile [@shamoon](https://github.com/shamoon) ([#9012](https://github.com/paperless-ngx/paperless-ngx/pull/9012))
### Maintenance
- docker(deps): Bump astral-sh/uv from 0.6.5-python3.12-bookworm-slim to 0.6.9-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9488](https://github.com/paperless-ngx/paperless-ngx/pull/9488))
- Chore: Enables dependabot for Dockerfile and our Compose files [@stumpylog](https://github.com/stumpylog) ([#9342](https://github.com/paperless-ngx/paperless-ngx/pull/9342))
- Chore: ensure codecov upload gets attempted [@shamoon](https://github.com/shamoon) ([#9308](https://github.com/paperless-ngx/paperless-ngx/pull/9308))
- Chore: Split out some items into extras [@stumpylog](https://github.com/stumpylog) ([#9297](https://github.com/paperless-ngx/paperless-ngx/pull/9297))
- Chore: Enables Codecov test reporting for the backend [@stumpylog](https://github.com/stumpylog) ([#9295](https://github.com/paperless-ngx/paperless-ngx/pull/9295))
- Chore: Combine Python settings files [@stumpylog](https://github.com/stumpylog) ([#9292](https://github.com/paperless-ngx/paperless-ngx/pull/9292))
### Dependencies
<details>
<summary>43 changes</summary>
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9536](https://github.com/paperless-ngx/paperless-ngx/pull/9536))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9538](https://github.com/paperless-ngx/paperless-ngx/pull/9538))
- Chore(deps-dev): Bump @types/node from 22.13.9 to 22.13.17 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9539](https://github.com/paperless-ngx/paperless-ngx/pull/9539))
- Chore(deps-dev): Bump jest-preset-angular from 14.5.3 to 14.5.4 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9537](https://github.com/paperless-ngx/paperless-ngx/pull/9537))
- Chore(deps-dev): Bump @playwright/test from 1.50.1 to 1.51.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9540](https://github.com/paperless-ngx/paperless-ngx/pull/9540))
- Chore(deps): Bump django from 5.1.6 to 5.1.7 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9486](https://github.com/paperless-ngx/paperless-ngx/pull/9486))
- docker(deps): Bump astral-sh/uv from 0.6.5-python3.12-bookworm-slim to 0.6.9-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9488](https://github.com/paperless-ngx/paperless-ngx/pull/9488))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9372](https://github.com/paperless-ngx/paperless-ngx/pull/9372))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9371](https://github.com/paperless-ngx/paperless-ngx/pull/9371))
- Chore(deps): Update ocrmypdf requirement from ~=16.9.0 to ~=16.10.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9348](https://github.com/paperless-ngx/paperless-ngx/pull/9348))
- Chore(deps): Update drf-spectacular-sidecar requirement from ~=2025.2.1 to ~=2025.3.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9347](https://github.com/paperless-ngx/paperless-ngx/pull/9347))
- Chore(deps): Bump the small-changes group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9345](https://github.com/paperless-ngx/paperless-ngx/pull/9345))
- docker-compose(deps): Bump library/postgres from 16 to 17 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#9353](https://github.com/paperless-ngx/paperless-ngx/pull/9353))
- docker(deps): Bump astral-sh/uv from 0.6.3-python3.12-bookworm-slim to 0.6.5-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9344](https://github.com/paperless-ngx/paperless-ngx/pull/9344))
- Chore(deps-dev): Bump the frontend-angular-dependencies group in /src-ui with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9288](https://github.com/paperless-ngx/paperless-ngx/pull/9288))
- Chore(deps-dev): Bump @types/node from 22.13.8 to 22.13.9 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9290](https://github.com/paperless-ngx/paperless-ngx/pull/9290))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9289](https://github.com/paperless-ngx/paperless-ngx/pull/9289))
- Chore(deps-dev): Bump @types/node from 22.13.5 to 22.13.8 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9267](https://github.com/paperless-ngx/paperless-ngx/pull/9267))
- Chore(deps): Bump stumpylog/image-cleaner-action from 0.9.0 to 0.10.0 in the actions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9252](https://github.com/paperless-ngx/paperless-ngx/pull/9252))
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9253](https://github.com/paperless-ngx/paperless-ngx/pull/9253))
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.8.0 to 1.9.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9260](https://github.com/paperless-ngx/paperless-ngx/pull/9260))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9256](https://github.com/paperless-ngx/paperless-ngx/pull/9256))
- Chore(deps): Bump uuid from 11.0.5 to 11.1.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9259](https://github.com/paperless-ngx/paperless-ngx/pull/9259))
- Chore(deps-dev): Bump jest-preset-angular from 14.5.1 to 14.5.3 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9255](https://github.com/paperless-ngx/paperless-ngx/pull/9255))
- Chore(deps): Bump rxjs from 7.8.1 to 7.8.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9258](https://github.com/paperless-ngx/paperless-ngx/pull/9258))
- Chore(deps-dev): Bump @types/node from 22.13.0 to 22.13.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9257](https://github.com/paperless-ngx/paperless-ngx/pull/9257))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 22 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9254](https://github.com/paperless-ngx/paperless-ngx/pull/9254))
- Chore(deps): Bump django-filter from 24.3 to 25.1 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9143](https://github.com/paperless-ngx/paperless-ngx/pull/9143))
- Chore(deps-dev): Bump mkdocs-material from 9.6.3 to 9.6.4 in the development group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9142](https://github.com/paperless-ngx/paperless-ngx/pull/9142))
- Dependencies: Updates to jbig2enc 0.30 [@stumpylog](https://github.com/stumpylog) ([#9092](https://github.com/paperless-ngx/paperless-ngx/pull/9092))
- Chore(deps): Bump cryptography from 44.0.0 to 44.0.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9080](https://github.com/paperless-ngx/paperless-ngx/pull/9080))
- Chore(deps): Bump the small-changes group with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9064](https://github.com/paperless-ngx/paperless-ngx/pull/9064))
- Chore(deps-dev): Bump the development group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9061](https://github.com/paperless-ngx/paperless-ngx/pull/9061))
- Chore(deps): Bump the django group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9065](https://github.com/paperless-ngx/paperless-ngx/pull/9065))
- Chore(deps): Bump drf-spectacular-sidecar from 2024.11.1 to 2025.2.1 in the major-versions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9063](https://github.com/paperless-ngx/paperless-ngx/pull/9063))
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9013](https://github.com/paperless-ngx/paperless-ngx/pull/9013))
- Chore(deps): Bump django-soft-delete from 1.0.16 to 1.0.18 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9014](https://github.com/paperless-ngx/paperless-ngx/pull/9014))
- Chore(deps): Bump uuid from 11.0.2 to 11.0.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8992](https://github.com/paperless-ngx/paperless-ngx/pull/8992))
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.2.1 to 1.8.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8991](https://github.com/paperless-ngx/paperless-ngx/pull/8991))
- Chore(deps-dev): Bump @playwright/test from 1.48.2 to 1.50.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8993](https://github.com/paperless-ngx/paperless-ngx/pull/8993))
- Chore(deps-dev): Bump @types/node from 22.8.6 to 22.13.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8989](https://github.com/paperless-ngx/paperless-ngx/pull/8989))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8988](https://github.com/paperless-ngx/paperless-ngx/pull/8988))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 23 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8986](https://github.com/paperless-ngx/paperless-ngx/pull/8986))
</details>
### All App Changes
<details>
<summary>109 changes</summary>
- Fix: ensure only matched scheduled workflows are applied [@shamoon](https://github.com/shamoon) ([#9580](https://github.com/paperless-ngx/paperless-ngx/pull/9580))
- Fix: fix large doc thumb hidden at unexpected screen sizes [@shamoon](https://github.com/shamoon) ([#9559](https://github.com/paperless-ngx/paperless-ngx/pull/9559))
- Fix: fix potential race condition when creating new cf from doc details [@shamoon](https://github.com/shamoon) ([#9542](https://github.com/paperless-ngx/paperless-ngx/pull/9542))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9536](https://github.com/paperless-ngx/paperless-ngx/pull/9536))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9538](https://github.com/paperless-ngx/paperless-ngx/pull/9538))
- Chore(deps-dev): Bump @types/node from 22.13.9 to 22.13.17 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9539](https://github.com/paperless-ngx/paperless-ngx/pull/9539))
- Chore(deps-dev): Bump jest-preset-angular from 14.5.3 to 14.5.4 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9537](https://github.com/paperless-ngx/paperless-ngx/pull/9537))
- Chore(deps-dev): Bump @playwright/test from 1.50.1 to 1.51.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9540](https://github.com/paperless-ngx/paperless-ngx/pull/9540))
- Fix: fix doc link input [@shamoon](https://github.com/shamoon) ([#9533](https://github.com/paperless-ngx/paperless-ngx/pull/9533))
- Enhancement: allow webUI first account signup [@shamoon](https://github.com/shamoon) ([#9500](https://github.com/paperless-ngx/paperless-ngx/pull/9500))
- Fix: fix cf dropdown placement on mobile [@shamoon](https://github.com/shamoon) ([#9508](https://github.com/paperless-ngx/paperless-ngx/pull/9508))
- Chore(deps): Bump django from 5.1.6 to 5.1.7 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9486](https://github.com/paperless-ngx/paperless-ngx/pull/9486))
- Fix: only overwrite existing cf values in workflow if set [@shamoon](https://github.com/shamoon) ([#9459](https://github.com/paperless-ngx/paperless-ngx/pull/9459))
- Fix: fix auto-close when doc update no longer has permissions [@shamoon](https://github.com/shamoon) ([#9453](https://github.com/paperless-ngx/paperless-ngx/pull/9453))
- Enhancement: support more 'not assigned' filtering, refactor [@shamoon](https://github.com/shamoon) ([#9429](https://github.com/paperless-ngx/paperless-ngx/pull/9429))
- Change: better handle permissions in patch requests [@shamoon](https://github.com/shamoon) ([#9393](https://github.com/paperless-ngx/paperless-ngx/pull/9393))
- Fix: use correct filename with webhook [@shamoon](https://github.com/shamoon) ([#9392](https://github.com/paperless-ngx/paperless-ngx/pull/9392))
- Change: sync OIDC groups on first login too [@shamoon](https://github.com/shamoon) ([#9387](https://github.com/paperless-ngx/paperless-ngx/pull/9387))
- Fix: only parse custom field queries when valid [@shamoon](https://github.com/shamoon) ([#9384](https://github.com/paperless-ngx/paperless-ngx/pull/9384))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9372](https://github.com/paperless-ngx/paperless-ngx/pull/9372))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9371](https://github.com/paperless-ngx/paperless-ngx/pull/9371))
- Development: change frontend package manager to pnpm [@shamoon](https://github.com/shamoon) ([#9363](https://github.com/paperless-ngx/paperless-ngx/pull/9363))
- Fix: Allow setting of other Granian options [@stumpylog](https://github.com/stumpylog) ([#9360](https://github.com/paperless-ngx/paperless-ngx/pull/9360))
- Fix: Always clean up INotify [@stumpylog](https://github.com/stumpylog) ([#9359](https://github.com/paperless-ngx/paperless-ngx/pull/9359))
- Tweak: add saved views hint to dashboard [@shamoon](https://github.com/shamoon) ([#9362](https://github.com/paperless-ngx/paperless-ngx/pull/9362))
- Chore(deps): Update ocrmypdf requirement from ~=16.9.0 to ~=16.10.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9348](https://github.com/paperless-ngx/paperless-ngx/pull/9348))
- Chore(deps): Update drf-spectacular-sidecar requirement from ~=2025.2.1 to ~=2025.3.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9347](https://github.com/paperless-ngx/paperless-ngx/pull/9347))
- Chore(deps): Bump the small-changes group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9345](https://github.com/paperless-ngx/paperless-ngx/pull/9345))
- Ensure the directories have been overridden and created for this test [@stumpylog](https://github.com/stumpylog) ([#9354](https://github.com/paperless-ngx/paperless-ngx/pull/9354))
- Fix typo in inactive account template [@ocean90](https://github.com/ocean90) ([#9356](https://github.com/paperless-ngx/paperless-ngx/pull/9356))
- Fix: fix notes serializing in API document response [@shamoon](https://github.com/shamoon) ([#9336](https://github.com/paperless-ngx/paperless-ngx/pull/9336))
- Fix: correct all results with whoosh queries [@shamoon](https://github.com/shamoon) ([#9331](https://github.com/paperless-ngx/paperless-ngx/pull/9331))
- Fix: fix typo in altered migration [@gothicVI](https://github.com/gothicVI) ([#9321](https://github.com/paperless-ngx/paperless-ngx/pull/9321))
- Fix: add account_inactive template / url [@shamoon](https://github.com/shamoon) ([#9322](https://github.com/paperless-ngx/paperless-ngx/pull/9322))
- Chore: Switch from os.path to pathlib.Path [@gothicVI](https://github.com/gothicVI) ([#9060](https://github.com/paperless-ngx/paperless-ngx/pull/9060))
- Enhancement: reorganize dates dropdown, add more relative options [@shamoon](https://github.com/shamoon) ([#9307](https://github.com/paperless-ngx/paperless-ngx/pull/9307))
- Chore: remove popper preventOverflow fix [@shamoon](https://github.com/shamoon) ([#9306](https://github.com/paperless-ngx/paperless-ngx/pull/9306))
- Enhancement: add switch to allow merging non-PDFs with archive version [@shamoon](https://github.com/shamoon) ([#9305](https://github.com/paperless-ngx/paperless-ngx/pull/9305))
- Enhancement: support assigning custom field values in workflows [@shamoon](https://github.com/shamoon) ([#9272](https://github.com/paperless-ngx/paperless-ngx/pull/9272))
- Chore: add codecov frontend test results [@shamoon](https://github.com/shamoon) ([#9296](https://github.com/paperless-ngx/paperless-ngx/pull/9296))
- Chore: Removes undocumented FileInfo [@stumpylog](https://github.com/stumpylog) ([#9298](https://github.com/paperless-ngx/paperless-ngx/pull/9298))
- Chore(deps-dev): Bump the frontend-angular-dependencies group in /src-ui with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9288](https://github.com/paperless-ngx/paperless-ngx/pull/9288))
- Fix: Switches data to content to upload raw bytes/text content [@stumpylog](https://github.com/stumpylog) ([#9293](https://github.com/paperless-ngx/paperless-ngx/pull/9293))
- Chore: Removes the unused Log model and LogFilterSet [@stumpylog](https://github.com/stumpylog) ([#9294](https://github.com/paperless-ngx/paperless-ngx/pull/9294))
- Chore: Combine Python settings files [@stumpylog](https://github.com/stumpylog) ([#9292](https://github.com/paperless-ngx/paperless-ngx/pull/9292))
- Chore(deps-dev): Bump @types/node from 22.13.8 to 22.13.9 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9290](https://github.com/paperless-ngx/paperless-ngx/pull/9290))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9289](https://github.com/paperless-ngx/paperless-ngx/pull/9289))
- Chore: Switch from pipenv to uv [@stumpylog](https://github.com/stumpylog) ([#9251](https://github.com/paperless-ngx/paperless-ngx/pull/9251))
- Enhancement: Add slugify filter in templating [@hwaterke](https://github.com/hwaterke) ([#9269](https://github.com/paperless-ngx/paperless-ngx/pull/9269))
- Fix: handle null workflow body and email subject [@shamoon](https://github.com/shamoon) ([#9271](https://github.com/paperless-ngx/paperless-ngx/pull/9271))
- Chore(deps-dev): Bump @types/node from 22.13.5 to 22.13.8 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9267](https://github.com/paperless-ngx/paperless-ngx/pull/9267))
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9253](https://github.com/paperless-ngx/paperless-ngx/pull/9253))
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.8.0 to 1.9.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9260](https://github.com/paperless-ngx/paperless-ngx/pull/9260))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9256](https://github.com/paperless-ngx/paperless-ngx/pull/9256))
- Chore(deps): Bump uuid from 11.0.5 to 11.1.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9259](https://github.com/paperless-ngx/paperless-ngx/pull/9259))
- Chore(deps-dev): Bump jest-preset-angular from 14.5.1 to 14.5.3 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9255](https://github.com/paperless-ngx/paperless-ngx/pull/9255))
- Chore(deps): Bump rxjs from 7.8.1 to 7.8.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9258](https://github.com/paperless-ngx/paperless-ngx/pull/9258))
- Chore(deps-dev): Bump @types/node from 22.13.0 to 22.13.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9257](https://github.com/paperless-ngx/paperless-ngx/pull/9257))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 22 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9254](https://github.com/paperless-ngx/paperless-ngx/pull/9254))
- Feature: Switch webserver to granian [@stumpylog](https://github.com/stumpylog) ([#9218](https://github.com/paperless-ngx/paperless-ngx/pull/9218))
- Enhancement: relocate and smaller upload widget, dont limit upload list [@shamoon](https://github.com/shamoon) ([#9244](https://github.com/paperless-ngx/paperless-ngx/pull/9244))
- Enhancement: run tasks from system status, report sanity check, simpler classifier check, styling updates [@shamoon](https://github.com/shamoon) ([#9106](https://github.com/paperless-ngx/paperless-ngx/pull/9106))
- Chore: Switch remote version check to HTTPx [@stumpylog](https://github.com/stumpylog) ([#9232](https://github.com/paperless-ngx/paperless-ngx/pull/9232))
- Fix: cleanup saved view references on custom field deletion, auto-refresh views, show error on saved view save [@shamoon](https://github.com/shamoon) ([#9225](https://github.com/paperless-ngx/paperless-ngx/pull/9225))
- Fix: revert thumbnail CSS workaround in favor of GPU workaround [@shamoon](https://github.com/shamoon) ([#9219](https://github.com/paperless-ngx/paperless-ngx/pull/9219))
- Chore: Reduce imports for a slight memory improvement [@stumpylog](https://github.com/stumpylog) ([#9217](https://github.com/paperless-ngx/paperless-ngx/pull/9217))
- Enhancement: include celery log in logs view [@shamoon](https://github.com/shamoon) ([#9214](https://github.com/paperless-ngx/paperless-ngx/pull/9214))
- Enhancement: support default groups for regular and social account signup, syncing on login [@shamoon](https://github.com/shamoon) ([#9039](https://github.com/paperless-ngx/paperless-ngx/pull/9039))
- Enhancement: allow disabling the filesystem consumer [@shamoon](https://github.com/shamoon) ([#9199](https://github.com/paperless-ngx/paperless-ngx/pull/9199))
- Fix: correct split confirm removal [@shamoon](https://github.com/shamoon) ([#9195](https://github.com/paperless-ngx/paperless-ngx/pull/9195))
- Feature: email document [@shamoon](https://github.com/shamoon) ([#8950](https://github.com/paperless-ngx/paperless-ngx/pull/8950))
- Enhancement: webui workflowtrigger source option [@shamoon](https://github.com/shamoon) ([#9170](https://github.com/paperless-ngx/paperless-ngx/pull/9170))
- Fix: saved views do not return to default display fields after setting and then removing [@shamoon](https://github.com/shamoon) ([#9168](https://github.com/paperless-ngx/paperless-ngx/pull/9168))
- Chore(deps): Bump django-filter from 24.3 to 25.1 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9143](https://github.com/paperless-ngx/paperless-ngx/pull/9143))
- Chore(deps-dev): Bump mkdocs-material from 9.6.3 to 9.6.4 in the development group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9142](https://github.com/paperless-ngx/paperless-ngx/pull/9142))
- Fix: correct logged number of deleted documents on trash empty [@shamoon](https://github.com/shamoon) ([#9148](https://github.com/paperless-ngx/paperless-ngx/pull/9148))
- Fix: include account confirm email allauth URL [@shamoon](https://github.com/shamoon) ([#9147](https://github.com/paperless-ngx/paperless-ngx/pull/9147))
- Fix: remove additional scrollbar from popup preview [@shamoon](https://github.com/shamoon) ([#9140](https://github.com/paperless-ngx/paperless-ngx/pull/9140))
- Fix: wrap selected display fields [@shamoon](https://github.com/shamoon) ([#9139](https://github.com/paperless-ngx/paperless-ngx/pull/9139))
- Enhancement: use charfield for webhook url, custom validation [@shamoon](https://github.com/shamoon) ([#9128](https://github.com/paperless-ngx/paperless-ngx/pull/9128))
- Fix: reset documents sort field if user deletes the custom field [@shamoon](https://github.com/shamoon) ([#9127](https://github.com/paperless-ngx/paperless-ngx/pull/9127))
- Chore: more efficient select cf update handler [@shamoon](https://github.com/shamoon) ([#9099](https://github.com/paperless-ngx/paperless-ngx/pull/9099))
- Fix: limit document title length in workflows [@shamoon](https://github.com/shamoon) ([#9085](https://github.com/paperless-ngx/paperless-ngx/pull/9085))
- Feature: Chinese Traditional translation [@LokiHung](https://github.com/LokiHung) ([#9076](https://github.com/paperless-ngx/paperless-ngx/pull/9076))
- Enhancement: Use cached sessions for a minor performance improvement [@stumpylog](https://github.com/stumpylog) ([#9074](https://github.com/paperless-ngx/paperless-ngx/pull/9074))
- Chore(deps): Bump the small-changes group with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9064](https://github.com/paperless-ngx/paperless-ngx/pull/9064))
- Chore(deps-dev): Bump the development group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9061](https://github.com/paperless-ngx/paperless-ngx/pull/9061))
- Chore(deps): Bump the django group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9065](https://github.com/paperless-ngx/paperless-ngx/pull/9065))
- Chore(deps): Bump drf-spectacular-sidecar from 2024.11.1 to 2025.2.1 in the major-versions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9063](https://github.com/paperless-ngx/paperless-ngx/pull/9063))
- Feature: openapi spec, full api browser [@shamoon](https://github.com/shamoon) ([#8948](https://github.com/paperless-ngx/paperless-ngx/pull/8948))
- Fix: include doc link input import in custom fields query dropdown [@shamoon](https://github.com/shamoon) ([#9058](https://github.com/paperless-ngx/paperless-ngx/pull/9058))
- Enhancement: filter by file type [@shamoon](https://github.com/shamoon) ([#8946](https://github.com/paperless-ngx/paperless-ngx/pull/8946))
- Enhancement: add layout options for email conversion [@RazielleS](https://github.com/RazielleS) ([#8907](https://github.com/paperless-ngx/paperless-ngx/pull/8907))
- Chore: Enable ruff FBT [@gothicVI](https://github.com/gothicVI) ([#8645](https://github.com/paperless-ngx/paperless-ngx/pull/8645))
- Feature: better toast notifications management [@shamoon](https://github.com/shamoon) ([#8980](https://github.com/paperless-ngx/paperless-ngx/pull/8980))
- Enhancement: date picker and date filter dropdown improvements [@shamoon](https://github.com/shamoon) ([#9033](https://github.com/paperless-ngx/paperless-ngx/pull/9033))
- Fix: deselect and trigger refresh for deleted documents from bulk operations with delete originals [@shamoon](https://github.com/shamoon) ([#8996](https://github.com/paperless-ngx/paperless-ngx/pull/8996))
- Tweak: improve date matching regex for dates after numbers [@XstreamGit](https://github.com/XstreamGit) ([#8964](https://github.com/paperless-ngx/paperless-ngx/pull/8964))
- Tweak: more accurate classifier last trained time [@shamoon](https://github.com/shamoon) ([#9004](https://github.com/paperless-ngx/paperless-ngx/pull/9004))
- Enhancement: allow setting default pdf zoom [@shamoon](https://github.com/shamoon) ([#9017](https://github.com/paperless-ngx/paperless-ngx/pull/9017))
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9013](https://github.com/paperless-ngx/paperless-ngx/pull/9013))
- Chore(deps): Bump django-soft-delete from 1.0.16 to 1.0.18 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9014](https://github.com/paperless-ngx/paperless-ngx/pull/9014))
- Fix: allow empty email in profile [@shamoon](https://github.com/shamoon) ([#9012](https://github.com/paperless-ngx/paperless-ngx/pull/9012))
- Chore(deps): Bump uuid from 11.0.2 to 11.0.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8992](https://github.com/paperless-ngx/paperless-ngx/pull/8992))
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.2.1 to 1.8.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8991](https://github.com/paperless-ngx/paperless-ngx/pull/8991))
- Chore(deps-dev): Bump @playwright/test from 1.48.2 to 1.50.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8993](https://github.com/paperless-ngx/paperless-ngx/pull/8993))
- Chore(deps-dev): Bump @types/node from 22.8.6 to 22.13.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8989](https://github.com/paperless-ngx/paperless-ngx/pull/8989))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8988](https://github.com/paperless-ngx/paperless-ngx/pull/8988))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 23 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8986](https://github.com/paperless-ngx/paperless-ngx/pull/8986))
</details>
## paperless-ngx 2.14.7
### Features

View File

@@ -404,7 +404,7 @@ set this value to /paperless. No trailing slash!
#### [`PAPERLESS_STATIC_URL=<path>`](#PAPERLESS_STATIC_URL) {#PAPERLESS_STATIC_URL}
: Override the STATIC_URL here. Unless you're hosting Paperless off a
specific path like /paperless/, you probably don't need to change this.
subdomain like /paperless/, you probably don't need to change this.
If you do change it, be sure to include the trailing slash.
Defaults to "/static/".
@@ -629,13 +629,7 @@ If both the [PAPERLESS_ACCOUNT_DEFAULT_GROUPS](#PAPERLESS_ACCOUNT_DEFAULT_GROUPS
!!! note
If you do not have a working email server set up this will be set to 'none'.
#### [`PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS=<bool>`](#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS) {#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS}
: See the relevant [django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html)
Defaults to True (from allauth)
If you do not have a working email server set up you should set this to 'none'.
#### [`PAPERLESS_DISABLE_REGULAR_LOGIN=<bool>`](#PAPERLESS_DISABLE_REGULAR_LOGIN) {#PAPERLESS_DISABLE_REGULAR_LOGIN}
@@ -1063,9 +1057,9 @@ be used with caution!
## Document Consumption {#consume_config}
#### [`PAPERLESS_CONSUMER_DISABLE`](#PAPERLESS_CONSUMER_DISABLE) {#PAPERLESS_CONSUMER_DISABLE}
#### [`PAPERLESS_CONSUMER_DISABLE=<bool>`](#PAPERLESS_CONSUMER_DISABLE) {#PAPERLESS_CONSUMER_DISABLE}
: If set (to anything), this completely disables the directory-based consumer in docker. If you don't plan to consume documents
: Completely disable the directory-based consumer in docker. If you don't plan to consume documents
via the consumption directory, you can disable the consumer to save resources.
#### [`PAPERLESS_CONSUMER_DELETE_DUPLICATES=<bool>`](#PAPERLESS_CONSUMER_DELETE_DUPLICATES) {#PAPERLESS_CONSUMER_DELETE_DUPLICATES}
@@ -1676,7 +1670,7 @@ started by the container.
## Email sending
Setting an SMTP server for the backend will allow you to use the Email workflow action, send documents from the UI as well as reset your
Setting an SMTP server for the backend will allow you to reset your
password. All of these options come from their similarly-named [Django settings](https://docs.djangoproject.com/en/4.2/ref/settings/#email-host)
#### [`PAPERLESS_EMAIL_HOST=<str>`](#PAPERLESS_EMAIL_HOST) {#PAPERLESS_EMAIL_HOST}

View File

@@ -84,7 +84,7 @@ first-time setup.
$ uv run pre-commit install
```
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
6. Apply migrations and create a superuser for your development instance:
```bash
# src/

View File

@@ -112,6 +112,30 @@ able to run paperless, you're a bit on your own. If you can't run the
docker image, the documentation has instructions for bare metal
installs.
## _How do I proxy this with NGINX?_
**A:** See [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx).
## _How do I get WebSocket support with Apache mod_wsgi_?
**A:** `mod_wsgi` by itself does not support ASGI. Paperless will
continue to work with WSGI, but certain features such as status
notifications about document consumption won't be available.
If you want to continue using `mod_wsgi`, you will have to run an
ASGI-enabled web server as well that processes WebSocket connections,
and configure Apache to redirect WebSocket connections to this server.
Multiple options for ASGI servers exist:
- `gunicorn` with `uvicorn` as the worker implementation (the default
of paperless)
- `daphne` as a standalone server, which is the reference
implementation for ASGI.
- `uvicorn` as a standalone server
You may also find the [Django documentation](https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/) on ASGI
useful to review.
## _What about the Redis licensing change and using one of the open source forks_?
Currently (October 2024), forks of Redis such as Valkey or Redirect are not officially supported by our upstream

View File

@@ -197,7 +197,7 @@ People interested in continuing the work on paperless-ngx are encouraged to reac
### Translation
Paperless-ngx is available in many languages that are coordinated on [Crowdin](https://crowdin.com/project/paperless-ngx). If you want to help out by translating paperless-ngx into your language, please head over to the [Paperless-ngx project at Crowdin](https://crowdin.com/project/paperless-ngx), and thank you!
Paperless-ngx is available in many languages that are coordinated on [Crowdin](https://crwd.in/paperless-ngx). If you want to help out by translating paperless-ngx into your language, please head over to the [Paperless-ngx project at Crowdin](https://crwd.in/paperless-ngx), and thank you!
## Scanners & Software

View File

@@ -131,11 +131,26 @@ account. The script essentially automatically performs the steps described in [D
by default but you can change the image to pull from Docker Hub by changing the `image`
line to `image: paperlessngx/paperless-ngx:latest`.
6. Run `docker compose up -d`. This will create and start the necessary containers.
6. To be able to login, you will need a "superuser". To create it,
execute the following command:
7. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
(or similar, depending on your configuration). When you first access the web interface, you will be
prompted to create a superuser account.
```shell-session
docker compose run --rm webserver createsuperuser
```
or using docker exec from within the container:
```shell-session
python3 manage.py createsuperuser
```
This will guide you through the superuser setup.
7. Run `docker compose up -d`. This will create and start the necessary containers.
8. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
(or similar, depending on your configuration). Use the superuser credentials you have
created in the previous step to login.
### Build the Docker image yourself {#docker_build}
@@ -371,14 +386,15 @@ are released, dependency support is confirmed, etc.
dependencies for Postgres or Mariadb. You can select those extras with `--extra <EXTRA>`
or all with `--all-extras`
9. Go to `/opt/paperless/src`, and execute the following command:
9. Go to `/opt/paperless/src`, and execute the following commands:
```bash
# This creates the database schema.
sudo -Hu paperless python3 manage.py migrate
```
When you first access the web interface you will be prompted to create a superuser account.
# This creates your first paperless user
sudo -Hu paperless python3 manage.py createsuperuser
```
10. Optional: Test that paperless is working by executing
@@ -692,8 +708,7 @@ Paperless runs on Raspberry Pi. However, some things are rather slow on
the Pi and configuring some options in paperless can help improve
performance immensely:
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
if you encounter issues with SQLite locking.
- Stick with SQLite to save some resources.
- If you do not need the filesystem-based consumer, consider disabling it
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will

View File

@@ -292,9 +292,7 @@ many workers attempting to access the database simultaneously.
Consider changing to the PostgreSQL database if you will be processing
many documents at once often. Otherwise, try tweaking the
[`PAPERLESS_DB_TIMEOUT`](configuration.md#PAPERLESS_DB_TIMEOUT) setting to allow more time for the database to
unlock. Additionally, you can change your SQLite database to use ["Write-Ahead Logging"](https://sqlite.org/wal.html).
These changes may have minor performance implications but can help
prevent database locking issues.
unlock. This may have minor performance implications.
## granian fails to start with "is not a valid port number"

View File

@@ -89,7 +89,7 @@ and more. These areas allow you to view, add, edit, delete and manage permission
for these objects. You can also manage saved views, mail accounts, mail rules,
workflows and more from the management sections.
## Adding documents to Paperless-ngx
## Adding documents to paperless
Once you've got Paperless setup, you need to start feeding documents
into it. When adding documents to paperless, it will perform the
@@ -115,8 +115,7 @@ following operations on your documents:
No matter which options you choose, Paperless will always store the
original document that it found in the consumption directory or in the
mail and will never overwrite that document (except when using certain
document actions, which make that clear). Archived versions are
mail and will never overwrite that document. Archived versions are
stored alongside the original versions. Any files found in the
consumption directory will stored inside the Paperless-ngx file
structure and will not be retained in the consumption directory.
@@ -160,7 +159,7 @@ process.
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects) for a user-maintained list of related projects and
software (e.g. for mobile devices) that is compatible with Paperless-ngx.
### Incoming Email {#incoming-mail}
### Email {#usage-email}
You can tell paperless-ngx to consume documents from your email
accounts. This is a very flexible and powerful feature, if you regularly
@@ -261,31 +260,6 @@ Once setup, navigating to the email settings page in Paperless-ngx will allow yo
You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads)
for details.
## Sharing documents from Paperless-ngx
Paperless-ngx supports sharing documents with other users by assigning them [permissions](#object-permissions)
to the document. Document files can also be shared externally via [share links](#share-links), [email](#email-sharing)
or using [email](#workflow-action-email) or [webhook](#workflow-action-webhook) actions in workflows.
### Share Links
"Share links" are shareable public links to files and can be created and managed under the 'Send' button on the document detail screen.
- Share links do not require a user to login and thus link directly to a file.
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
- Links can optionally have an expiration time set.
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
!!! tip
If your paperless-ngx instance is behind a reverse-proxy you may want to create an exception to bypass any authentication layers that are part of your setup in order to make links truly publicly-accessible. Of course, do so with caution.
### Email Sharing {#email-sharing}
Paperless-ngx supports directly sending documents via email. If an email server has been [configured](configuration.md#email-sending)
the "Send" button on the document detail page will include an "Email" option. You can also share files via email automatically by using
a [workflow action](#workflow-action-email).
## Permissions
Permissions in Paperless-ngx are based around ['global' permissions](#global-permissions) as well as
@@ -395,7 +369,7 @@ fields and permissions, which will be merged.
### Workflow Triggers
#### Types {#workflow-trigger-types}
#### Types
Currently, there are three events that correspond to workflow trigger 'types':
@@ -407,8 +381,7 @@ Currently, there are three events that correspond to workflow trigger 'types':
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
tags, doc type, or correspondent.
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
offsets will trigger before the date, negative offsets will trigger after).
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date.
The following flow diagram illustrates the three document trigger types:
@@ -456,11 +429,11 @@ Workflows allow you to filter by:
### Workflow Actions
#### Types {#workflow-action-types}
#### Types
The following workflow action types are available:
##### Assignment {#workflow-action-assignment}
##### Assignment
"Assignment" actions can assign:
@@ -470,7 +443,7 @@ The following workflow action types are available:
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
##### Removal {#workflow-action-removal}
##### Removal
"Removal" actions can remove either all of or specific sets of the following:
@@ -479,7 +452,7 @@ The following workflow action types are available:
- View and / or edit permissions
- Custom fields
##### Email {#workflow-action-email}
##### Email
"Email" actions can send documents via email. This action requires a mail server to be [configured](configuration.md#email-sending). You can specify:
@@ -487,7 +460,7 @@ The following workflow action types are available:
- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
- Whether to include the document as an attachment
##### Webhook {#workflow-action-webhook}
##### Webhook
"Webhook" actions send a POST request to a specified URL. You can specify:
@@ -571,6 +544,19 @@ The following custom field types are supported:
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
- `Select`: a pre-defined list of strings from which the user can choose
## Share Links
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
- Share links do not require a user to login and thus link directly to a file.
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
- Links can optionally have an expiration time set.
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
!!! tip
If your paperless-ngx instance is behind a reverse-proxy you may want to create an exception to bypass any authentication layers that are part of your setup in order to make links truly publicly-accessible. Of course, do so with caution.
## PDF Actions
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):

View File

@@ -11,12 +11,14 @@ theme:
toggle:
icon: material/brightness-auto
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
@@ -58,17 +60,17 @@ markdown_extensions:
emoji_generator: !!python/name:material.extensions.emoji.to_svg
strict: true
nav:
- index.md
- setup.md
- 'Basic Usage': usage.md
- configuration.md
- administration.md
- advanced_usage.md
- 'REST API': api.md
- development.md
- 'FAQs': faq.md
- troubleshooting.md
- changelog.md
- index.md
- setup.md
- 'Basic Usage': usage.md
- configuration.md
- administration.md
- advanced_usage.md
- 'REST API': api.md
- development.md
- 'FAQs': faq.md
- troubleshooting.md
- changelog.md
copyright: Copyright &copy; 2016 - 2023 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
extra:
social:
@@ -81,5 +83,5 @@ extra:
plugins:
- search
- glightbox:
skip_classes:
- no-lightbox
skip_classes:
- no-lightbox

View File

@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "2.16.0"
version = "2.14.7"
description = "A community-supported supercharged version of paperless: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.10"
@@ -16,20 +16,20 @@ classifiers = [
dependencies = [
"bleach~=6.2.0",
"celery[redis]~=5.5.1",
"celery[redis]~=5.4.0",
"channels~=4.2",
"channels-redis~=4.2",
"concurrent-log-handler~=0.9.25",
"dateparser~=1.2",
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.1.7",
"django~=5.1.6",
"django-allauth[socialaccount,mfa]~=65.4.0",
"django-auditlog~=3.1.2",
"django-celery-results~=2.6.0",
"django-auditlog~=3.0.0",
"django-celery-results~=2.5.1",
"django-compression-middleware~=0.5.0",
"django-cors-headers~=4.7.0",
"django-extensions~=4.1",
"django-extensions~=3.2.3",
"django-filter~=25.1",
"django-guardian~=2.4.0",
"django-multiselectfield~=0.1.13",
@@ -37,11 +37,11 @@ dependencies = [
"djangorestframework~=3.15",
"djangorestframework-guardian~=0.3.0",
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2025.4.1",
"drf-spectacular-sidecar~=2025.3.1",
"drf-writable-nested~=0.7.1",
"filelock~=3.18.0",
"filelock~=3.17.0",
"flower~=2.0.1",
"gotenberg-client~=0.10.0",
"gotenberg-client~=0.9.0",
"httpx-oauth~=0.16",
"imap-tools~=1.10.0",
"inotifyrecursive~=0.3",
@@ -52,12 +52,12 @@ dependencies = [
"pathvalidate~=3.2.3",
"pdf2image~=1.17.0",
"python-dateutil~=2.9.0",
"python-dotenv~=1.1.0",
"python-dotenv~=1.0.1",
"python-gnupg~=0.5.4",
"python-ipware~=3.0.0",
"python-magic~=0.4.27",
"pyzbar~=0.1.9",
"rapidfuzz~=3.13.0",
"rapidfuzz~=3.12.1",
"redis[hiredis]~=5.2.1",
"scikit-learn~=1.6.1",
"setproctitle~=1.3.4",
@@ -65,7 +65,7 @@ dependencies = [
"tqdm~=4.67.1",
"watchdog~=6.0",
"whitenoise~=6.9",
"whoosh-reloaded>=2.7.5",
"whoosh~=2.7",
"zxing-cpp~=2.3.0",
]
@@ -78,7 +78,7 @@ optional-dependencies.postgres = [
"psycopg-c==3.2.5",
]
optional-dependencies.webserver = [
"granian[uvloop]~=2.2.0",
"granian~=2.0.1",
]
[dependency-groups]
@@ -227,9 +227,27 @@ lint.per-file-ignores."src/documents/tests/test_consumer.py" = [
lint.per-file-ignores."src/documents/tests/test_file_handling.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_management.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_management_consumer.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_management_exporter.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_migration_archive_files.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_migration_document_pages_count.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_migration_mime_type.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_sanity_check.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/views.py" = [
"PTH",
] # TODO Enable & remove

View File

@@ -1,13 +0,0 @@
export const getDocument = jest.fn(() => ({
promise: Promise.resolve({ numPages: 3 }),
}))
export const GlobalWorkerOptions = { workerSrc: '' }
export const VerbosityLevel = { ERRORS: 0 }
globalThis.pdfjsLib = {
getDocument,
GlobalWorkerOptions,
VerbosityLevel,
AbortException: class AbortException extends Error {},
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,28 @@
{
"name": "paperless-ngx-ui",
"version": "2.16.0",
"name": "paperless-ui",
"version": "0.0.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test --no-watch --coverage",
"lint": "ng lint"
"lint": "ng lint",
"postinstall": "patch-package"
},
"private": true,
"dependencies": {
"@angular/cdk": "^19.2.14",
"@angular/common": "~19.2.9",
"@angular/compiler": "~19.2.9",
"@angular/core": "~19.2.9",
"@angular/forms": "~19.2.9",
"@angular/localize": "~19.2.9",
"@angular/platform-browser": "~19.2.9",
"@angular/platform-browser-dynamic": "~19.2.9",
"@angular/router": "~19.2.9",
"@angular/cdk": "^19.2.2",
"@angular/common": "~19.2.1",
"@angular/compiler": "~19.2.1",
"@angular/core": "~19.2.1",
"@angular/forms": "~19.2.1",
"@angular/localize": "~19.2.1",
"@angular/platform-browser": "~19.2.1",
"@angular/platform-browser-dynamic": "~19.2.1",
"@angular/router": "~19.2.1",
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
"@ng-select/ng-select": "^14.7.0",
"@ng-select/ng-select": "^14.2.3",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3",
@@ -32,6 +33,7 @@
"ngx-color": "^10.0.0",
"ngx-cookie-service": "^19.1.2",
"ngx-device-detector": "^9.0.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
@@ -40,31 +42,32 @@
"zone.js": "^0.15.0"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^19.0.1",
"@angular-builders/jest": "^19.0.1",
"@angular-devkit/build-angular": "^19.2.10",
"@angular-devkit/core": "^19.2.10",
"@angular-devkit/schematics": "^19.2.10",
"@angular-eslint/builder": "19.3.0",
"@angular-eslint/eslint-plugin": "19.3.0",
"@angular-eslint/eslint-plugin-template": "19.3.0",
"@angular-eslint/schematics": "19.3.0",
"@angular-eslint/template-parser": "19.3.0",
"@angular/cli": "~19.2.10",
"@angular/compiler-cli": "~19.2.9",
"@angular-builders/custom-webpack": "^19.0.0",
"@angular-builders/jest": "^19.0.0",
"@angular-devkit/build-angular": "^19.2.1",
"@angular-devkit/core": "^19.2.1",
"@angular-devkit/schematics": "^19.2.1",
"@angular-eslint/builder": "19.2.1",
"@angular-eslint/eslint-plugin": "19.2.1",
"@angular-eslint/eslint-plugin-template": "19.2.1",
"@angular-eslint/schematics": "19.2.1",
"@angular-eslint/template-parser": "19.2.1",
"@angular/cli": "~19.2.1",
"@angular/compiler-cli": "~19.2.1",
"@codecov/webpack-plugin": "^1.9.0",
"@playwright/test": "^1.51.1",
"@playwright/test": "^1.50.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.3",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"@typescript-eslint/utils": "^8.31.1",
"eslint": "^9.25.1",
"@types/node": "^22.13.9",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"@typescript-eslint/utils": "^8.26.1",
"eslint": "^9.22.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^14.5.5",
"jest-preset-angular": "^14.5.3",
"jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0",
"prettier-plugin-organize-imports": "^4.1.0",
"ts-node": "~10.9.1",
"typescript": "^5.5.4"

File diff suppressed because one or more lines are too long

3867
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -121,4 +121,19 @@ HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext
>jest.fn()
jest.mock('pdfjs-dist')
// pdfjs
jest.mock('pdfjs-dist', () => ({
getDocument: jest.fn(() => ({
promise: Promise.resolve({ numPages: 3 }),
})),
GlobalWorkerOptions: { workerSrc: '' },
VerbosityLevel: { ERRORS: 0 },
globalThis: {
pdfjsLib: {
GlobalWorkerOptions: {
workerSrc: '',
},
},
},
}))
jest.mock('pdfjs-dist/web/pdf_viewer', () => ({}))

View File

@@ -12,7 +12,7 @@ import { DocumentAsnComponent } from './components/document-asn/document-asn.com
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { CustomFieldsListComponent } from './components/manage/custom-fields-list/custom-fields-list.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
@@ -239,7 +239,7 @@ export const routes: Routes = [
},
{
path: 'customfields',
component: CustomFieldsComponent,
component: CustomFieldsListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {

View File

@@ -9,6 +9,7 @@ import {
import { Router, RouterModule } from '@angular/router'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { Subject } from 'rxjs'
import { routes } from './app-routing.module'
@@ -42,6 +43,7 @@ describe('AppComponent', () => {
imports: [
TourNgBootstrapModule,
RouterModule.forRoot(routes),
NgxFileDropModule,
NgbModalModule,
AppComponent,
ToastsComponent,

View File

@@ -105,9 +105,9 @@ describe('ConfigComponent', () => {
it('should support JSON validation for e.g. user_args', () => {
component.configForm.patchValue({ user_args: '{ foo bar }' })
expect(component.errors['user_args']).toEqual('Invalid JSON')
expect(component.errors).toEqual({ user_args: 'Invalid JSON' })
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
expect(component.errors['user_args']).toBeNull()
expect(component.errors).toEqual({ user_args: null })
})
it('should upload file, show error if necessary', () => {

View File

@@ -15,7 +15,7 @@
</svg>
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
@if (customAppTitle?.length) {
<div class="d-flex flex-column align-items-start custom-title">
<div class="d-flex flex-column align-items-start">
<span class="title">{{customAppTitle}}</span>
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
</div>

View File

@@ -244,7 +244,7 @@ main {
}
}
@media screen and (min-width: 366px) and (max-width: 768px) {
@media screen and (max-width: 768px) {
.navbar-toggler {
// compensate for 2 buttons on the right
margin-right: 45px;
@@ -257,13 +257,6 @@ main {
}
}
@media screen and (max-width: 345px) {
.custom-title {
max-width: 110px;
overflow: hidden;
}
}
:host ::ng-deep .dropdown.show .dropdown-toggle,
:host ::ng-deep .dropdown-toggle:hover {
opacity: 0.7;

View File

@@ -89,7 +89,7 @@
@if (searchResults?.documents.length) {
<h6 class="dropdown-header" i18n="@@searchResults.documents">Documents</h6>
@for (document of searchResults.documents; track document.id) {
<ng-container *ngTemplateOutlet="resultItemTemplate; context: {item: document, nameProp: 'title', type: DataType.Document, icon: 'file-text', date: document.created}"></ng-container>
<ng-container *ngTemplateOutlet="resultItemTemplate; context: {item: document, nameProp: 'title', type: DataType.Document, icon: 'file-text', date: document.added}"></ng-container>
}
}
@if (searchResults?.saved_views.length) {

View File

@@ -405,7 +405,7 @@ describe('GlobalSearchComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(object as any)
editDialog.succeeded.emit(true)
expect(toastInfoSpy).toHaveBeenCalled()
})
@@ -456,7 +456,7 @@ describe('GlobalSearchComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(searchResults.tags[0] as any)
editDialog.succeeded.emit(true)
expect(toastInfoSpy).toHaveBeenCalled()
})

View File

@@ -1,4 +1,4 @@
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions" placement="bottom-end">
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)">
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
<i-bs name="ui-radios"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Custom Fields</ng-container></div>

View File

@@ -21,7 +21,6 @@ import {
} from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { pngxPopperOptions } from 'src/app/utils/popper-options'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@@ -37,8 +36,6 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
],
})
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
public popperOptions = pngxPopperOptions
@Input()
documentId: number

View File

@@ -11,7 +11,7 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
@if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
<small class="d-block mt-n2 fst-italic text-muted" i18n>Data type cannot be changed after a field is created</small>
}
<div [formGroup]="objectForm.controls.extra_data">
@switch (objectForm.get('data_type').value) {
@@ -39,6 +39,14 @@
}
}
</div>
<hr/>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@@ -21,6 +21,7 @@ import {
CustomFieldDataType,
DATA_TYPE_LABELS,
} from 'src/app/data/custom-field'
import { MATCH_NONE, MATCHING_ALGORITHMS } from 'src/app/data/matching-model'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -28,6 +29,27 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component'
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
const FIELDS_WITH_DISCRETE_MATCHING = [
CustomFieldDataType.Boolean,
CustomFieldDataType.Select,
]
const MATCHING_ALGORITHMS_FOR_ALL_FIELDS = [
// MATCH_NONE
MATCHING_ALGORITHMS[6],
// MATCH_REGEX
MATCHING_ALGORITHMS[4],
]
const MATCHING_ALGORITHMS_FOR_DISCRETE_FIELDS = [
// MATCH_NONE
MATCHING_ALGORITHMS[6],
// MATCH_AUTO
MATCHING_ALGORITHMS[0],
// MATCH_REGEX
MATCHING_ALGORITHMS[4],
]
@Component({
selector: 'pngx-custom-field-edit-dialog',
templateUrl: './custom-field-edit-dialog.component.html',
@@ -107,6 +129,9 @@ export class CustomFieldEditDialogComponent
select_options: new FormArray([]),
default_currency: new FormControl(null),
}),
matching_algorithm: new FormControl(MATCH_NONE),
match: new FormControl(''),
is_insensitive: new FormControl(true),
})
}
@@ -127,4 +152,15 @@ export class CustomFieldEditDialogComponent
public removeSelectOption(index: number) {
this.selectOptions.removeAt(index)
}
public getMatchingAlgorithms() {
if (
FIELDS_WITH_DISCRETE_MATCHING.includes(this.getForm().value.data_type) ||
FIELDS_WITH_DISCRETE_MATCHING.includes(this.object?.data_type)
) {
return MATCHING_ALGORITHMS_FOR_DISCRETE_FIELDS
} else {
return MATCHING_ALGORITHMS_FOR_ALL_FIELDS
}
}
}

View File

@@ -47,7 +47,7 @@ export abstract class EditDialogComponent<
object: T
@Output()
succeeded = new EventEmitter<T>()
succeeded = new EventEmitter()
@Output()
failed = new EventEmitter()

View File

@@ -123,15 +123,7 @@
<p class="small" i18n>Set scheduled trigger offset and which date field to use.</p>
<div class="row">
<div class="col-4">
<pngx-input-number
i18n-title
title="Offset days"
formControlName="schedule_offset_days"
[showAdd]="false"
[error]="error?.schedule_offset_days"
hint="Positive values will trigger the workflow before the date, negative values after."
i18n-hint
></pngx-input-number>
<pngx-input-number i18n-title title="Offset days" formControlName="schedule_offset_days" [showAdd]="false" [error]="error?.schedule_offset_days"></pngx-input-number>
</div>
<div class="col-4">
<pngx-input-select i18n-title title="Relative to" formControlName="schedule_date_field" [items]="scheduleDateFieldOptions" [error]="error?.schedule_date_field"></pngx-input-select>

View File

@@ -492,9 +492,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
component.selectionModel.items = items
component.selectionModel = selectionModel
component.selectionModel.intersection = Intersection.Include
console.log(component.selectionModel.items[0])
component.selectionModel.set(null, ToggleableItemState.Selected)
component.selectionModel.intersection = Intersection.Exclude
component.selectionModel.toggleIntersection()
console.log(component.selectionModel)
expect(component.selectionModel.getExcludedItems()).toEqual([
negativeNullItem,
])

View File

@@ -586,8 +586,6 @@ export class FilterableDropdownComponent
this.selectionModel.reset()
this.modelIsDirty = false
}
this.selectionModel.singleSelect =
this.editing && !this.selectionModel.manyToOne
this.opened.next(this)
} else {
if (this.creating) {

View File

@@ -30,24 +30,25 @@
[placeholder]="placeholder"
[notFoundText]="notFoundText"
[multiple]="true"
bindValue="id"
[compareWith]="compareDocuments"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="loading"
[typeahead]="documentsInput$"
(mousedown)="$event.stopImmediatePropagation()"
(change)="onChange(selectedDocumentIDs)">
(change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center">
@if (!disabled) {
<button class="btn p-0 lh-1" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title><i-bs name="x"></i-bs></button>
<button class="btn p-0 lh-1" (click)="unselect(document)" title="Remove link" i18n-title><i-bs name="x"></i-bs></button>
}
@if (document.title) {
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();" title="Open link" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{document.title}}</span>
</a>
} @else {
<span class="badge bg-light text-muted" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title>
<span class="badge bg-light text-muted" (click)="unselect(document)" title="Remove link" i18n-title>
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle-fill"></i-bs>&nbsp;<span i18n>Not found</span>
</span>
}

View File

@@ -74,11 +74,6 @@ describe('DocumentLinkComponent', () => {
expect(component.selectedDocuments).toEqual([documents[1], documents[0]])
})
it('should retrieve document IDs from selected documents', () => {
component.selectedDocuments = documents
expect(component.selectedDocumentIDs).toEqual([1, 12, 16, 23])
})
it('should search API on select text input', () => {
const listSpy = jest.spyOn(documentService, 'listFiltered')
listSpy.mockImplementation(

View File

@@ -71,10 +71,6 @@ export class DocumentLinkComponent
@Input()
placeholder: string = $localize`Search for documents`
get selectedDocumentIDs(): number[] {
return this.selectedDocuments.map((d) => d.id)
}
constructor(private documentsService: DocumentService) {
super()
}
@@ -94,8 +90,8 @@ export class DocumentLinkComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((documentResults) => {
this.loading = false
this.selectedDocuments = documentIDs.map(
(id) => documentResults.results.find((d) => d.id === id) ?? {}
this.selectedDocuments = documentIDs.map((id) =>
documentResults.results.find((d) => d.id === id)
)
super.writeValue(documentIDs)
})

View File

@@ -17,7 +17,7 @@
(change)="onChange(value)">
<ng-template ng-label-tmp let-item="item">
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag(item.id)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove tag" i18n-title>
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
<i-bs name="x" style="margin-inline-end: 1px;"></i-bs>
@if (item.id && tags) {
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
@@ -33,7 +33,7 @@
</ng-template>
</ng-select>
@if (allowCreate && !hideAddButton) {
<button class="btn btn-outline-secondary" type="button" (click)="createTag(null, true)" [disabled]="disabled">
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
</button>
}

View File

@@ -154,11 +154,11 @@ describe('TagsComponent', () => {
it('support remove tags', () => {
component.tags = tags
component.value = [1, 2]
component.removeTag(2)
component.removeTag(new PointerEvent('point'), 2)
expect(component.value).toEqual([1])
component.disabled = true
component.removeTag(1)
component.removeTag(new PointerEvent('point'), 1)
expect(component.value).toEqual([1])
})

View File

@@ -118,10 +118,13 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
}
}
removeTag(tagID: number) {
removeTag(event: PointerEvent, id: number) {
if (this.disabled) return
let index = this.value.indexOf(tagID)
// prevent opening dropdown
event.stopImmediatePropagation()
let index = this.value.indexOf(id)
if (index > -1) {
let oldValue = this.value
oldValue.splice(index, 1)
@@ -130,7 +133,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
}
}
createTag(name: string = null, add: boolean = false) {
createTag(name: string = null) {
var modal = this.modalService.open(TagEditDialogComponent, {
backdrop: 'static',
})
@@ -143,10 +146,9 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
return firstValueFrom(
(modal.componentInstance as TagEditDialogComponent).succeeded.pipe(
first(),
tap((newTag) => {
tap(() => {
this.tagService.listAll().subscribe((tags) => {
this.tags = tags.results
add && this.addTag(newTag.id)
})
})
)

View File

@@ -43,7 +43,7 @@
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
}
@case (DisplayField.CREATED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created | customDate}}</a>
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a>
}
@case (DisplayField.TITLE) {
<a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>

View File

@@ -82,20 +82,10 @@ describe('UploadFileWidgetComponent', () => {
})
it('should upload files', () => {
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
const fileInput = fixture.debugElement.query(By.css('input'))
jest.spyOn(fileInput.nativeElement, 'files', 'get').mockReturnValue({
item: () => file,
length: 1,
[Symbol.iterator]: () => ({
next: () => ({ done: false, value: file }),
}),
} as any)
fileInput.nativeElement.dispatchEvent(new Event('change'))
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
fixture.debugElement
.query(By.css('input'))
.nativeElement.dispatchEvent(new Event('change'))
expect(uploadSpy).toHaveBeenCalled()
})

View File

@@ -134,11 +134,9 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
}
public onFileSelected(event: Event) {
const files = (event.target as HTMLInputElement).files
for (let i = 0; i < files?.length; i++) {
const file = files.item(i)
file && this.uploadDocumentsService.uploadFile(file)
}
this.uploadDocumentsService.uploadFiles(
(event.target as HTMLInputElement).files
)
}
get slimSidebarEnabled(): boolean {

View File

@@ -9,9 +9,9 @@
}
<div class="input-group input-group-sm me-md-5 d-none d-md-flex">
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
<select class="form-select" (change)="setZoom($event.target.value)" [ngModel]="currentZoom">
<select class="form-select" (change)="setZoom($event.target.value)">
@for (setting of zoomSettings; track setting) {
<option [value]="setting">
<option [value]="setting" [attr.selected]="isZoomSelected(setting) ? 'selected' : null">
{{ getZoomSettingTitle(setting) }}
</option>
}
@@ -129,8 +129,8 @@
<div>
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
<pngx-input-date i18n-title title="Date created" formControlName="created" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error?.created"></pngx-input-date>
<pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error?.created_date"></pngx-input-date>
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)"
(createNew)="createCorrespondent($event)" [hideAddButton]="createDisabled(DataType.Correspondent)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
<pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)"

View File

@@ -1,10 +1,5 @@
import { DatePipe } from '@angular/common'
import {
HttpHeaders,
HttpResponse,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
HttpTestingController,
provideHttpClientTesting,
@@ -456,11 +451,11 @@ describe('DocumentDetailComponent', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
patchSpy.mockImplementation((o) => of(doc))
updateSpy.mockImplementation((o) => of(doc))
component.save(true)
expect(patchSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
@@ -471,11 +466,11 @@ describe('DocumentDetailComponent', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
patchSpy.mockImplementation((o) => of(doc))
updateSpy.mockImplementation((o) => of(doc))
component.save()
expect(patchSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
@@ -487,12 +482,12 @@ describe('DocumentDetailComponent', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showError')
const error = new Error('failed to save')
patchSpy.mockImplementation(() => throwError(() => error))
updateSpy.mockImplementation(() => throwError(() => error))
component.save()
expect(patchSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
'Error saving document "Doc 3"',
@@ -505,13 +500,13 @@ describe('DocumentDetailComponent', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
patchSpy.mockImplementation(() =>
updateSpy.mockImplementation(() =>
throwError(() => new Error('failed to save'))
)
component.save(true)
expect(patchSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
@@ -522,8 +517,8 @@ describe('DocumentDetailComponent', () => {
initNormally()
const nextDocId = 100
component.title = 'Foo Bar'
const patchSpy = jest.spyOn(documentService, 'patch')
patchSpy.mockReturnValue(of(doc))
const updateSpy = jest.spyOn(documentService, 'update')
updateSpy.mockReturnValue(of(doc))
const nextSpy = jest.spyOn(documentListViewService, 'getNext')
nextSpy.mockReturnValue(of(nextDocId))
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
@@ -531,7 +526,7 @@ describe('DocumentDetailComponent', () => {
const navigateSpy = jest.spyOn(router, 'navigate')
component.saveEditNext()
expect(patchSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalled()
expect(navigateSpy).toHaveBeenCalledWith(['documents', nextDocId])
expect
})
@@ -541,12 +536,12 @@ describe('DocumentDetailComponent', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showError')
const error = new Error('failed to save')
patchSpy.mockImplementation(() => throwError(() => error))
updateSpy.mockImplementation(() => throwError(() => error))
component.saveEditNext()
expect(patchSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Error saving document', error)
})
@@ -791,9 +786,14 @@ describe('DocumentDetailComponent', () => {
it('should select correct zoom setting in dropdown', () => {
initNormally()
component.setZoom(ZoomSetting.PageFit)
expect(component.currentZoom).toEqual(ZoomSetting.PageFit)
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeTruthy()
expect(component.isZoomSelected(ZoomSetting.One)).toBeFalsy()
component.setZoom(ZoomSetting.PageWidth)
expect(component.isZoomSelected(ZoomSetting.One)).toBeTruthy()
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy()
component.setZoom(ZoomSetting.Quarter)
expect(component.currentZoom).toEqual(ZoomSetting.Quarter)
expect(component.isZoomSelected(ZoomSetting.Quarter)).toBeTruthy()
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy()
})
it('should support updating notes dynamically', () => {
@@ -965,10 +965,10 @@ describe('DocumentDetailComponent', () => {
expect(fixture.debugElement.nativeElement.textContent).toContain(
customFields[1].name
)
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
component.save(true)
expect(patchSpy.mock.lastCall[0].custom_fields).toHaveLength(2)
expect(patchSpy.mock.lastCall[0].custom_fields[1]).toEqual({
expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(2)
expect(updateSpy.mock.lastCall[0].custom_fields[1]).toEqual({
field: customFields[1].id,
value: null,
})
@@ -985,51 +985,13 @@ describe('DocumentDetailComponent', () => {
expect(
fixture.debugElement.query(By.css('form')).nativeElement.textContent
).not.toContain('Field 1')
const patchSpy = jest.spyOn(documentService, 'patch')
const updateSpy = jest.spyOn(documentService, 'update')
component.save(true)
expect(patchSpy.mock.lastCall[0].custom_fields).toHaveLength(
expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(
initialLength - 1
)
})
it('should correctly determine changed fields', () => {
initNormally()
expect(component['getChangedFields']()).toEqual({
id: doc.id,
})
component.documentForm.get('title').setValue('Foo Bar')
component.documentForm.get('permissions_form').setValue({
owner: 1,
set_permissions: {
view: {
users: [2],
groups: [],
},
change: {
users: [3],
groups: [],
},
},
})
component.documentForm.get('title').markAsDirty()
component.documentForm.get('permissions_form').markAsDirty()
expect(component['getChangedFields']()).toEqual({
id: doc.id,
title: 'Foo Bar',
owner: 1,
set_permissions: {
view: {
users: [2],
groups: [],
},
change: {
users: [3],
groups: [],
},
},
})
})
it('should show custom field errors', () => {
initNormally()
component.error = {
@@ -1369,34 +1331,6 @@ describe('DocumentDetailComponent', () => {
expect(urlRevokeSpy).toHaveBeenCalled()
})
it('should download a file with the correct filename', () => {
const mockBlob = new Blob(['test content'], { type: 'text/plain' })
const mockResponse = new HttpResponse({
body: mockBlob,
headers: new HttpHeaders({
'Content-Disposition': 'attachment; filename="test-file.txt"',
}),
})
const downloadUrl = 'http://example.com/download'
component.documentId = 123
jest.spyOn(documentService, 'getDownloadUrl').mockReturnValue(downloadUrl)
const createSpy = jest.spyOn(document, 'createElement')
const anchor: HTMLAnchorElement = {} as HTMLAnchorElement
createSpy.mockReturnValueOnce(anchor)
component.download(false)
httpTestingController
.expectOne(downloadUrl)
.flush(mockBlob, { headers: mockResponse.headers })
expect(createSpy).toHaveBeenCalledWith('a')
expect(anchor.download).toBe('test-file.txt')
createSpy.mockClear()
})
it('should get email enabled status from settings', () => {
jest.spyOn(settingsService, 'get').mockReturnValue(true)
expect(component.emailEnabled).toBeTruthy()

View File

@@ -1,5 +1,5 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
import { HttpClient, HttpResponse } from '@angular/common/http'
import { HttpClient } from '@angular/common/http'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import {
FormArray,
@@ -77,7 +77,6 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { getFilenameFromContentDisposition } from 'src/app/utils/http'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
@@ -208,7 +207,7 @@ export class DocumentDetailComponent
documentForm: FormGroup = new FormGroup({
title: new FormControl(''),
content: new FormControl(''),
created: new FormControl(),
created_date: new FormControl(),
correspondent: new FormControl(),
document_type: new FormControl(),
storage_path: new FormControl(),
@@ -490,7 +489,7 @@ export class DocumentDetailComponent
this.store = new BehaviorSubject({
title: doc.title,
content: doc.content,
created: doc.created,
created_date: doc.created_date,
correspondent: doc.correspondent,
document_type: doc.document_type,
storage_path: doc.storage_path,
@@ -784,7 +783,6 @@ export class DocumentDetailComponent
this.title = doc.title
this.updateFormForCustomFields()
this.documentForm.patchValue(doc)
this.documentForm.markAsPristine()
this.openDocumentService.setDirty(doc, false)
},
error: () => {
@@ -795,30 +793,11 @@ export class DocumentDetailComponent
})
}
private getChangedFields(): any {
const changes = {
id: this.document.id,
}
Object.keys(this.documentForm.controls).forEach((key) => {
if (this.documentForm.get(key).dirty) {
if (key === 'permissions_form') {
changes['owner'] =
this.documentForm.get('permissions_form').value['owner']
changes['set_permissions'] =
this.documentForm.get('permissions_form').value['set_permissions']
} else {
changes[key] = this.documentForm.get(key).value
}
}
})
return changes
}
save(close: boolean = false) {
this.networkActive = true
;(document.activeElement as HTMLElement)?.dispatchEvent(new Event('change'))
this.documentsService
.patch(this.getChangedFields())
.update(this.document)
.pipe(first())
.subscribe({
next: (docValues) => {
@@ -845,18 +824,11 @@ export class DocumentDetailComponent
},
error: (error) => {
this.networkActive = false
const canEdit =
this.permissionsService.currentUserHasObjectPermissions(
PermissionAction.Change,
this.document
)
if (!canEdit) {
// document was 'given away'
this.openDocumentService.setDirty(this.document, false)
if (!this.userCanEdit) {
this.toastService.showInfo(
$localize`Document "${this.document.title}" saved successfully.`
)
this.close()
close && this.close()
} else {
this.error = error.error
this.toastService.showError(
@@ -872,7 +844,7 @@ export class DocumentDetailComponent
this.networkActive = true
this.store.next(this.documentForm.value)
this.documentsService
.patch(this.getChangedFields())
.update(this.document)
.pipe(
switchMap((updateResult) => {
return this.documentListViewService
@@ -1016,46 +988,44 @@ export class DocumentDetailComponent
this.documentId,
original
)
this.http
.get(downloadUrl, { observe: 'response', responseType: 'blob' })
.subscribe({
next: (response: HttpResponse<Blob>) => {
const contentDisposition = response.headers.get('Content-Disposition')
const filename =
getFilenameFromContentDisposition(contentDisposition) ||
this.document.title
const blob = new Blob([response.body], {
type: response.body.type,
})
this.downloading = false
const file = new File([blob], filename, {
type: response.body.type,
})
if (
!this.deviceDetectorService.isDesktop() &&
navigator.canShare &&
navigator.canShare({ files: [file] })
) {
navigator.share({
files: [file],
})
} else {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
this.http.get(downloadUrl, { responseType: 'blob' }).subscribe({
next: (blob) => {
this.downloading = false
const blobParts = [blob]
const file = new File(
blobParts,
original
? this.document.original_file_name
: this.document.archived_file_name,
{
type: original ? this.document.mime_type : 'application/pdf',
}
},
error: (error) => {
this.downloading = false
this.toastService.showError(
$localize`Error downloading document`,
error
)
},
})
)
if (
!this.deviceDetectorService.isDesktop() &&
navigator.canShare &&
navigator.canShare({ files: [file] })
) {
navigator.share({
files: [file],
})
} else {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = this.document.title
a.click()
URL.revokeObjectURL(url)
}
},
error: (error) => {
this.downloading = false
this.toastService.showError(
$localize`Error downloading document`,
error
)
},
})
}
hasNext() {
@@ -1119,10 +1089,12 @@ export class DocumentDetailComponent
)
}
get currentZoom() {
isZoomSelected(setting: ZoomSetting): boolean {
if (this.previewZoomScale === ZoomSetting.PageFit) {
return ZoomSetting.PageFit
} else return this.previewZoomSetting
return setting === ZoomSetting.PageFit
}
return this.previewZoomSetting === setting
}
getZoomSettingTitle(setting: ZoomSetting): string {
@@ -1307,7 +1279,9 @@ export class DocumentDetailComponent
this.document.custom_fields?.forEach((fieldInstance) => {
this.customFieldFormFields.push(
new FormGroup({
field: new FormControl(fieldInstance.field),
field: new FormControl(
this.getCustomFieldFromInstance(fieldInstance)?.id
),
value: new FormControl(fieldInstance.value),
}),
{ emitEvent }
@@ -1323,8 +1297,6 @@ export class DocumentDetailComponent
created: new Date(),
})
this.updateFormForCustomFields(true)
this.documentForm.get('custom_fields').markAsDirty()
this.documentForm.updateValueAndValidity()
}
public removeField(fieldInstance: CustomFieldInstance) {
@@ -1333,7 +1305,6 @@ export class DocumentDetailComponent
1
)
this.updateFormForCustomFields(true)
this.documentForm.get('custom_fields').markAsDirty()
this.documentForm.updateValueAndValidity()
}

View File

@@ -15,7 +15,7 @@
}
</div>
<div class="col col-md-10">
<div class="col">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title w-100">
@@ -112,14 +112,14 @@
@if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) {
<ng-template #dateTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Created: {{ document.created_date | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
</div>
</ng-template>
@if (displayFields.includes(DisplayField.CREATED)) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created | customDate:'mediumDate'}}</small>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
}
@if (displayFields.includes(DisplayField.ADDED)) {

View File

@@ -73,14 +73,14 @@
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Created: {{ document.created_date | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
</div>
</ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
<small>{{document.created | customDate:'mediumDate'}}</small>
<small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
</div>
}

View File

@@ -348,7 +348,7 @@
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<td>
{{d.created | customDate}}
{{d.created_date | customDate}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {

View File

@@ -2,6 +2,13 @@
<ng-content select="[content]"></ng-content>
</div>
<div class="global-dropzone-overlay position-fixed top-0 start-0 bottom-0 end-0 text-center pe-none" [class.active]="fileIsOver && !hidden">
<div class="global-dropzone-overlay position-fixed top-0 start-0 bottom-0 end-0 text-center pe-none fade" [class.show]="fileIsOver" [class.hide]="hidden">
<h2 class="pe-none position-absolute top-50 start-50 translate-middle" i18n>Drop files to begin upload</h2>
</div>
<ngx-file-drop
dropZoneClassName="visually-hidden"
contentClassName="visually-hidden"
(onFileDrop)="dropped($event)"
#ngxFileDrop>
</ngx-file-drop>

View File

@@ -1,14 +1,8 @@
.global-dropzone-overlay {
opacity: 0;
transition: opacity 0.25s ease-in-out;
background-color: hsla(var(--pngx-primary), var(--pngx-primary-lightness), .8);
z-index: 1200;
h2 {
color: var(--pngx-primary-text-contrast)
}
&.active {
opacity: 1;
}
}

View File

@@ -9,6 +9,7 @@ import {
tick,
} from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
@@ -26,7 +27,7 @@ describe('FileDropComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FileDropComponent, ToastsComponent],
imports: [NgxFileDropModule, FileDropComponent, ToastsComponent],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@@ -65,12 +66,12 @@ describe('FileDropComponent', () => {
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
expect(dropzone.classes['active']).toBeFalsy()
expect(dropzone.classes['hide']).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
// drop
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
files: {
@@ -92,209 +93,53 @@ describe('FileDropComponent', () => {
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
expect(dropzone.classes['hide']).toBeTruthy()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
const uploadSpy = jest.spyOn(
UploadDocumentsService.prototype as any,
'uploadFile'
)
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
items: [
{
kind: 'file',
type: 'application/pdf',
getAsFile: () => file,
files: {
item: () => {
return new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
},
],
length: 1,
} as unknown as FileList,
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
it('should support drag drop, initiate upload with webkitGetAsEntry', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
items: [
{
kind: 'file',
type: 'application/pdf',
webkitGetAsEntry: () => ({
isFile: true,
isDirectory: false,
file: (cb: (file: File) => void) => cb(file),
}),
},
],
files: [],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
it('should show an error on traverseFileTree error', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'showError')
const traverseSpy = jest
.spyOn(component as any, 'traverseFileTree')
.mockReturnValue(Promise.reject(new Error('Error traversing file tree')))
fixture.detectChanges()
// Simulate a drop with a directory entry
const mockEntry = {
isDirectory: true,
isFile: false,
createReader: () => ({ readEntries: jest.fn() }),
} as unknown as FileSystemDirectoryEntry
const event = {
preventDefault: () => {},
stopImmediatePropagation: () => {},
dataTransfer: {
items: [
{
kind: 'file',
webkitGetAsEntry: () => mockEntry,
component.dropped([
{
fileEntry: {
isFile: true,
file: (callback) => {
callback(
new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
)
},
],
},
} as unknown as DragEvent
component.onDrop(event)
tick() // flush microtasks (e.g., Promise.reject)
expect(traverseSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
$localize`Failed to read dropped items: Error traversing file tree`
)
discardPeriodicTasks()
}))
it('should support drag drop, initiate upload without DataTransfer API support', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
items: [],
files: [file],
}
component.onDrop(dragEvent as DragEvent)
},
} as unknown as NgxFileDropEntry,
])
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
it('should resolve a single file when entry isFile', () => {
const mockFile = new File(['data'], 'test.txt', { type: 'text/plain' })
const mockEntry = {
isFile: true,
isDirectory: false,
file: (cb: (f: File) => void) => cb(mockFile),
} as unknown as FileSystemFileEntry
return (component as any)
.traverseFileTree(mockEntry)
.then((result: File[]) => {
expect(result).toEqual([mockFile])
})
})
it('should resolve all files in a flat directory', async () => {
const file1 = new File(['data'], 'file1.txt')
const file2 = new File(['data'], 'file2.txt')
const mockFileEntry1 = {
isFile: true,
isDirectory: false,
file: (cb: (f: File) => void) => cb(file1),
} as unknown as FileSystemFileEntry
const mockFileEntry2 = {
isFile: true,
isDirectory: false,
file: (cb: (f: File) => void) => cb(file2),
} as unknown as FileSystemFileEntry
let callCount = 0
const mockDirEntry = {
isFile: false,
isDirectory: true,
createReader: () => ({
readEntries: (cb: (batch: FileSystemEntry[]) => void) => {
if (callCount++ === 0) {
cb([mockFileEntry1, mockFileEntry2])
} else {
cb([]) // second call: signal EOF
}
},
}),
} as unknown as FileSystemDirectoryEntry
const result = await (component as any).traverseFileTree(mockDirEntry)
expect(result).toEqual([file1, file2])
})
it('should resolve a non-file non-directory entry as an empty array', () => {
const mockEntry = {
isFile: false,
isDirectory: false,
file: (cb: (f: File) => void) => cb(new File([], '')),
} as unknown as FileSystemEntry
return (component as any)
.traverseFileTree(mockEntry)
.then((result: File[]) => {
expect(result).toEqual([])
})
})
it('should ignore events if disabled', fakeAsync(() => {
settingsService.globalDropzoneEnabled = false
expect(settingsService.globalDropzoneActive).toBeFalsy()

View File

@@ -1,4 +1,9 @@
import { Component, HostListener } from '@angular/core'
import { Component, HostListener, ViewChild } from '@angular/core'
import {
NgxFileDropComponent,
NgxFileDropEntry,
NgxFileDropModule,
} from 'ngx-file-drop'
import {
PermissionAction,
PermissionsService,
@@ -12,7 +17,7 @@ import { UploadDocumentsService } from 'src/app/services/upload-documents.servic
selector: 'pngx-file-drop',
templateUrl: './file-drop.component.html',
styleUrls: ['./file-drop.component.scss'],
imports: [],
imports: [NgxFileDropModule],
})
export class FileDropComponent {
private fileLeaveTimeoutID: any
@@ -36,6 +41,8 @@ export class FileDropComponent {
)
}
@ViewChild('ngxFileDrop') ngxFileDrop: NgxFileDropComponent
@HostListener('document:dragover', ['$event']) onDragOver(event: DragEvent) {
if (!this.dragDropEnabled || !event.dataTransfer?.types?.includes('Files'))
return
@@ -71,87 +78,21 @@ export class FileDropComponent {
}, ms)
}
private traverseFileTree(entry: FileSystemEntry): Promise<File[]> {
if (entry.isFile) {
return new Promise((resolve, reject) => {
;(entry as FileSystemFileEntry).file(resolve, reject)
}).then((file: File) => [file])
}
if (entry.isDirectory) {
return new Promise<File[]>((resolve, reject) => {
const dirReader = (entry as FileSystemDirectoryEntry).createReader()
const allEntries: FileSystemEntry[] = []
const readEntries = () => {
dirReader.readEntries((batch) => {
if (batch.length === 0) {
const promises = allEntries.map((child) =>
this.traverseFileTree(child)
)
Promise.all(promises)
.then((results) => resolve([].concat(...results)))
.catch(reject)
} else {
allEntries.push(...batch)
readEntries() // keep reading
}
}, reject)
}
readEntries()
})
}
return Promise.resolve([])
}
@HostListener('document:drop', ['$event']) public onDrop(event: DragEvent) {
if (!this.dragDropEnabled) return
event.preventDefault()
event.stopImmediatePropagation()
const files: File[] = []
const entries: FileSystemEntry[] = []
if (event.dataTransfer?.items && event.dataTransfer.items.length) {
for (const item of Array.from(event.dataTransfer.items)) {
if (item.webkitGetAsEntry) {
// webkitGetAsEntry not standard, but is widely supported
const entry = item.webkitGetAsEntry()
if (entry) entries.push(entry)
} else if (item.kind === 'file') {
const file = item.getAsFile()
if (file) files.push(file)
}
}
} else if (event.dataTransfer?.files) {
// Fallback for browsers without DataTransferItem API
for (const file of Array.from(event.dataTransfer.files)) {
files.push(file)
}
}
if (entries.length) {
const promises = entries.map((entry) => this.traverseFileTree(entry))
Promise.all(promises)
.then((results) => {
files.push(...[].concat(...results))
this.toastService.showInfo($localize`Initiating upload...`, 3000)
files.forEach((file) => this.uploadDocumentsService.uploadFile(file))
})
.catch((e) => {
this.toastService.showError(
$localize`Failed to read dropped items: ${e.message}`
)
})
} else if (files.length) {
this.toastService.showInfo($localize`Initiating upload...`, 3000)
files.forEach((file) => this.uploadDocumentsService.uploadFile(file))
}
// pass event onto ngx-file-drop to handle files
this.ngxFileDrop.dropFiles(event)
this.onDragLeave(event, true)
}
public dropped(files: NgxFileDropEntry[]) {
this.uploadDocumentsService.onNgxFileDrop(files)
if (files.length > 0)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
}
@HostListener('window:blur', ['$event']) public onWindowBlur() {
if (this.fileIsOver) this.onDragLeave(null)
}

View File

@@ -28,7 +28,7 @@ import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { CustomFieldsComponent } from './custom-fields.component'
import { CustomFieldsListComponent } from './custom-fields-list.component'
const fields: CustomField[] = [
{
@@ -43,9 +43,9 @@ const fields: CustomField[] = [
},
]
describe('CustomFieldsComponent', () => {
let component: CustomFieldsComponent
let fixture: ComponentFixture<CustomFieldsComponent>
describe('CustomFieldsListComponent', () => {
let component: CustomFieldsListComponent
let fixture: ComponentFixture<CustomFieldsListComponent>
let customFieldsService: CustomFieldsService
let modalService: NgbModal
let toastService: ToastService
@@ -61,7 +61,7 @@ describe('CustomFieldsComponent', () => {
NgbModalModule,
NgbPopoverModule,
NgxBootstrapIconsModule.pick(allIcons),
CustomFieldsComponent,
CustomFieldsListComponent,
IfPermissionsDirective,
PageHeaderComponent,
ConfirmDialogComponent,
@@ -94,7 +94,7 @@ describe('CustomFieldsComponent', () => {
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 0, username: 'test' }
fixture = TestBed.createComponent(CustomFieldsComponent)
fixture = TestBed.createComponent(CustomFieldsListComponent)
component = fixture.componentInstance
fixture.detectChanges()
jest.useFakeTimers()
@@ -106,7 +106,7 @@ describe('CustomFieldsComponent', () => {
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const reloadSpy = jest.spyOn(component, 'reloadData')
const createButton = fixture.debugElement.queryAll(By.css('button'))[1]
createButton.triggerEventHandler('click')
@@ -131,7 +131,7 @@ describe('CustomFieldsComponent', () => {
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const reloadSpy = jest.spyOn(component, 'reloadData')
const editButton = fixture.debugElement.queryAll(By.css('button'))[2]
editButton.triggerEventHandler('click')
@@ -156,7 +156,7 @@ describe('CustomFieldsComponent', () => {
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const deleteSpy = jest.spyOn(customFieldsService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
const reloadSpy = jest.spyOn(component, 'reloadData')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[5]
deleteButton.triggerEventHandler('click')

View File

@@ -0,0 +1,96 @@
import { NgClass, TitleCasePipe } from '@angular/common'
import { Component } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import {
NgbDropdownModule,
NgbModal,
NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
import {
CustomFieldQueryLogicalOperator,
CustomFieldQueryOperator,
} from 'src/app/data/custom-field-query'
import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import {
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { ManagementListComponent } from '../management-list/management-list.component'
@Component({
selector: 'pngx-custom-fields-list',
templateUrl: './../management-list/management-list.component.html',
styleUrls: ['./../management-list/management-list.component.scss'],
imports: [
SortableDirective,
PageHeaderComponent,
TitleCasePipe,
IfPermissionsDirective,
SafeHtmlPipe,
FormsModule,
ReactiveFormsModule,
NgClass,
NgbDropdownModule,
NgbPaginationModule,
NgxBootstrapIconsModule,
],
})
export class CustomFieldsListComponent extends ManagementListComponent<CustomField> {
permissionsDisabled = true
constructor(
customFieldsService: CustomFieldsService,
modalService: NgbModal,
toastService: ToastService,
documentListViewService: DocumentListViewService,
permissionsService: PermissionsService
) {
super(
customFieldsService,
modalService,
CustomFieldEditDialogComponent,
toastService,
documentListViewService,
permissionsService,
0, // see filterDocuments override below
$localize`custom field`,
$localize`custom fields`,
PermissionType.CustomField,
[
{
key: 'data_type',
name: $localize`Data Type`,
valueFn: (field: CustomField) => {
return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name
},
},
]
)
}
filterDocuments(field: CustomField) {
this.documentListViewService.quickFilter([
{
rule_type: FILTER_CUSTOM_FIELDS_QUERY,
value: JSON.stringify([
CustomFieldQueryLogicalOperator.Or,
[[field.id, CustomFieldQueryOperator.Exists, true]],
]),
},
])
}
getDeleteMessage(object: CustomField) {
return $localize`Do you really want to delete the field "${object.name}"?`
}
}

View File

@@ -1,72 +0,0 @@
<pngx-page-header
title="Custom Fields"
i18n-title
info="Customize the data fields that can be attached to documents."
i18n-info
infoLink="usage/#custom-fields"
>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Field</ng-container>
</button>
</pngx-page-header>
<ul class="list-group">
<li class="list-group-item">
<div class="row">
<div class="col" i18n>Name</div>
<div class="col" i18n>Data Type</div>
<div class="col" i18n>Actions</div>
</div>
</li>
@if (loading) {
<li class="list-group-item">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</li>
}
@for (field of fields; track field) {
<li class="list-group-item">
<div class="row fade" [class.show]="show">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
<div class="col">
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="editField(field)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" ngbDropdownItem i18n>Edit</button>
<button class="text-danger" (click)="deleteField(field)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" ngbDropdownItem i18n>Delete</button>
@if (field.document_count > 0) {
<button (click)="filterDocuments(field)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ field.document_count }})</button>
}
</div>
</div>
</div>
<div class="btn-group d-none d-sm-inline-block">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
@if (field.document_count > 0) {
<div class="btn-group d-none d-sm-inline-block ms-2">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="filterDocuments(field)">
<i-bs width="1em" height="1em" name="filter"></i-bs>&nbsp;<ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ field.document_count }}</span>
</button>
</div>
}
</div>
</div>
</li>
}
@if (!loading && fields.length === 0) {
<li class="list-group-item" i18n>No fields defined.</li>
}
</ul>

View File

@@ -1,4 +0,0 @@
// hide caret on mobile dropdown
.d-block.d-sm-none .dropdown-toggle::after {
display: none;
}

View File

@@ -1,148 +0,0 @@
import { Component, OnInit } from '@angular/core'
import {
NgbDropdownModule,
NgbModal,
NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs'
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
import {
CustomFieldQueryLogicalOperator,
CustomFieldQueryOperator,
} from 'src/app/data/custom-field-query'
import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-custom-fields',
templateUrl: './custom-fields.component.html',
styleUrls: ['./custom-fields.component.scss'],
imports: [
PageHeaderComponent,
IfPermissionsDirective,
NgbDropdownModule,
NgbPaginationModule,
NgxBootstrapIconsModule,
],
})
export class CustomFieldsComponent
extends LoadingComponentWithPermissions
implements OnInit
{
public fields: CustomField[] = []
constructor(
private customFieldsService: CustomFieldsService,
public permissionsService: PermissionsService,
private modalService: NgbModal,
private toastService: ToastService,
private documentListViewService: DocumentListViewService,
private settingsService: SettingsService,
private documentService: DocumentService,
private savedViewService: SavedViewService
) {
super()
}
ngOnInit() {
this.reload()
}
reload() {
this.customFieldsService
.listAll()
.pipe(
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.fields = r.results
}),
delay(100)
)
.subscribe(() => {
this.show = true
this.loading = false
})
}
editField(field: CustomField) {
const modal = this.modalService.open(CustomFieldEditDialogComponent)
modal.componentInstance.dialogMode = field
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.object = field
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newField) => {
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
this.customFieldsService.clearCache()
this.settingsService.initializeDisplayFields()
this.documentService.reload()
this.reload()
})
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving field.`, e)
})
}
deleteField(field: CustomField) {
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Confirm delete field`
modal.componentInstance.messageBold = $localize`This operation will permanently delete this field.`
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.customFieldsService.delete(field).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted field "${field.name}"`)
this.customFieldsService.clearCache()
this.settingsService.initializeDisplayFields()
this.documentService.reload()
this.savedViewService.reload()
this.reload()
},
error: (e) => {
this.toastService.showError(
$localize`Error deleting field "${field.name}".`,
e
)
},
})
})
}
getDataType(field: CustomField): string {
return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name
}
filterDocuments(field: CustomField) {
this.documentListViewService.quickFilter([
{
rule_type: FILTER_CUSTOM_FIELDS_QUERY,
value: JSON.stringify([
CustomFieldQueryLogicalOperator.Or,
[[field.id, CustomFieldQueryOperator.Exists, true]],
]),
},
])
}
}

View File

@@ -188,7 +188,7 @@ describe('MailComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailAccounts[0] as any)
editDialog.succeeded.emit(mailAccounts[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved account "${mailAccounts[0].name}".`
)
@@ -246,7 +246,7 @@ describe('MailComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailRules[0] as any)
editDialog.succeeded.emit(mailRules[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved rule "${mailRules[0].name}".`
)

View File

@@ -2,7 +2,7 @@
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
<button *ngIf="!permissionsDisabled" type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
<i-bs name="person-fill-lock"></i-bs>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">

View File

@@ -64,7 +64,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
private modalService: NgbModal,
private editDialogComponent: any,
private toastService: ToastService,
private documentListViewService: DocumentListViewService,
protected documentListViewService: DocumentListViewService,
private permissionsService: PermissionsService,
protected filterRuleType: number,
public typeName: string,
@@ -93,6 +93,8 @@ export abstract class ManagementListComponent<T extends MatchingModel>
public selectedObjects: Set<number> = new Set()
public togggleAll: boolean = false
protected permissionsDisabled: boolean = false
ngOnInit(): void {
this.reloadData()

View File

@@ -1,4 +1,4 @@
import { ObjectWithId } from './object-with-id'
import { MatchingModel } from './matching-model'
export enum CustomFieldDataType {
String = 'string',
@@ -51,13 +51,11 @@ export const DATA_TYPE_LABELS = [
},
]
export interface CustomField extends ObjectWithId {
export interface CustomField extends MatchingModel {
data_type: CustomFieldDataType
name: string
created?: Date
extra_data?: {
select_options?: Array<{ label: string; id: string }>
default_currency?: string
}
document_count?: number
}

View File

@@ -130,6 +130,9 @@ export interface Document extends ObjectWithPermissions {
// UTC
created?: Date
// localized date
created_date?: Date
modified?: Date
added?: Date

View File

@@ -49,7 +49,6 @@ export enum ConfigOptionType {
export const ConfigCategory = {
General: $localize`General Settings`,
OCR: $localize`OCR Settings`,
Barcode: $localize`Barcode Settings`,
}
export interface ConfigOption {
@@ -181,83 +180,6 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_APP_TITLE',
category: ConfigCategory.General,
},
{
key: 'barcodes_enabled',
title: $localize`Enable Barcodes`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_ENABLE_BARCODES',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_enable_tiff_support',
title: $localize`Enable TIFF Support`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_string',
title: $localize`Barcode String`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_CONSUMER_BARCODE_STRING',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_retain_split_pages',
title: $localize`Retain Split Pages`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_enable_asn',
title: $localize`Enable ASN`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_asn_prefix',
title: $localize`ASN Prefix`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_upscale',
title: $localize`Upscale`,
type: ConfigOptionType.Number,
config_key: 'PAPERLESS_CONSUMER_BARCODE_UPSCALE',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_dpi',
title: $localize`DPI`,
type: ConfigOptionType.Number,
config_key: 'PAPERLESS_CONSUMER_BARCODE_DPI',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_max_pages',
title: $localize`Max Pages`,
type: ConfigOptionType.Number,
config_key: 'PAPERLESS_CONSUMER_BARCODE_MAX_PAGES',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_enable_tag',
title: $localize`Enable Tag Detection`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_tag_mapping',
title: $localize`Tag Mapping`,
type: ConfigOptionType.JSON,
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING',
category: ConfigCategory.Barcode,
},
]
export interface PaperlessConfig extends ObjectWithId {
@@ -276,15 +198,4 @@ export interface PaperlessConfig extends ObjectWithId {
user_args: object
app_logo: string
app_title: string
barcodes_enabled: boolean
barcode_enable_tiff_support: boolean
barcode_string: string
barcode_retain_split_pages: boolean
barcode_enable_asn: boolean
barcode_asn_prefix: string
barcode_upscale: number
barcode_dpi: number
barcode_max_pages: number
barcode_enable_tag: boolean
barcode_tag_mapping: object
}

View File

@@ -7,6 +7,4 @@ export interface WebsocketProgressMessage {
message?: string
document_id: number
owner_id?: number
users_can_view?: number[]
groups_can_view?: number[]
}

View File

@@ -50,7 +50,7 @@ describe('ObjectNamePipe', () => {
})
})
it('should return Private string if object not found', (done) => {
it('should return empty string if object not found', (done) => {
const mockObjects = {
results: [{ id: 2, name: 'Object 2' }],
count: 1,
@@ -60,7 +60,7 @@ describe('ObjectNamePipe', () => {
jest.spyOn(objectService, 'listAll').mockReturnValue(of(mockObjects))
pipe.transform(1).subscribe((result) => {
expect(result).toBe('Private')
expect(result).toBe('')
done()
})
})

View File

@@ -35,10 +35,7 @@ export abstract class ObjectNamePipe implements PipeTransform {
return this.objectService.listAll().pipe(
map((objects) => {
this.objects = objects.results
return (
this.objects.find((o) => o.id === obejctId)?.name ||
$localize`Private`
)
return this.objects.find((o) => o.id === obejctId)?.name || ''
}),
catchError(() => of(''))
)

View File

@@ -1,12 +1,12 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { CustomField } from 'src/app/data/custom-field'
import { AbstractPaperlessService } from './abstract-paperless-service'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class CustomFieldsService extends AbstractPaperlessService<CustomField> {
export class CustomFieldsService extends AbstractNameFilterService<CustomField> {
constructor(http: HttpClient) {
super(http, 'custom_fields')
}

View File

@@ -268,15 +268,15 @@ describe(`DocumentService`, () => {
expect(req.request.method).toEqual('GET')
})
it('should pass remove_inbox_tags setting to patch', () => {
subscription = service.patch(documents[0]).subscribe()
it('should pass remove_inbox_tags setting to update', () => {
subscription = service.update(documents[0]).subscribe()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
)
expect(req.request.body.remove_inbox_tags).toEqual(false)
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS, true)
subscription = service.patch(documents[0]).subscribe()
subscription = service.update(documents[0]).subscribe()
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
)

View File

@@ -189,11 +189,13 @@ export class DocumentService extends AbstractPaperlessService<Document> {
return this.http.get<number>(this.getResourceUrl(null, 'next_asn'))
}
patch(o: Document): Observable<Document> {
update(o: Document): Observable<Document> {
// we want to only set created_date
o.created = undefined
o.remove_inbox_tags = !!this.settingsService.get(
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
)
return super.patch(o)
return super.update(o)
}
uploadDocument(formData) {

Some files were not shown because too many files have changed in this diff Show More