Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c2a0b9e6 | ||
|
|
5468bf28c4 | ||
|
|
7a07f1e81d | ||
|
|
92524ae97a | ||
|
|
1c89f6da24 | ||
|
|
d1a3e3b859 | ||
|
|
79ae594d54 | ||
|
|
97fe5c4176 | ||
|
|
e60bd3a132 | ||
|
|
5d6cfa7349 | ||
|
|
3105317137 | ||
|
|
1456169d7f | ||
|
|
22a6fe5e10 | ||
|
|
a0c1a19263 | ||
|
|
1f5d1b6f26 |
@@ -21,19 +21,17 @@
|
||||
# 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:
|
||||
@@ -60,25 +58,20 @@ 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:
|
||||
|
||||
@@ -5,7 +5,6 @@ version: 2
|
||||
# Required for uv support for now
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
|
||||
# Enable version updates for pnpm
|
||||
- package-ecosystem: "npm"
|
||||
target-branch: "dev"
|
||||
@@ -35,7 +34,6 @@ updates:
|
||||
patterns:
|
||||
- "@typescript-eslint*"
|
||||
- "eslint"
|
||||
|
||||
# Enable version updates for Python
|
||||
- package-ecosystem: "uv"
|
||||
target-branch: "dev"
|
||||
@@ -70,7 +68,6 @@ updates:
|
||||
patterns:
|
||||
- psycopg*
|
||||
- zxing-cpp
|
||||
|
||||
# Enable updates for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
target-branch: "dev"
|
||||
@@ -90,7 +87,6 @@ updates:
|
||||
- "major"
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Update Dockerfile in root directory
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
@@ -105,7 +101,6 @@ updates:
|
||||
commit-message:
|
||||
prefix: "docker"
|
||||
include: "scope"
|
||||
|
||||
# Update Docker Compose files in docker/compose directory
|
||||
- package-ecosystem: "docker-compose"
|
||||
directory: "/docker/compose/"
|
||||
|
||||
+73
-159
@@ -1,5 +1,4 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -12,72 +11,57 @@ 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
|
||||
- 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"
|
||||
@@ -88,14 +72,12 @@ 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
|
||||
@@ -106,49 +88,40 @@ 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
|
||||
- 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
|
||||
@@ -161,28 +134,24 @@ 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
|
||||
@@ -194,8 +163,7 @@ 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
|
||||
@@ -209,15 +177,12 @@ 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
|
||||
- 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 Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -235,8 +200,7 @@ 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
|
||||
@@ -252,31 +216,25 @@ 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: Run Playwright e2e tests
|
||||
- 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
|
||||
- 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/
|
||||
|
||||
frontend-bundle-analysis:
|
||||
name: "Frontend Bundle Analysis"
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -284,20 +242,17 @@ jobs:
|
||||
- tests-frontend
|
||||
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:
|
||||
@@ -305,15 +260,12 @@ 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
|
||||
@@ -325,8 +277,7 @@ jobs:
|
||||
- tests-backend
|
||||
- tests-frontend
|
||||
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
|
||||
@@ -342,15 +293,13 @@ 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:
|
||||
@@ -365,37 +314,31 @@ 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'
|
||||
@@ -403,8 +346,7 @@ 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: .
|
||||
@@ -422,23 +364,19 @@ 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:
|
||||
@@ -446,63 +384,52 @@ 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
|
||||
- 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 \
|
||||
@@ -539,21 +466,18 @@ 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
|
||||
@@ -565,14 +489,12 @@ 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
|
||||
@@ -581,8 +503,7 @@ 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:
|
||||
@@ -593,8 +514,7 @@ 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:
|
||||
@@ -603,7 +523,6 @@ 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
|
||||
@@ -611,26 +530,22 @@ 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
|
||||
- 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: |
|
||||
@@ -652,8 +567,7 @@ 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: |
|
||||
|
||||
@@ -6,17 +6,14 @@
|
||||
# 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 }}
|
||||
@@ -30,8 +27,7 @@ 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:
|
||||
@@ -43,7 +39,6 @@ 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'
|
||||
@@ -58,8 +53,7 @@ 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:
|
||||
|
||||
@@ -10,16 +10,14 @@
|
||||
# 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
|
||||
@@ -28,27 +26,23 @@ 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
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
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 }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: Project Automations
|
||||
|
||||
on:
|
||||
pull_request_target: #_target allows access to secrets
|
||||
types:
|
||||
@@ -8,10 +7,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pr_opened_or_reopened:
|
||||
name: pr_opened_or_reopened
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
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'
|
||||
@@ -27,9 +23,8 @@ 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'
|
||||
@@ -42,20 +37,14 @@ 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'
|
||||
|
||||
@@ -76,3 +76,8 @@ 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"
|
||||
|
||||
@@ -36,7 +36,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/mariadb:11
|
||||
restart: unless-stopped
|
||||
@@ -48,7 +47,6 @@ services:
|
||||
MARIADB_USER: paperless
|
||||
MARIADB_PASSWORD: paperless
|
||||
MARIADB_ROOT_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -75,7 +73,6 @@ 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.17
|
||||
restart: unless-stopped
|
||||
@@ -85,11 +82,9 @@ services:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: docker.io/apache/tika:latest
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
@@ -32,7 +32,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/mariadb:11
|
||||
restart: unless-stopped
|
||||
@@ -44,7 +43,6 @@ services:
|
||||
MARIADB_USER: paperless
|
||||
MARIADB_PASSWORD: paperless
|
||||
MARIADB_ROOT_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -66,7 +64,6 @@ 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:
|
||||
|
||||
@@ -36,7 +36,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
@@ -46,7 +45,6 @@ services:
|
||||
POSTGRES_DB: paperless
|
||||
POSTGRES_USER: paperless
|
||||
POSTGRES_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -65,7 +63,6 @@ services:
|
||||
PAPERLESS_DBHOST: db
|
||||
env_file:
|
||||
- stack.env
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
@@ -36,7 +36,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
@@ -46,7 +45,6 @@ services:
|
||||
POSTGRES_DB: paperless
|
||||
POSTGRES_USER: paperless
|
||||
POSTGRES_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -69,22 +67,18 @@ 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.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:
|
||||
|
||||
@@ -32,7 +32,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
@@ -42,7 +41,6 @@ services:
|
||||
POSTGRES_DB: paperless
|
||||
POSTGRES_USER: paperless
|
||||
POSTGRES_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -60,7 +58,6 @@ services:
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
@@ -36,7 +36,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -57,22 +56,18 @@ 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.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:
|
||||
|
||||
@@ -29,7 +29,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
@@ -45,7 +44,6 @@ services:
|
||||
env_file: docker-compose.env
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
+13
-15
@@ -11,14 +11,12 @@ 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
|
||||
@@ -60,17 +58,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 © 2016 - 2023 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
|
||||
extra:
|
||||
social:
|
||||
@@ -83,5 +81,5 @@ extra:
|
||||
plugins:
|
||||
- search
|
||||
- glightbox:
|
||||
skip_classes:
|
||||
- no-lightbox
|
||||
skip_classes:
|
||||
- no-lightbox
|
||||
|
||||
+151
-145
File diff suppressed because it is too large
Load Diff
@@ -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">
|
||||
<div class="d-flex flex-column align-items-start custom-title">
|
||||
<span class="title">{{customAppTitle}}</span>
|
||||
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
||||
</div>
|
||||
|
||||
@@ -244,7 +244,7 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (min-width: 366px) and (max-width: 768px) {
|
||||
.navbar-toggler {
|
||||
// compensate for 2 buttons on the right
|
||||
margin-right: 45px;
|
||||
@@ -257,6 +257,13 @@ 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;
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
this.emailAddress = ''
|
||||
this.emailSubject = ''
|
||||
this.emailMessage = ''
|
||||
this.close()
|
||||
this.toastService.showInfo($localize`Email sent`)
|
||||
},
|
||||
error: (e) => {
|
||||
|
||||
+75
-39
@@ -7,6 +7,7 @@ import {
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
@@ -44,6 +45,11 @@ const nullItem = {
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
const negativeNullItem = {
|
||||
id: NEGATIVE_NULL_FILTER_VALUE,
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
let selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||
@@ -64,6 +70,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
hotkeyService = TestBed.inject(HotKeyService)
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.selectionModel = new FilterableDropdownSelectionModel()
|
||||
selectionModel = new FilterableDropdownSelectionModel()
|
||||
})
|
||||
|
||||
@@ -74,7 +81,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(1)
|
||||
@@ -96,7 +103,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items selected', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -110,11 +117,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
|
||||
expect(component.items).toEqual([nullItem, ...items])
|
||||
expect(component.selectionModel.items).toEqual([nullItem, ...items])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -124,7 +131,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -139,8 +146,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should exclude items when excluded and not editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
component.excludeClicked(items[0].id)
|
||||
@@ -149,8 +156,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should toggle when items excluded and editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
@@ -160,8 +167,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should hide count for item if adding will increase size of set', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.hideCount(items[0])).toBeFalsy()
|
||||
selectionModel.logicalOperator = LogicalOperator.Or
|
||||
@@ -170,7 +177,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should enforce single select when editing', () => {
|
||||
component.editing = true
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -182,11 +189,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support manyToOne selecting', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
selectionModel.manyToOne = false
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
expect(component.manyToOne).toBeTruthy()
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.selectionModel.manyToOne).toBeTruthy()
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
@@ -197,12 +204,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should dynamically enable / disable modifier toggle', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
selectionModel.toggle(null)
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
@@ -210,7 +215,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
@@ -232,7 +237,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
@@ -250,7 +255,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -277,7 +282,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
@@ -297,7 +302,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
@@ -319,7 +324,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -364,7 +369,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -400,7 +405,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -425,9 +430,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
@@ -454,7 +459,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
@@ -483,22 +488,53 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
expect(changedResult.getExcludedItems()).toEqual(items)
|
||||
}))
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.items = items.concat([{ id: null, name: 'Null B' }])
|
||||
it('should update null item selection on toggleIntersection', () => {
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.set(null, ToggleableItemState.Selected)
|
||||
component.selectionModel.intersection = Intersection.Exclude
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getExcludedItems()).toEqual([
|
||||
negativeNullItem,
|
||||
])
|
||||
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
|
||||
})
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
|
||||
selectionModel.toggle(items[1].id)
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items.length).toEqual(4)
|
||||
expect(selectionModel.items).toEqual([
|
||||
nullItem,
|
||||
{ id: null, name: 'Null B' },
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
selectionModel.intersection = Intersection.Exclude
|
||||
selectionModel.toggleIntersection()
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items).toEqual([
|
||||
negativeNullItem,
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
// coverage
|
||||
selectionModel.items = selectionModel.items.reverse()
|
||||
selectionModel.apply()
|
||||
})
|
||||
|
||||
it('selection model should sort items by state and document counts = 0, if set', () => {
|
||||
const tagA = { id: 4, name: 'Tag A' }
|
||||
component.items = items.concat([tagA])
|
||||
component.selectionModel.items = items.concat([tagA])
|
||||
component.selectionModel = selectionModel
|
||||
component.documentCounts = [
|
||||
{ id: 1, document_count: 0 }, // Tag1
|
||||
@@ -529,7 +565,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
@@ -549,7 +585,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.createRef = jest.fn()
|
||||
@@ -569,7 +605,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
const id = 1
|
||||
const state = ToggleableItemState.Selected
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel.singleSelect = true
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel['temporarySelectionStates'].set(id, state)
|
||||
@@ -596,7 +632,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support shortcut keys', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.shortcutKey = 't'
|
||||
fixture.detectChanges()
|
||||
@@ -606,7 +642,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support an extra button and not apply changes when clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.extraButtonTitle = 'Extra'
|
||||
component.selectionModel = selectionModel
|
||||
|
||||
+68
-27
@@ -12,6 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, filter, takeUntil } from 'rxjs'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
@@ -61,15 +62,56 @@ export class FilterableDropdownSelectionModel {
|
||||
}
|
||||
|
||||
set items(items: MatchingModel[]) {
|
||||
this._items = items
|
||||
this.sortItems()
|
||||
if (items) {
|
||||
this._items = Array.from(items)
|
||||
this.sortItems()
|
||||
this.setNullItem()
|
||||
}
|
||||
}
|
||||
|
||||
private setNullItem() {
|
||||
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
||||
if (this._items[0]?.id === null) {
|
||||
this._items.shift()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const item = {
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id:
|
||||
this.manyToOne || this.intersection === Intersection.Include
|
||||
? null
|
||||
: NEGATIVE_NULL_FILTER_VALUE,
|
||||
}
|
||||
|
||||
if (
|
||||
this._items[0]?.id === null ||
|
||||
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
||||
) {
|
||||
this._items[0] = item
|
||||
} else if (this._items) {
|
||||
this._items.unshift(item)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(manyToOne: boolean = false) {
|
||||
this.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
private sortItems() {
|
||||
this._items.sort((a, b) => {
|
||||
if (a.id == null && b.id != null) {
|
||||
if (
|
||||
(a.id == null && b.id != null) ||
|
||||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id != NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return -1
|
||||
} else if (a.id != null && b.id == null) {
|
||||
} else if (
|
||||
(a.id != null && b.id == null) ||
|
||||
(a.id != NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id == NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return 1
|
||||
} else if (
|
||||
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||
@@ -230,6 +272,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set logicalOperator(operator: LogicalOperator) {
|
||||
this.temporaryLogicalOperator = operator
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleOperator() {
|
||||
@@ -242,6 +285,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set intersection(intersection: Intersection) {
|
||||
this.temporaryIntersection = intersection
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleIntersection() {
|
||||
@@ -250,9 +294,20 @@ export class FilterableDropdownSelectionModel {
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
|
||||
this.temporarySelectionStates.forEach((state, key) => {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
if (key === null && this.intersection === Intersection.Exclude) {
|
||||
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
||||
} else if (
|
||||
key === NEGATIVE_NULL_FILTER_VALUE &&
|
||||
this.intersection === Intersection.Include
|
||||
) {
|
||||
this.temporarySelectionStates.set(null, newState)
|
||||
} else {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
}
|
||||
})
|
||||
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
@@ -274,6 +329,7 @@ export class FilterableDropdownSelectionModel {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||
this.setNullItem()
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
@@ -305,8 +361,10 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
isNoneSelected() {
|
||||
return (
|
||||
this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected
|
||||
(this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected) ||
|
||||
(this.intersection == Intersection.Exclude &&
|
||||
this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -384,25 +442,13 @@ export class FilterableDropdownComponent
|
||||
|
||||
filterText: string
|
||||
|
||||
@Input()
|
||||
set items(items: MatchingModel[]) {
|
||||
if (items) {
|
||||
this._selectionModel.items = Array.from(items)
|
||||
this._selectionModel.items.unshift({
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
_selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
get items(): MatchingModel[] {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel: FilterableDropdownSelectionModel =
|
||||
new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
@@ -423,11 +469,6 @@ export class FilterableDropdownComponent
|
||||
@Output()
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set manyToOne(manyToOne: boolean) {
|
||||
this.selectionModel.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
get manyToOne() {
|
||||
return this.selectionModel.manyToOne
|
||||
}
|
||||
@@ -484,7 +525,7 @@ export class FilterableDropdownComponent
|
||||
return this.manyToOne
|
||||
? this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
: !this.selectionModel.isNoneSelected()
|
||||
: true
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
|
||||
@@ -824,11 +824,18 @@ export class DocumentDetailComponent
|
||||
},
|
||||
error: (error) => {
|
||||
this.networkActive = false
|
||||
if (!this.userCanEdit) {
|
||||
const canEdit =
|
||||
this.permissionsService.currentUserHasObjectPermissions(
|
||||
PermissionAction.Change,
|
||||
this.document
|
||||
)
|
||||
if (!canEdit) {
|
||||
// document was 'given away'
|
||||
this.openDocumentService.setDirty(this.document, false)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${this.document.title}" saved successfully.`
|
||||
)
|
||||
close && this.close()
|
||||
this.close()
|
||||
} else {
|
||||
this.error = error.error
|
||||
this.toastService.showError(
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createTag.bind(this)"
|
||||
(opened)="openTagsDropdown()"
|
||||
@@ -36,7 +34,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@@ -51,7 +48,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@@ -66,7 +62,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@@ -81,10 +76,8 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
|
||||
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
|
||||
[items]="customFields"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createCustomField.bind(this)"
|
||||
(opened)="openCustomFieldsDropdown()"
|
||||
|
||||
@@ -1150,10 +1150,10 @@ describe('BulkEditorComponent', () => {
|
||||
|
||||
it('should not attempt to retrieve objects if user does not have permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.tags).toBeUndefined()
|
||||
expect(component.correspondents).toBeUndefined()
|
||||
expect(component.documentTypes).toBeUndefined()
|
||||
expect(component.storagePaths).toBeUndefined()
|
||||
expect(component.tagSelectionModel.items.length).toEqual(0)
|
||||
expect(component.correspondentSelectionModel.items.length).toEqual(0)
|
||||
expect(component.documentTypeSelectionModel.items.length).toEqual(0)
|
||||
expect(component.storagePathsSelectionModel.items.length).toEqual(0)
|
||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/correspondents/`
|
||||
@@ -1204,7 +1204,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(tagListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
|
||||
expect(component.tags).toEqual(tags.results)
|
||||
expect(component.tagSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(tags.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new correspondent', () => {
|
||||
@@ -1251,7 +1253,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCorrespondent.id
|
||||
)
|
||||
expect(component.correspondents).toEqual(correspondents.results)
|
||||
expect(component.correspondentSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(correspondents.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new document type', () => {
|
||||
@@ -1295,7 +1299,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newDocumentType.id
|
||||
)
|
||||
expect(component.documentTypes).toEqual(documentTypes.results)
|
||||
expect(component.documentTypeSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(documentTypes.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new storage path', () => {
|
||||
@@ -1339,7 +1345,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newStoragePath.id
|
||||
)
|
||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||
expect(component.storagePathsSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(storagePaths.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new custom field', () => {
|
||||
@@ -1391,7 +1399,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCustomField.id
|
||||
)
|
||||
expect(component.customFields).toEqual(customFields.results)
|
||||
expect(component.customFieldsSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(customFields.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the bulk edit custom field values dialog with correct parameters', () => {
|
||||
@@ -1416,17 +1426,17 @@ describe('BulkEditorComponent', () => {
|
||||
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||
|
||||
component.customFields = [
|
||||
component.customFieldsSelectionModel.items = [
|
||||
{ id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String },
|
||||
{ id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String },
|
||||
]
|
||||
] as any
|
||||
|
||||
component.setCustomFieldValues({
|
||||
itemsToAdd: [{ id: 1 }, { id: 2 }],
|
||||
itemsToRemove: [1],
|
||||
} as any)
|
||||
|
||||
expect(modal.componentInstance.customFields).toEqual(component.customFields)
|
||||
expect(modal.componentInstance.customFields.length).toEqual(2)
|
||||
expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2])
|
||||
expect(modal.componentInstance.documents).toEqual([3, 4])
|
||||
|
||||
|
||||
@@ -14,12 +14,8 @@ import { saveAs } from 'file-saver'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
@@ -75,17 +71,11 @@ export class BulkEditorComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tags: Tag[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
customFields: CustomField[]
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
correspondentDocumentCounts: SelectionDataItem[]
|
||||
documentTypeDocumentCounts: SelectionDataItem[]
|
||||
@@ -176,7 +166,7 @@ export class BulkEditorComponent
|
||||
this.tagService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.tags = result.results))
|
||||
.subscribe((result) => (this.tagSelectionModel.items = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@@ -187,7 +177,9 @@ export class BulkEditorComponent
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.correspondentSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@@ -198,7 +190,9 @@ export class BulkEditorComponent
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.documentTypeSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@@ -209,7 +203,9 @@ export class BulkEditorComponent
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.storagePathsSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@@ -220,7 +216,9 @@ export class BulkEditorComponent
|
||||
this.customFieldService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.customFieldsSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
|
||||
this.downloadForm
|
||||
@@ -651,7 +649,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newTag, tags }) => {
|
||||
this.tags = tags.results
|
||||
this.tagSelectionModel.items = tags.results
|
||||
this.tagSelectionModel.toggle(newTag.id)
|
||||
})
|
||||
}
|
||||
@@ -674,7 +672,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCorrespondent, correspondents }) => {
|
||||
this.correspondents = correspondents.results
|
||||
this.correspondentSelectionModel.items = correspondents.results
|
||||
this.correspondentSelectionModel.toggle(newCorrespondent.id)
|
||||
})
|
||||
}
|
||||
@@ -695,7 +693,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newDocumentType, documentTypes }) => {
|
||||
this.documentTypes = documentTypes.results
|
||||
this.documentTypeSelectionModel.items = documentTypes.results
|
||||
this.documentTypeSelectionModel.toggle(newDocumentType.id)
|
||||
})
|
||||
}
|
||||
@@ -716,7 +714,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newStoragePath, storagePaths }) => {
|
||||
this.storagePaths = storagePaths.results
|
||||
this.storagePathsSelectionModel.items = storagePaths.results
|
||||
this.storagePathsSelectionModel.toggle(newStoragePath.id)
|
||||
})
|
||||
}
|
||||
@@ -737,7 +735,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCustomField, customFields }) => {
|
||||
this.customFields = customFields.results
|
||||
this.customFieldsSelectionModel.items = customFields.results
|
||||
this.customFieldsSelectionModel.toggle(newCustomField.id)
|
||||
})
|
||||
}
|
||||
@@ -875,7 +873,9 @@ export class BulkEditorComponent
|
||||
})
|
||||
const dialog =
|
||||
modal.componentInstance as CustomFieldsBulkEditDialogComponent
|
||||
dialog.customFields = this.customFields
|
||||
dialog.customFields = (
|
||||
this.customFieldsSelectionModel.items as CustomField[]
|
||||
).filter((f) => f.id !== null)
|
||||
dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map(
|
||||
(item) => item.id
|
||||
)
|
||||
|
||||
@@ -35,11 +35,9 @@
|
||||
<div class="col-auto">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[manyToOne]="true"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onTagsDropdownOpen()"
|
||||
@@ -48,10 +46,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="t"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onCorrespondentDropdownOpen()"
|
||||
@@ -60,10 +57,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="y"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onDocumentTypeDropdownOpen()"
|
||||
@@ -72,10 +68,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="u"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[(selectionModel)]="storagePathSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onStoragePathDropdownOpen()"
|
||||
|
||||
+79
-12
@@ -69,6 +69,7 @@ import {
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_TITLE,
|
||||
FILTER_TITLE_CONTENT,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
@@ -671,9 +672,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '12',
|
||||
},
|
||||
]
|
||||
expect(component.correspondentSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.correspondentSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@@ -681,6 +679,19 @@ describe('FilterEditorComponent', () => {
|
||||
correspondents[0],
|
||||
])
|
||||
component.toggleCorrespondent(12) // coverage
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
]
|
||||
expect(component.correspondentSelectionModel.intersection).toEqual(
|
||||
Intersection.Exclude
|
||||
)
|
||||
expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
|
||||
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
|
||||
])
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
|
||||
@@ -754,9 +765,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '22',
|
||||
},
|
||||
]
|
||||
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.documentTypeSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@@ -764,6 +772,19 @@ describe('FilterEditorComponent', () => {
|
||||
document_types[0],
|
||||
])
|
||||
component.toggleDocumentType(22) // coverage
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
]
|
||||
expect(component.documentTypeSelectionModel.intersection).toEqual(
|
||||
Intersection.Exclude
|
||||
)
|
||||
expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
|
||||
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
|
||||
])
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any of document types', fakeAsync(() => {
|
||||
@@ -780,9 +801,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '23',
|
||||
},
|
||||
]
|
||||
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.documentTypeSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@@ -837,9 +855,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '32',
|
||||
},
|
||||
]
|
||||
expect(component.storagePathSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.storagePathSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@@ -847,6 +862,19 @@ describe('FilterEditorComponent', () => {
|
||||
storage_paths[0],
|
||||
])
|
||||
component.toggleStoragePath(32) // coverage
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
]
|
||||
expect(component.storagePathSelectionModel.intersection).toEqual(
|
||||
Intersection.Exclude
|
||||
)
|
||||
expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
|
||||
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
|
||||
])
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
|
||||
@@ -1398,6 +1426,19 @@ describe('FilterEditorComponent', () => {
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
|
||||
const excludeButton = correspondentsFilterableDropdown.queryAll(
|
||||
By.css('input[value=exclude]')
|
||||
)[0]
|
||||
excludeButton.nativeElement.checked = true
|
||||
excludeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
|
||||
@@ -1455,6 +1496,19 @@ describe('FilterEditorComponent', () => {
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
|
||||
const excludeButton = docTypesFilterableDropdown.queryAll(
|
||||
By.css('input[value=exclude]')
|
||||
)[0]
|
||||
excludeButton.nativeElement.checked = true
|
||||
excludeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
|
||||
@@ -1512,6 +1566,19 @@ describe('FilterEditorComponent', () => {
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
|
||||
const excludeButton = storagePathsFilterableDropdown.queryAll(
|
||||
By.css('input[value=exclude]')
|
||||
)[0]
|
||||
excludeButton.nativeElement.checked = true
|
||||
excludeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
|
||||
|
||||
@@ -26,14 +26,12 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from 'rxjs/operators'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import {
|
||||
CustomFieldQueryLogicalOperator,
|
||||
CustomFieldQueryOperator,
|
||||
} from 'src/app/data/custom-field-query'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import {
|
||||
FILTER_ADDED_AFTER,
|
||||
@@ -75,9 +73,8 @@ import {
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_TITLE,
|
||||
FILTER_TITLE_CONTENT,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
@@ -251,7 +248,9 @@ export class FilterEditorComponent
|
||||
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Correspondent: ${
|
||||
this.correspondents.find((c) => c.id == +rule.value)?.name
|
||||
this.correspondentSelectionModel.items.find(
|
||||
(c) => c.id == +rule.value
|
||||
)?.name
|
||||
}`
|
||||
} else {
|
||||
return $localize`Without correspondent`
|
||||
@@ -261,7 +260,9 @@ export class FilterEditorComponent
|
||||
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Document type: ${
|
||||
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
|
||||
this.documentTypeSelectionModel.items.find(
|
||||
(dt) => dt.id == +rule.value
|
||||
)?.name
|
||||
}`
|
||||
} else {
|
||||
return $localize`Without document type`
|
||||
@@ -271,7 +272,9 @@ export class FilterEditorComponent
|
||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Storage path: ${
|
||||
this.storagePaths.find((sp) => sp.id == +rule.value)?.name
|
||||
this.storagePathSelectionModel.items.find(
|
||||
(sp) => sp.id == +rule.value
|
||||
)?.name
|
||||
}`
|
||||
} else {
|
||||
return $localize`Without storage path`
|
||||
@@ -279,7 +282,7 @@ export class FilterEditorComponent
|
||||
|
||||
case FILTER_HAS_TAGS_ALL:
|
||||
return $localize`Tag: ${
|
||||
this.tags.find((t) => t.id == +rule.value)?.name
|
||||
this.tagSelectionModel.items.find((t) => t.id == +rule.value)?.name
|
||||
}`
|
||||
|
||||
case FILTER_HAS_ANY_TAG:
|
||||
@@ -326,10 +329,6 @@ export class FilterEditorComponent
|
||||
@ViewChild('textFilterInput')
|
||||
textFilterInput: ElementRef
|
||||
|
||||
tags: Tag[] = []
|
||||
correspondents: Correspondent[] = []
|
||||
documentTypes: DocumentType[] = []
|
||||
storagePaths: StoragePath[] = []
|
||||
customFields: CustomField[] = []
|
||||
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
@@ -370,7 +369,7 @@ export class FilterEditorComponent
|
||||
)
|
||||
}
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||
@@ -551,6 +550,19 @@ export class FilterEditorComponent
|
||||
)
|
||||
break
|
||||
case FILTER_CORRESPONDENT:
|
||||
this.correspondentSelectionModel.intersection =
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
? Intersection.Exclude
|
||||
: Intersection.Include
|
||||
this.correspondentSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
this.correspondentSelectionModel.intersection ==
|
||||
Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.correspondentSelectionModel.intersection = Intersection.Include
|
||||
@@ -569,6 +581,18 @@ export class FilterEditorComponent
|
||||
)
|
||||
break
|
||||
case FILTER_DOCUMENT_TYPE:
|
||||
this.documentTypeSelectionModel.intersection =
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
? Intersection.Exclude
|
||||
: Intersection.Include
|
||||
this.documentTypeSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
this.documentTypeSelectionModel.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.documentTypeSelectionModel.intersection = Intersection.Include
|
||||
@@ -587,6 +611,18 @@ export class FilterEditorComponent
|
||||
)
|
||||
break
|
||||
case FILTER_STORAGE_PATH:
|
||||
this.storagePathSelectionModel.intersection =
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
? Intersection.Exclude
|
||||
: Intersection.Include
|
||||
this.storagePathSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
this.storagePathSelectionModel.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.storagePathSelectionModel.intersection = Intersection.Include
|
||||
@@ -809,9 +845,21 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.correspondentSelectionModel.isNoneSelected()) {
|
||||
if (
|
||||
this.correspondentSelectionModel.isNoneSelected() &&
|
||||
this.correspondentSelectionModel.intersection == Intersection.Include
|
||||
) {
|
||||
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
|
||||
} else {
|
||||
if (
|
||||
this.correspondentSelectionModel.isNoneSelected() &&
|
||||
this.correspondentSelectionModel.intersection == Intersection.Exclude
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
})
|
||||
}
|
||||
this.correspondentSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((correspondent) => {
|
||||
@@ -822,6 +870,7 @@ export class FilterEditorComponent
|
||||
})
|
||||
this.correspondentSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((correspondent) => correspondent.id > 0)
|
||||
.forEach((correspondent) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||
@@ -829,9 +878,21 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.documentTypeSelectionModel.isNoneSelected()) {
|
||||
if (
|
||||
this.documentTypeSelectionModel.isNoneSelected() &&
|
||||
this.documentTypeSelectionModel.intersection === Intersection.Include
|
||||
) {
|
||||
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
|
||||
} else {
|
||||
if (
|
||||
this.documentTypeSelectionModel.isNoneSelected() &&
|
||||
this.documentTypeSelectionModel.intersection == Intersection.Exclude
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
})
|
||||
}
|
||||
this.documentTypeSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((documentType) => {
|
||||
@@ -842,6 +903,7 @@ export class FilterEditorComponent
|
||||
})
|
||||
this.documentTypeSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((documentType) => documentType.id > 0)
|
||||
.forEach((documentType) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||
@@ -849,9 +911,21 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.storagePathSelectionModel.isNoneSelected()) {
|
||||
if (
|
||||
this.storagePathSelectionModel.isNoneSelected() &&
|
||||
this.storagePathSelectionModel.intersection == Intersection.Include
|
||||
) {
|
||||
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
||||
} else {
|
||||
if (
|
||||
this.storagePathSelectionModel.isNoneSelected() &&
|
||||
this.storagePathSelectionModel.intersection == Intersection.Exclude
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
})
|
||||
}
|
||||
this.storagePathSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((storagePath) => {
|
||||
@@ -862,6 +936,7 @@ export class FilterEditorComponent
|
||||
})
|
||||
this.storagePathSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((storagePath) => storagePath.id > 0)
|
||||
.forEach((storagePath) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||
@@ -1062,7 +1137,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.tagService.listAll().subscribe((result) => {
|
||||
this.tags = result.results
|
||||
this.tagSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
@@ -1074,7 +1149,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.correspondentService.listAll().subscribe((result) => {
|
||||
this.correspondents = result.results
|
||||
this.correspondentSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
@@ -1086,7 +1161,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.documentTypeService.listAll().subscribe((result) => {
|
||||
this.documentTypes = result.results
|
||||
this.documentTypeSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
@@ -1098,7 +1173,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.storagePathService.listAll().subscribe((result) => {
|
||||
this.storagePaths = result.results
|
||||
this.storagePathSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DataType } from './datatype'
|
||||
|
||||
export const NEGATIVE_NULL_FILTER_VALUE = -1
|
||||
|
||||
// These correspond to src/documents/models.py and changes here require a DB migration (and vice versa)
|
||||
export const FILTER_TITLE = 0
|
||||
export const FILTER_CONTENT = 1
|
||||
|
||||
@@ -602,7 +602,6 @@ export class SettingsService {
|
||||
)
|
||||
} catch (error) {
|
||||
this.toastService.showError(errorMessage)
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
this.storeSettings()
|
||||
@@ -614,7 +613,6 @@ export class SettingsService {
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(errorMessage)
|
||||
console.log(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -636,7 +634,6 @@ export class SettingsService {
|
||||
this.toastService.showError(
|
||||
'Error migrating update checking setting'
|
||||
)
|
||||
console.log(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from '../data/filter-rule-type'
|
||||
import {
|
||||
filterRulesFromQueryParams,
|
||||
@@ -97,6 +98,16 @@ describe('QueryParams Utils', () => {
|
||||
correspondent__isnull: 1,
|
||||
})
|
||||
|
||||
params = queryParamsFromFilterRules([
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
expect(params).toEqual({
|
||||
correspondent__isnull: 0,
|
||||
})
|
||||
|
||||
params = queryParamsFromFilterRules([
|
||||
{
|
||||
rule_type: FILTER_HAS_ANY_TAG,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
FILTER_RULE_TYPES,
|
||||
FilterRuleType,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from '../data/filter-rule-type'
|
||||
import { ListViewState } from '../services/document-list-view.service'
|
||||
|
||||
@@ -113,6 +114,10 @@ export function filterRulesFromQueryParams(
|
||||
rt.isnull_filtervar == filterQueryParamName
|
||||
)
|
||||
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
|
||||
const nullRuleValue =
|
||||
queryParams.get(filterQueryParamName) == '1'
|
||||
? null
|
||||
: NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
const valueURIComponent: string = queryParams.get(filterQueryParamName)
|
||||
const filterQueryParamValues: string[] = rule_type.multi
|
||||
? valueURIComponent.split(',')
|
||||
@@ -125,7 +130,7 @@ export function filterRulesFromQueryParams(
|
||||
val = val.replace('1', 'true').replace('0', 'false')
|
||||
return {
|
||||
rule_type: rule_type.id,
|
||||
value: isNullRuleType ? null : val,
|
||||
value: isNullRuleType ? nullRuleValue : val,
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -143,6 +148,11 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
|
||||
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
||||
if (ruleType.isnull_filtervar && rule.value == null) {
|
||||
params[ruleType.isnull_filtervar] = 1
|
||||
} else if (
|
||||
ruleType.isnull_filtervar &&
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
) {
|
||||
params[ruleType.isnull_filtervar] = 0
|
||||
} else if (ruleType.multi) {
|
||||
params[ruleType.filtervar] = params[ruleType.filtervar]
|
||||
? params[ruleType.filtervar] + ',' + rule.value
|
||||
|
||||
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1445
-1074
File diff suppressed because it is too large
Load Diff
+1450
-1079
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1446
-1075
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1714
-1343
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1455
-1084
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1451
-1080
File diff suppressed because it is too large
Load Diff
+1466
-1095
File diff suppressed because it is too large
Load Diff
+1443
-1073
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1482
-1112
File diff suppressed because it is too large
Load Diff
+1448
-1077
File diff suppressed because it is too large
Load Diff
+1445
-1074
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1465
-1094
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1447
-1076
File diff suppressed because it is too large
Load Diff
+1469
-1098
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1556
-1183
File diff suppressed because it is too large
Load Diff
+1443
-1072
File diff suppressed because it is too large
Load Diff
+1497
-1126
File diff suppressed because it is too large
Load Diff
+1446
-1075
File diff suppressed because it is too large
Load Diff
+2335
-1964
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
--pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
|
||||
--pngx-bg-disabled: #f7f7f7;
|
||||
--pngx-focus-alpha: 0.3;
|
||||
--pngx-toast-max-width: 360px;
|
||||
--pngx-toast-max-width: 340px;
|
||||
--bs-info: var(--pngx-bg-alt2);
|
||||
--bs-info-rgb: 233, 236, 239;
|
||||
@media screen and (min-width: 1024px) {
|
||||
|
||||
@@ -870,7 +870,7 @@ class BasicUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class NotesSerializer(serializers.ModelSerializer):
|
||||
user = BasicUserSerializer()
|
||||
user = BasicUserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
@@ -893,7 +893,7 @@ class DocumentSerializer(
|
||||
created_date = serializers.DateField(required=False)
|
||||
page_count = SerializerMethodField()
|
||||
|
||||
notes = NotesSerializer(many=True, required=False)
|
||||
notes = NotesSerializer(many=True, required=False, read_only=True)
|
||||
|
||||
custom_fields = CustomFieldInstanceSerializer(
|
||||
many=True,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user