mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-12 17:20:48 +00:00
Compare commits
94
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17627ec603 | ||
|
|
a1d76f3a19 | ||
|
|
f5ca9bbcb6 | ||
|
|
60a35edee1 | ||
|
|
7cdcd71a6d | ||
|
|
759f9cd22b | ||
|
|
d05e96cda2 | ||
|
|
fefce7be1b | ||
|
|
cc65226d6d | ||
|
|
00096e0252 | ||
|
|
2829e78009 | ||
|
|
d7d4014bbd | ||
|
|
a139bdff46 | ||
|
|
936b1b3bfa | ||
|
|
e0c33b3d39 | ||
|
|
32bc5522e9 | ||
|
|
d5698329f5 | ||
|
|
2e0d92d8f5 | ||
|
|
0736c07d8e | ||
|
|
53d9aa34a1 | ||
|
|
19fdc3b815 | ||
|
|
5357be2d43 | ||
|
|
2642cefd35 | ||
|
|
8aa99a381a | ||
|
|
d14cfc93f4 | ||
|
|
53e386ec81 | ||
|
|
6fd91beae2 | ||
|
|
a4543c03dc | ||
|
|
45a2ac4879 | ||
|
|
8bc9db816a | ||
|
|
c5239f9f74 | ||
|
|
e88303cc16 | ||
|
|
cb3513c6f3 | ||
|
|
4468a510a6 | ||
|
|
09d70ed7da | ||
|
|
057aee8bbd | ||
|
|
fa83a4c5dc | ||
|
|
22746aa202 | ||
|
|
137342c1f9 | ||
|
|
0ae5e2d3d4 | ||
|
|
b7b0de100f | ||
|
|
8531d3e0c9 | ||
|
|
05f7545a4a | ||
|
|
fc29f3511f | ||
|
|
d939d873e1 | ||
|
|
f0cc7fa953 | ||
|
|
43c73796ac | ||
|
|
ec47c0f11d | ||
|
|
cc15aa0180 | ||
|
|
e6ea046e31 | ||
|
|
04bae0af62 | ||
|
|
d2f4cfa783 | ||
|
|
7cdf8ec3c2 | ||
|
|
786eac0b22 | ||
|
|
16b79fc023 | ||
|
|
cd58cffd89 | ||
|
|
2c82b06b93 | ||
|
|
5486fa7c40 | ||
|
|
fefe4e5802 | ||
|
|
936f9363a0 | ||
|
|
1b8b861a60 | ||
|
|
c2b25bdd93 | ||
|
|
dddb1f9014 | ||
|
|
6ac609c64e | ||
|
|
4d2c733f21 | ||
|
|
90bddfcb88 | ||
|
|
0de2cb8372 | ||
|
|
37168b927e | ||
|
|
ef3c9f69fe | ||
|
|
3586c5747b | ||
|
|
f8dc15c43d | ||
|
|
bfb8b9076b | ||
|
|
cef246a761 | ||
|
|
5b63c9a0ff | ||
|
|
217c5e681d | ||
|
|
45e99cde5a | ||
|
|
7ea58eaf46 | ||
|
|
a19804a0b7 | ||
|
|
3289be7383 | ||
|
|
d98918dd97 | ||
|
|
34c88613f6 | ||
|
|
13acdabb47 | ||
|
|
b67cfdf3fd | ||
|
|
98d2ddd1c6 | ||
|
|
91b35eada6 | ||
|
|
11248f776d | ||
|
|
331fa13088 | ||
|
|
ebddd5687c | ||
|
|
2925b8de81 | ||
|
|
5e397e1222 | ||
|
|
6f99ad9469 | ||
|
|
a5d3247ca7 | ||
|
|
8a67481a9d | ||
|
|
652f964f5d |
No files matched your search
@@ -1,5 +1,23 @@
|
|||||||
title: "[Feature Request] "
|
title: "[Feature Request] "
|
||||||
body:
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
#### Use of AI tools
|
||||||
|
|
||||||
|
If this request was generated, in whole or in part, by an AI tool or agent you **must** say so using the field below. Failing to clearly disclose this is a violation of our [Code of Conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
The project prefers thoughtful, well-described ideas to AI-generated content. Please describe the problem, your use case, the desired behavior, and who would benefit.
|
||||||
|
- type: dropdown
|
||||||
|
id: ai-usage
|
||||||
|
attributes:
|
||||||
|
label: AI usage
|
||||||
|
description: Was any part of this request written by an AI tool or agent?
|
||||||
|
options:
|
||||||
|
- No AI tools were used
|
||||||
|
- Yes, AI tools helped write this request
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
title: "[Support] "
|
title: "[Support] "
|
||||||
body:
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
#### Use of AI tools
|
||||||
|
|
||||||
|
If this post was generated, in whole or in part, by an AI tool or agent you **must** say so using the field below. Failing to clearly disclose this is a violation of our [Code of Conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md).
|
||||||
|
- type: dropdown
|
||||||
|
id: ai-usage
|
||||||
|
attributes:
|
||||||
|
label: AI usage
|
||||||
|
description: Was any part of this post written by an AI tool or agent?
|
||||||
|
options:
|
||||||
|
- No AI tools were used
|
||||||
|
- Yes, AI tools helped write this post
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
name: Install apt packages
|
name: Install apt packages
|
||||||
description: >
|
description: >
|
||||||
Installs apt packages with a per-attempt timeout and retries. The hosted runner's Azure apt mirror intermittently serves archives at a crawl, which apt's own timeout does not catch. A stuck attempt is killed and retried, which usually lands on a healthy mirror node.
|
Installs apt packages with bounded download retries and Ubuntu mirror fallback. Caches downloaded packages as an optimization; apt resolves dependencies on every run.
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
packages:
|
packages:
|
||||||
description: Space separated list of packages to install
|
description: Space separated list of package names to install
|
||||||
required: true
|
required: true
|
||||||
attempts:
|
attempts:
|
||||||
description: Maximum number of attempts
|
description: Maximum number of download attempts
|
||||||
default: "3"
|
default: "3"
|
||||||
attempt-timeout:
|
attempt-timeout:
|
||||||
description: Seconds before a single attempt is killed
|
description: Seconds before a single download attempt is killed
|
||||||
default: "180"
|
default: "180"
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
- name: Identify apt cache
|
||||||
|
id: cache-key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PACKAGES: ${{ inputs.packages }}
|
||||||
|
run: |
|
||||||
|
. /etc/os-release
|
||||||
|
packages_hash=$(printf '%s' "$PACKAGES" | sha256sum | cut -d ' ' -f 1)
|
||||||
|
# Cached downloads are optional: apt fetches any missing/newer packages.
|
||||||
|
# Refresh weekly, including inside job containers without ImageVersion.
|
||||||
|
echo "key=apt-v2-${ID}-${VERSION_ID}-$(dpkg --print-architecture)-$(date -u +%G-%V)-${packages_hash}" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Restore apt packages
|
||||||
|
id: cache
|
||||||
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ runner.temp }}/apt-packages/*.deb
|
||||||
|
${{ runner.temp }}/apt-packages/.keep
|
||||||
|
key: ${{ steps.cache-key.outputs.key }}
|
||||||
- name: Install ${{ inputs.packages }}
|
- name: Install ${{ inputs.packages }}
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
PACKAGES: ${{ inputs.packages }}
|
PACKAGES: ${{ inputs.packages }}
|
||||||
ATTEMPTS: ${{ inputs.attempts }}
|
ATTEMPTS: ${{ inputs.attempts }}
|
||||||
ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }}
|
ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }}
|
||||||
run: |
|
CACHE_DIR: ${{ runner.temp }}/apt-packages
|
||||||
SUDO=""
|
run: bash "$GITHUB_ACTION_PATH/install.sh"
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
# Save immediately: a later test failure must not discard downloaded packages.
|
||||||
SUDO="sudo"
|
- name: Save apt packages
|
||||||
fi
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
attempt_install() {
|
with:
|
||||||
$SUDO apt-get update -q
|
path: |
|
||||||
# Intentionally unquoted, this is a list of package names
|
${{ runner.temp }}/apt-packages/*.deb
|
||||||
$SUDO apt-get install -q --yes --no-install-recommends $PACKAGES
|
${{ runner.temp }}/apt-packages/.keep
|
||||||
}
|
key: ${{ steps.cache-key.outputs.key }}
|
||||||
export -f attempt_install
|
|
||||||
export SUDO PACKAGES
|
|
||||||
|
|
||||||
for attempt in $(seq 1 "${ATTEMPTS}"); do
|
|
||||||
echo "::group::apt install attempt ${attempt}/${ATTEMPTS}"
|
|
||||||
start=$(date +%s)
|
|
||||||
if timeout --kill-after=10 "${ATTEMPT_TIMEOUT}" bash -c attempt_install; then
|
|
||||||
echo "::endgroup::"
|
|
||||||
echo "Installed in $(( $(date +%s) - start ))s"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "::endgroup::"
|
|
||||||
echo "::warning::apt install attempt ${attempt}/${ATTEMPTS} failed or exceeded ${ATTEMPT_TIMEOUT}s"
|
|
||||||
# A killed attempt can leave dpkg half configured
|
|
||||||
$SUDO dpkg --configure -a || true
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "::error::apt install failed after ${ATTEMPTS} attempts"
|
|
||||||
exit 1
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Run as root in the timed child process. Only downloads may be interrupted.
|
||||||
|
if [ "${1:-}" = download ]; then
|
||||||
|
download_archive_dir=$2
|
||||||
|
shift 2
|
||||||
|
apt-get update -q
|
||||||
|
# Keep apt itself under timeout supervision, including the kill grace period.
|
||||||
|
exec apt-get install -q --yes --no-install-recommends --download-only \
|
||||||
|
-o "Dir::Cache::archives=$download_archive_dir" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUDO=()
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
SUDO=(sudo)
|
||||||
|
fi
|
||||||
|
read -r -a requested_packages <<< "$PACKAGES"
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
touch "$CACHE_DIR/.keep"
|
||||||
|
|
||||||
|
use_ubuntu_mirrors() {
|
||||||
|
# Hosted runners use a mirror list; job containers can point directly at Azure.
|
||||||
|
# Handle both layouts, leaving other repositories and signature checks intact.
|
||||||
|
if [ -f /etc/apt/apt-mirrors.txt ]; then
|
||||||
|
"${SUDO[@]}" sed -i '\|azure\.archive\.ubuntu\.com|d' /etc/apt/apt-mirrors.txt
|
||||||
|
fi
|
||||||
|
for source in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
|
||||||
|
if [ -f "$source" ]; then
|
||||||
|
"${SUDO[@]}" sed -i 's|azure\.archive\.ubuntu\.com|archive.ubuntu.com|g' "$source"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||||
|
echo "::group::apt download attempt ${attempt}/${ATTEMPTS}"
|
||||||
|
# Run timeout as root so it can signal the root-owned apt processes.
|
||||||
|
if "${SUDO[@]}" timeout --kill-after=10 "$ATTEMPT_TIMEOUT" bash "$0" download "$CACHE_DIR" "${requested_packages[@]}"; then
|
||||||
|
echo "::endgroup::"
|
||||||
|
# Apt handles aliases (e.g. libmagic1 -> libmagic1t64) and dependencies.
|
||||||
|
"${SUDO[@]}" apt-get install -q --yes --no-install-recommends --no-download \
|
||||||
|
-o "Dir::Cache::archives=$CACHE_DIR" "${requested_packages[@]}"
|
||||||
|
# Allow the unprivileged cache action to read archives downloaded as root.
|
||||||
|
find "$CACHE_DIR" -maxdepth 1 -name '*.deb' -exec "${SUDO[@]}" chmod a+r {} +
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
echo "::warning::apt download attempt ${attempt}/${ATTEMPTS} failed or exceeded ${ATTEMPT_TIMEOUT}s"
|
||||||
|
if [ "$attempt" -eq 1 ] && [ "$ATTEMPTS" -gt 1 ]; then
|
||||||
|
echo "Switching from Azure to Ubuntu archive mirrors for remaining attempts"
|
||||||
|
use_ubuntu_mirrors
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "::error::apt download failed after ${ATTEMPTS} attempts"
|
||||||
|
exit 1
|
||||||
@@ -8,7 +8,8 @@ on:
|
|||||||
- 'translations**'
|
- 'translations**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: backend-${{ github.event.pull_request.number || github.ref }}
|
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
|
||||||
|
group: backend-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.13.x"
|
DEFAULT_UV_VERSION: "0.13.x"
|
||||||
@@ -76,6 +77,7 @@ jobs:
|
|||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
- 'docker/compose/docker-compose.ci-test.yml'
|
- 'docker/compose/docker-compose.ci-test.yml'
|
||||||
- '.github/workflows/ci-backend.yml'
|
- '.github/workflows/ci-backend.yml'
|
||||||
|
- '.github/actions/apt-install/**'
|
||||||
test:
|
test:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.backend_changed == 'true'
|
if: needs.changes.outputs.backend_changed == 'true'
|
||||||
@@ -98,11 +100,11 @@ jobs:
|
|||||||
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -168,11 +170,11 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.DEFAULT_PYTHON }}"
|
python-version: "${{ env.DEFAULT_PYTHON }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ on:
|
|||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: docker-${{ github.event.pull_request.number || github.ref }}
|
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
|
||||||
|
group: docker-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
@@ -108,7 +109,7 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -154,7 +155,7 @@ jobs:
|
|||||||
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
|
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
if: steps.check-push.outputs.should-push == 'true'
|
if: steps.check-push.outputs.should-push == 'true'
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.arch }}
|
name: digests-${{ matrix.arch }}
|
||||||
path: /tmp/digests/digest-${{ matrix.arch }}.txt
|
path: /tmp/digests/digest-${{ matrix.arch }}.txt
|
||||||
@@ -172,7 +173,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digest-*.txt
|
pattern: digest-*.txt
|
||||||
@@ -184,20 +185,20 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: docs-${{ github.event.pull_request.number || github.ref }}
|
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
|
||||||
|
group: docs-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -74,11 +75,11 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ on:
|
|||||||
- 'translations**'
|
- 'translations**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: frontend-${{ github.event.pull_request.number || github.ref }}
|
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
|
||||||
|
group: frontend-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
permissions: {}
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
@@ -68,6 +69,7 @@ jobs:
|
|||||||
frontend:
|
frontend:
|
||||||
- 'src-ui/**'
|
- 'src-ui/**'
|
||||||
- '.github/workflows/ci-frontend.yml'
|
- '.github/workflows/ci-frontend.yml'
|
||||||
|
- '.github/actions/apt-install/**'
|
||||||
install-dependencies:
|
install-dependencies:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
@@ -85,7 +87,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -117,7 +119,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -156,7 +158,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -191,7 +193,7 @@ jobs:
|
|||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
container: mcr.microsoft.com/playwright:v1.62.1-resolute
|
container: mcr.microsoft.com/playwright:v1.64.1-resolute
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
@@ -205,18 +207,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: '0.13.x'
|
version: '0.13.x'
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
@@ -260,7 +262,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ on:
|
|||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'translations**'
|
- 'translations**'
|
||||||
concurrency:
|
concurrency:
|
||||||
group: lint-${{ github.event.pull_request.number || github.ref }}
|
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
|
||||||
|
group: lint-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -21,8 +22,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
- name: Run prek
|
- name: Run prek
|
||||||
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
|
uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
@@ -55,11 +55,11 @@ jobs:
|
|||||||
# ---- Backend Setup ----
|
# ---- Backend Setup ----
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
sudo chown -R 1000:1000 paperless-ngx/
|
sudo chown -R 1000:1000 paperless-ngx/
|
||||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||||
- name: Upload release artifact
|
- name: Upload release artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/paperless-ngx.tar.xz
|
path: dist/paperless-ngx.tar.xz
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
version: ${{ steps.get-version.outputs.version }}
|
version: ${{ steps.get-version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download release artifact
|
- name: Download release artifact
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: ./
|
path: ./
|
||||||
@@ -172,7 +172,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Create release and changelog
|
- name: Create release and changelog
|
||||||
id: create-release
|
id: create-release
|
||||||
uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
|
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
|
||||||
with:
|
with:
|
||||||
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
||||||
tag: ${{ steps.get-version.outputs.version }}
|
tag: ${{ steps.get-version.outputs.version }}
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Upload release archive
|
- name: Upload release archive
|
||||||
uses: shogo82148/actions-upload-release-asset@aaba0f56bdbc1071f4af234d5cb16055e8a400de # v1.10.4
|
uses: shogo82148/actions-upload-release-asset@73f83b114970a3485e1423ecc1fb93d887c9b096 # v1.10.7
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||||
@@ -209,11 +209,11 @@ jobs:
|
|||||||
persist-credentials: true # for pushing changelog branch
|
persist-credentials: true # for pushing changelog branch
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ on:
|
|||||||
- 'translations**'
|
- 'translations**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: static-analysis-${{ github.event.pull_request.number || github.ref }}
|
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
|
||||||
|
group: static-analysis-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -26,7 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
||||||
semgrep:
|
semgrep:
|
||||||
name: Semgrep CE
|
name: Semgrep CE
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-26.04
|
||||||
@@ -44,7 +45,7 @@ jobs:
|
|||||||
- name: Run Semgrep
|
- name: Run Semgrep
|
||||||
run: semgrep scan --config auto --sarif-output results.sarif
|
run: semgrep scan --config auto --sarif-output results.sarif
|
||||||
- name: Upload results to GitHub code scanning
|
- name: Upload results to GitHub code scanning
|
||||||
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Clean temporary images
|
- name: Clean temporary images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
|
uses: stumpylog/image-cleaner-action/ephemeral@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "${{ github.repository_owner }}"
|
owner: "${{ github.repository_owner }}"
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Clean untagged images
|
- name: Clean untagged images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
|
uses: stumpylog/image-cleaner-action/untagged@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "${{ github.repository_owner }}"
|
owner: "${{ github.repository_owner }}"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -47,4 +47,4 @@ jobs:
|
|||||||
# Prefix the list here with "+" to use these queries and those in the 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
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0
|
uses: crowdin/github-action@959dc74ac104447731cba99a016f85d0ae719788 # v3.5.0
|
||||||
with:
|
with:
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||||
steps:
|
steps:
|
||||||
- name: Label PR with release-drafter
|
- name: Label PR with release-drafter
|
||||||
uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
|
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -23,14 +23,14 @@ jobs:
|
|||||||
persist-credentials: true # for pushing translation branch
|
persist-credentials: true # for pushing translation branch
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
timeout-minutes: 12
|
timeout-minutes: 12
|
||||||
uses: $/.github/actions/apt-install
|
uses: $/.github/actions/apt-install
|
||||||
with:
|
with:
|
||||||
packages: gettext
|
packages: gettext
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package_json_file: src-ui/package.json
|
package_json_file: src-ui/package.json
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
cd src-ui
|
cd src-ui
|
||||||
pnpm run ng extract-i18n
|
pnpm run ng extract-i18n
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
|
||||||
with:
|
with:
|
||||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||||
commit_message: "Auto translate strings"
|
commit_message: "Auto translate strings"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ repos:
|
|||||||
- json
|
- json
|
||||||
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/rbubley/mirrors-prettier
|
||||||
rev: 'v3.9.9'
|
rev: 'v3.9.10'
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or:
|
types_or:
|
||||||
@@ -46,16 +46,16 @@ repos:
|
|||||||
- ts
|
- ts
|
||||||
- markdown
|
- markdown
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- prettier@3.9.9
|
- prettier@3.9.10
|
||||||
- 'prettier-plugin-organize-imports@4.3.0'
|
- 'prettier-plugin-organize-imports@4.3.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.16.9
|
rev: v0.17.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.29.4"
|
rev: "v2.30.3"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Guidance for AI agents
|
||||||
|
|
||||||
|
Read [CONTRIBUTING.md](CONTRIBUTING.md) before working on this repository, especially [Use of AI Tools](CONTRIBUTING.md#use-of-ai-tools). These contribution rules apply to all agent-assisted work.
|
||||||
|
|
||||||
|
## Contribution boundaries
|
||||||
|
|
||||||
|
- Do not open or submit pull requests, including "on behalf of" a human. The project does not accept PRs from AI agents. Prepare local changes for a human to review, verify, and submit personally.
|
||||||
|
- The project does not accept PRs that are entirely or mostly AI-derived. Human submission and disclosure do not override this rule.
|
||||||
|
- AI-generated content must be clearly disclosed as described in the contributing guidelines. When handing work back to a human, identify what was generated or changed with AI assistance so they can disclose it accurately.
|
||||||
|
- Trust matters. Encourage first-time contributors to discuss their proposed change before preparing a PR, especially for large changes or changes involving AI-generated code. New features and enhancements should almost always target an existing feature request with community interest and discussion.
|
||||||
|
- The project prefers thoughtful, well-described ideas to AI-generated pull requests. Help contributors describe their problem, use case, desired behavior, and who would benefit. An implementation is not required for a valuable contribution, and a flawed implementation can obscure a useful idea before it receives proper discussion.
|
||||||
|
- AI tools have lowered the effort needed to produce a PR, but reviewing and maintaining it still requires substantial work. Encourage discussion of the idea before investing in implementation and review.
|
||||||
|
- Issue reports must describe observed behavior, reproduction steps, and relevant logs. Do not add speculative root causes, code analysis, or proposed fixes to agent-generated reports.
|
||||||
|
|
||||||
|
## Security reports
|
||||||
|
|
||||||
|
- AI-assisted security reports must meet the legitimacy and verification requirements in [SECURITY.md](SECURITY.md#ai-assisted-submissions). A human must verify a real Paperless-ngx code path, concrete reproduction steps, affected versions, and realistic security impact before submission.
|
||||||
|
- Account for existing permission checks, configuration requirements, and trust boundaries. Speculation, scanner output, or concerns about third-party software or deployment choices without a demonstrated Paperless-ngx vulnerability do not meet this threshold and may be closed without extended discussion.
|
||||||
|
- Disclose AI assistance and use the private vulnerability reporting channel specified in [SECURITY.md](SECURITY.md#reporting-a-vulnerability).
|
||||||
|
|
||||||
|
## Working on changes
|
||||||
|
|
||||||
|
- Read the relevant code, tests, and documentation before editing. Paperless-ngx is complex: trace interactions between affected components rather than assuming a change is isolated.
|
||||||
|
- Keep changes focused on the requested work and follow existing patterns. Avoid unrelated refactoring, formatting, dependency updates, or generated-file changes.
|
||||||
|
- Consider effects on permissions, document processing, background tasks, storage, and the API when those areas are involved.
|
||||||
|
- Add or update meaningful tests for behavior changes and regressions. Do not weaken tests or checks to make a change pass.
|
||||||
|
- Update user or developer documentation when behavior or setup changes.
|
||||||
|
- Keep credentials and private documents out of commits, fixtures, logs, and examples.
|
||||||
|
|
||||||
|
## Repository and validation
|
||||||
|
|
||||||
|
- `src/`: Django back end.
|
||||||
|
- `src-ui/`: Angular front end, managed with `pnpm`.
|
||||||
|
- `docs/`: Documentation.
|
||||||
|
- `docker/` and `scripts/`: Deployment and development tooling.
|
||||||
|
- Base functional changes on `dev`; documentation-only changes may target `main`.
|
||||||
|
|
||||||
|
Follow [the development documentation](docs/development.md) for environment setup and testing. Common checks after setup are:
|
||||||
|
|
||||||
|
- Back end: run `uv run pytest` from `src/` (use a test path to focus on the affected area).
|
||||||
|
- Front end: run `pnpm test` and `pnpm lint` from `src-ui/`.
|
||||||
|
- Formatting and other pre-commit checks: run `uv run prek run --files <changed-files>` from the repository root.
|
||||||
|
|
||||||
|
When handing work back, summarize the changes, the checks actually run and their results, and any remaining uncertainty. Clearly state when a check could not be run; do not claim verification that did not happen. The human contributor must understand and independently verify the contribution before submitting it.
|
||||||
+16
-3
@@ -4,6 +4,18 @@ If you feel like contributing to the project, please do! Bug fixes and improveme
|
|||||||
|
|
||||||
⚠️ Please note: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Pull requests that are opened without meeting this requirement may not be merged.
|
⚠️ Please note: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Pull requests that are opened without meeting this requirement may not be merged.
|
||||||
|
|
||||||
|
## Trust and collaboration
|
||||||
|
|
||||||
|
Paperless-ngx places a high value on trust between contributors and maintainers. If you have not contributed before, consider starting with a discussion before preparing a pull request, especially for a large contribution or one involving AI-generated code.
|
||||||
|
|
||||||
|
Paperless-ngx is complex, and understanding how its components interact takes time. A change that appears correct in isolation can have consequences elsewhere in the application. Contributors should understand the code they submit, explain how it fits into the project, and work with maintainers through review. Starting with smaller, focused contributions can help build that understanding and trust.
|
||||||
|
|
||||||
|
We prefer thoughtful, well-described ideas to AI-generated pull requests. You do not need to supply an implementation for your contribution to be valuable. Explain the problem you have encountered, your use case, the behavior you would like to see, and who would benefit. Concrete examples, alternatives you have considered, and relevant constraints help the community discuss and refine the idea.
|
||||||
|
|
||||||
|
AI tools have lowered the effort needed to produce a pull request, but evaluating one still requires substantial maintainer time. Maintainers still need to establish whether the implementation is correct, fits the project, and can be maintained over time.
|
||||||
|
|
||||||
|
A flawed implementation can also obscure a good idea: discussion becomes focused on problems with the generated code before the community has considered whether the underlying proposal is useful. Starting with the idea gives it a fair discussion on its own merits and lets contributors and maintainers agree on an approach before investing in implementation and review.
|
||||||
|
|
||||||
If you want to implement something big:
|
If you want to implement something big:
|
||||||
|
|
||||||
- As above, please start with a discussion! Maybe something similar is already in development and we can make it happen together.
|
- As above, please start with a discussion! Maybe something similar is already in development and we can make it happen together.
|
||||||
@@ -61,9 +73,10 @@ This project does not specifically prohibit the use of AI tools or agents _durin
|
|||||||
|
|
||||||
1. Any content — code, pull request descriptions, issue reports or comments — that was generated in whole or in part by an AI tool or agent **must be clearly disclosed as such**. Failing to clearly disclose this is considered a violation of our [Code of Conduct](CODE_OF_CONDUCT.md) and may result in your contributions being closed and further action being taken.
|
1. Any content — code, pull request descriptions, issue reports or comments — that was generated in whole or in part by an AI tool or agent **must be clearly disclosed as such**. Failing to clearly disclose this is considered a violation of our [Code of Conduct](CODE_OF_CONDUCT.md) and may result in your contributions being closed and further action being taken.
|
||||||
2. AI-generated code must not violate copyright protections.
|
2. AI-generated code must not violate copyright protections.
|
||||||
3. We will not accept PRs that are entirely or mostly AI-derived.
|
3. We will not accept PRs opened or submitted by AI agents, including agents acting "on behalf of" a human. A human contributor must personally review, verify, and submit the contribution and take responsibility for it throughout review.
|
||||||
4. Issue reports must describe _observed behavior only_: what happened, how to reproduce it and the relevant logs. Reports generated by an AI agent should not include code analysis, speculation about the root cause or suggest fixes.
|
4. We will not accept PRs that are entirely or mostly AI-derived.
|
||||||
5. Contributors are responsible for everything they submit, regardless of how it was produced. Please do not open issues or PRs you have not verified yourself.
|
5. Issue reports must describe _observed behavior only_: what happened, how to reproduce it and the relevant logs. Reports generated by an AI agent should not include code analysis, speculation about the root cause or suggest fixes.
|
||||||
|
6. Contributors are responsible for everything they submit, regardless of how it was produced. Please do not open issues or PRs you have not verified yourself.
|
||||||
|
|
||||||
# Translating Paperless-ngx
|
# Translating Paperless-ngx
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.13.5-python3.14-trixie-slim@sha256:d8e732839dfc4225a1ff698803b478b2223c05636c055d3add41ab9dbec0f315 AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.13.7-python3.14-trixie-slim@sha256:e80c05a310400cfc90af00b5b7271ef9ad24816e3265e1162af8a678e1aad099 AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -56,6 +56,9 @@ We may close reports that are:
|
|||||||
|
|
||||||
The following are not generally considered vulnerabilities unless accompanied by a concrete, reproducible impact in Paperless-ngx:
|
The following are not generally considered vulnerabilities unless accompanied by a concrete, reproducible impact in Paperless-ngx:
|
||||||
|
|
||||||
|
- password length inferred from masked mail-account passwords by users who already have permission to view the account, without disclosure of the password or token itself
|
||||||
|
- disclosure limited to document IDs, existence, page counts, checksums, or modification/deletion timestamps, without access to document contents or an unintended privileged action
|
||||||
|
- authenticated access to non-secret application settings, such as service endpoint URLs, model names, or configured environment variable names, without disclosure of credentials or secret values
|
||||||
- large uploads or resource usage that do not bypass documented limits or privileges
|
- large uploads or resource usage that do not bypass documented limits or privileges
|
||||||
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
|
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
|
||||||
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
|
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
|
||||||
@@ -66,7 +69,7 @@ The following are not generally considered vulnerabilities unless accompanied by
|
|||||||
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
|
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
|
||||||
- missing limits or hardening settings presented without concrete impact
|
- missing limits or hardening settings presented without concrete impact
|
||||||
- generic AI or static-analysis output that is not confirmed against the current codebase and a real deployment scenario
|
- generic AI or static-analysis output that is not confirmed against the current codebase and a real deployment scenario
|
||||||
- metadata names visible in a document's custom storage path, even when the user cannot access the underlying metadata object; this is expected behavior
|
- metadata names or normalized words visible in a document's custom storage path or through search/autocomplete for documents the user can access, even when the user cannot access the underlying metadata object; this is expected behavior
|
||||||
- the ability to attach objects that a user cannot access to a document by ID is an intentional design choice, and not considered a vulnerability
|
- the ability to attach objects that a user cannot access to a document by ID is an intentional design choice, and not considered a vulnerability
|
||||||
|
|
||||||
## Transparency
|
## Transparency
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# correct networking for the tests
|
# correct networking for the tests
|
||||||
services:
|
services:
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.37
|
image: docker.io/gotenberg/gotenberg:8.38
|
||||||
hostname: gotenberg
|
hostname: gotenberg
|
||||||
container_name: gotenberg
|
container_name: gotenberg
|
||||||
network_mode: host
|
network_mode: host
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
greenmail:
|
greenmail:
|
||||||
image: docker.io/greenmail/standalone:2.1.14
|
image: docker.io/greenmail/standalone:2.1.15
|
||||||
hostname: greenmail
|
hostname: greenmail
|
||||||
container_name: greenmail
|
container_name: greenmail
|
||||||
environment:
|
environment:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- "3143:3143" # IMAP
|
- "3143:3143" # IMAP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
nginx:
|
nginx:
|
||||||
image: docker.io/nginx:1.31.6-alpine
|
image: docker.io/nginx:1.31.7-alpine
|
||||||
hostname: nginx
|
hostname: nginx
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.37
|
image: docker.io/gotenberg/gotenberg:8.38
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.37
|
image: docker.io/gotenberg/gotenberg:8.38
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.37
|
image: docker.io/gotenberg/gotenberg:8.38
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
# Container init flow
|
||||||
|
|
||||||
|
Startup order of the s6-rc oneshot services under `docker/rootfs/etc/s6-overlay/s6-rc.d/`. An arrow `A --> B` means B lists A in its `dependencies.d/`, so B waits for A. Redundant (transitive) edges are omitted.
|
||||||
|
|
||||||
|
Each labelled box is a group of services that can run in parallel: every service in it starts as soon as its own dependencies finish, so services in the same box run in parallel. A service does not wait for the rest of its group, only for the arrows pointing at it. Long-running services such as `init-tesseract-langs` (package installs) overlap with several later steps.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph w3 ["Run in parallel"]
|
||||||
|
init-tesseract-langs
|
||||||
|
init-modify-user
|
||||||
|
init-wait-for-db
|
||||||
|
init-wait-for-redis
|
||||||
|
end
|
||||||
|
subgraph w4 ["Run in parallel"]
|
||||||
|
init-folders
|
||||||
|
init-compile-bytecode
|
||||||
|
end
|
||||||
|
subgraph w6 ["Run in parallel"]
|
||||||
|
init-search-index
|
||||||
|
init-llmindex-migrate
|
||||||
|
init-superuser
|
||||||
|
end
|
||||||
|
|
||||||
|
base --> init-start --> init-env-file
|
||||||
|
|
||||||
|
init-env-file --> init-tesseract-langs
|
||||||
|
init-env-file --> init-modify-user
|
||||||
|
init-env-file --> init-wait-for-db
|
||||||
|
init-env-file --> init-wait-for-redis
|
||||||
|
init-start --> init-compile-bytecode
|
||||||
|
|
||||||
|
init-modify-user --> init-folders
|
||||||
|
init-modify-user --> init-compile-bytecode
|
||||||
|
|
||||||
|
init-folders --> init-migrations
|
||||||
|
init-compile-bytecode --> init-migrations
|
||||||
|
init-wait-for-db --> init-migrations
|
||||||
|
|
||||||
|
init-migrations --> init-search-index
|
||||||
|
init-migrations --> init-llmindex-migrate
|
||||||
|
init-migrations --> init-superuser
|
||||||
|
|
||||||
|
init-tesseract-langs --> init-search-index
|
||||||
|
init-tesseract-langs --> init-llmindex-migrate
|
||||||
|
init-tesseract-langs --> init-system-checks
|
||||||
|
init-superuser --> init-system-checks
|
||||||
|
|
||||||
|
init-search-index --> init-custom-init
|
||||||
|
init-system-checks --> init-custom-init
|
||||||
|
init-wait-for-redis --> init-custom-init
|
||||||
|
|
||||||
|
init-custom-init --> init-complete
|
||||||
|
init-llmindex-migrate --> init-complete
|
||||||
|
```
|
||||||
|
|
||||||
|
`init-search-index` and `init-llmindex-migrate` run `manage.py`, which runs the Django system checks, including the check that the configured OCR languages are installed, so they wait for `init-tesseract-langs`.
|
||||||
@@ -39,7 +39,6 @@ declare -a scope=(
|
|||||||
"${site_packages}/allauth"
|
"${site_packages}/allauth"
|
||||||
"${site_packages}/drf_spectacular"
|
"${site_packages}/drf_spectacular"
|
||||||
"${site_packages}/drf_spectacular_sidecar"
|
"${site_packages}/drf_spectacular_sidecar"
|
||||||
"${site_packages}/treenode"
|
|
||||||
"${site_packages}/compression_middleware"
|
"${site_packages}/compression_middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -10,9 +10,11 @@ if [[ -n "${PAPERLESS_CONSUMER_DISABLE}" ]]; then
|
|||||||
else
|
else
|
||||||
cd ${PAPERLESS_SRC_DIR}
|
cd ${PAPERLESS_SRC_DIR}
|
||||||
|
|
||||||
|
# Django system checks already ran once in init-system-checks, skip repeating them
|
||||||
|
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
exec python3 manage.py document_consumer
|
exec python3 manage.py document_consumer --skip-checks
|
||||||
else
|
else
|
||||||
exec s6-setuidgid paperless python3 manage.py document_consumer
|
exec s6-setuidgid paperless python3 manage.py document_consumer --skip-checks
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
cd ${PAPERLESS_SRC_DIR}
|
cd ${PAPERLESS_SRC_DIR}
|
||||||
|
|
||||||
|
# Django system checks already ran once in init-system-checks, skip repeating them
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
exec /usr/local/bin/celery --app paperless beat --loglevel INFO
|
exec /usr/local/bin/celery --skip-checks --app paperless beat --loglevel INFO
|
||||||
else
|
else
|
||||||
exec s6-setuidgid paperless /usr/local/bin/celery --app paperless beat --loglevel INFO
|
exec s6-setuidgid paperless /usr/local/bin/celery --skip-checks --app paperless beat --loglevel INFO
|
||||||
fi
|
fi
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
cd ${PAPERLESS_SRC_DIR}
|
cd ${PAPERLESS_SRC_DIR}
|
||||||
|
|
||||||
|
# Django system checks already ran once in init-system-checks, skip repeating them
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
exec /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
|
exec /usr/local/bin/celery --skip-checks --app paperless worker --loglevel INFO --without-mingle --without-gossip
|
||||||
else
|
else
|
||||||
exec s6-setuidgid paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
|
exec s6-setuidgid paperless /usr/local/bin/celery --skip-checks --app paperless worker --loglevel INFO --without-mingle --without-gossip
|
||||||
fi
|
fi
|
||||||
@@ -565,6 +565,8 @@ Specify `rebuild` to build the index from scratch from all documents in the data
|
|||||||
this the first time you enable the feature, or after changing the embedding backend or
|
this the first time you enable the feature, or after changing the embedding backend or
|
||||||
model.
|
model.
|
||||||
|
|
||||||
|
When using the `huggingface` embedding backend, a model that is already downloaded is loaded from the local cache without contacting the Hugging Face Hub. Only `rebuild` checks the Hub for a newer revision of the model, so a model update is picked up together with re-embedding every document. If the model is not cached yet, it is downloaded on first use.
|
||||||
|
|
||||||
Specify `update` to incrementally index new and changed documents. This is what the
|
Specify `update` to incrementally index new and changed documents. This is what the
|
||||||
scheduled task runs.
|
scheduled task runs.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,96 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.4.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: mobile save button display obsessing [@shamoon](https://github.com/shamoon) ([#14676](https://github.com/paperless-ngx/paperless-ngx/pull/14676))
|
||||||
|
- Fix: make index and llmindex init wait for tesseract language install [@stumpylog](https://github.com/stumpylog) ([#14670](https://github.com/paperless-ngx/paperless-ngx/pull/14670))
|
||||||
|
- Fix: prevent loss of save view name on save + close [@shamoon](https://github.com/shamoon) ([#14667](https://github.com/paperless-ngx/paperless-ngx/pull/14667))
|
||||||
|
- Fixhancement: doc detail mobile ux tweaks [@shamoon](https://github.com/shamoon) ([#14664](https://github.com/paperless-ngx/paperless-ngx/pull/14664))
|
||||||
|
- Fix: Bumps filelock to 4.2.6 to pick up ACL related fixes [@stumpylog](https://github.com/stumpylog) ([#14656](https://github.com/paperless-ngx/paperless-ngx/pull/14656))
|
||||||
|
- Fix: ensure import bulk\_edit tasks in Celery workers [@shamoon](https://github.com/shamoon) ([#14653](https://github.com/paperless-ngx/paperless-ngx/pull/14653))
|
||||||
|
- Fixhancement: add tag names to document history list [@shamoon](https://github.com/shamoon) ([#14625](https://github.com/paperless-ngx/paperless-ngx/pull/14625))
|
||||||
|
- Fix: add button type to zoom buttons to prevent form submission [@shamoon](https://github.com/shamoon) ([#14624](https://github.com/paperless-ngx/paperless-ngx/pull/14624))
|
||||||
|
- Fix: move document list preview button to own column [@shamoon](https://github.com/shamoon) ([#14610](https://github.com/paperless-ngx/paperless-ngx/pull/14610))
|
||||||
|
- Fix: Various small bugs found during typing updates [@stumpylog](https://github.com/stumpylog) ([#14593](https://github.com/paperless-ngx/paperless-ngx/pull/14593))
|
||||||
|
- Fix: fix custom fields dropdown text size [@shamoon](https://github.com/shamoon) ([#14606](https://github.com/paperless-ngx/paperless-ngx/pull/14606))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>14 changes</summary>
|
||||||
|
|
||||||
|
- Chore: ditch the abandoned ngx-bootstrap-icons, roll our own [@shamoon](https://github.com/shamoon) ([#14665](https://github.com/paperless-ngx/paperless-ngx/pull/14665))
|
||||||
|
- chore(deps): bump turbohtml to 1.15.3 [@stumpylog](https://github.com/stumpylog) ([#14662](https://github.com/paperless-ngx/paperless-ngx/pull/14662))
|
||||||
|
- docker-compose(deps): bump gotenberg/gotenberg from 8.37 to 8.38 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14634](https://github.com/paperless-ngx/paperless-ngx/pull/14634))
|
||||||
|
- chore(deps): bump Django, django-guardian, django-filter, drf-spectacular-sidecar [@stumpylog](https://github.com/stumpylog) ([#14661](https://github.com/paperless-ngx/paperless-ngx/pull/14661))
|
||||||
|
- docker-compose(deps): bump greenmail/standalone from 2.1.14 to 2.1.15 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14631](https://github.com/paperless-ngx/paperless-ngx/pull/14631))
|
||||||
|
- docker-compose(deps): Bump nginx from 1.31.6-alpine to 1.31.7-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14633](https://github.com/paperless-ngx/paperless-ngx/pull/14633))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14637](https://github.com/paperless-ngx/paperless-ngx/pull/14637))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.62.1 to 1.64.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14641](https://github.com/paperless-ngx/paperless-ngx/pull/14641))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 6.3.289 to 6.4.299 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14644](https://github.com/paperless-ngx/paperless-ngx/pull/14644))
|
||||||
|
- Chore(deps): Bump uuid from 14.0.2 to 14.0.3 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14640](https://github.com/paperless-ngx/paperless-ngx/pull/14640))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14638](https://github.com/paperless-ngx/paperless-ngx/pull/14638))
|
||||||
|
- Chore(deps): Bump marked from 18.0.14 to 18.1.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14643](https://github.com/paperless-ngx/paperless-ngx/pull/14643))
|
||||||
|
- Chore(deps-dev): Bump prettier from 3.9.9 to 3.9.10 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14645](https://github.com/paperless-ngx/paperless-ngx/pull/14645))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 26.6.2 to 26.6.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14639](https://github.com/paperless-ngx/paperless-ngx/pull/14639))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>21 changes</summary>
|
||||||
|
|
||||||
|
- Fix: mobile save button display obsessing [@shamoon](https://github.com/shamoon) ([#14676](https://github.com/paperless-ngx/paperless-ngx/pull/14676))
|
||||||
|
- Fix: prevent loss of save view name on save + close [@shamoon](https://github.com/shamoon) ([#14667](https://github.com/paperless-ngx/paperless-ngx/pull/14667))
|
||||||
|
- Chore: ditch the abandoned ngx-bootstrap-icons, roll our own [@shamoon](https://github.com/shamoon) ([#14665](https://github.com/paperless-ngx/paperless-ngx/pull/14665))
|
||||||
|
- Fixhancement: doc detail mobile ux tweaks [@shamoon](https://github.com/shamoon) ([#14664](https://github.com/paperless-ngx/paperless-ngx/pull/14664))
|
||||||
|
- chore(deps): bump turbohtml to 1.15.3 [@stumpylog](https://github.com/stumpylog) ([#14662](https://github.com/paperless-ngx/paperless-ngx/pull/14662))
|
||||||
|
- chore(deps): bump Django, django-guardian, django-filter, drf-spectacular-sidecar [@stumpylog](https://github.com/stumpylog) ([#14661](https://github.com/paperless-ngx/paperless-ngx/pull/14661))
|
||||||
|
- Fix: Bumps filelock to 4.2.6 to pick up ACL related fixes [@stumpylog](https://github.com/stumpylog) ([#14656](https://github.com/paperless-ngx/paperless-ngx/pull/14656))
|
||||||
|
- Fix: ensure import bulk\_edit tasks in Celery workers [@shamoon](https://github.com/shamoon) ([#14653](https://github.com/paperless-ngx/paperless-ngx/pull/14653))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14637](https://github.com/paperless-ngx/paperless-ngx/pull/14637))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.62.1 to 1.64.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14641](https://github.com/paperless-ngx/paperless-ngx/pull/14641))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 6.3.289 to 6.4.299 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14644](https://github.com/paperless-ngx/paperless-ngx/pull/14644))
|
||||||
|
- Chore(deps): Bump uuid from 14.0.2 to 14.0.3 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14640](https://github.com/paperless-ngx/paperless-ngx/pull/14640))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14638](https://github.com/paperless-ngx/paperless-ngx/pull/14638))
|
||||||
|
- Chore(deps): Bump marked from 18.0.14 to 18.1.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14643](https://github.com/paperless-ngx/paperless-ngx/pull/14643))
|
||||||
|
- Chore(deps-dev): Bump prettier from 3.9.9 to 3.9.10 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14645](https://github.com/paperless-ngx/paperless-ngx/pull/14645))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 26.6.2 to 26.6.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14639](https://github.com/paperless-ngx/paperless-ngx/pull/14639))
|
||||||
|
- Fixhancement: add tag names to document history list [@shamoon](https://github.com/shamoon) ([#14625](https://github.com/paperless-ngx/paperless-ngx/pull/14625))
|
||||||
|
- Fix: add button type to zoom buttons to prevent form submission [@shamoon](https://github.com/shamoon) ([#14624](https://github.com/paperless-ngx/paperless-ngx/pull/14624))
|
||||||
|
- Fix: move document list preview button to own column [@shamoon](https://github.com/shamoon) ([#14610](https://github.com/paperless-ngx/paperless-ngx/pull/14610))
|
||||||
|
- Fix: Various small bugs found during typing updates [@stumpylog](https://github.com/stumpylog) ([#14593](https://github.com/paperless-ngx/paperless-ngx/pull/14593))
|
||||||
|
- Fix: fix custom fields dropdown text size [@shamoon](https://github.com/shamoon) ([#14606](https://github.com/paperless-ngx/paperless-ngx/pull/14606))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 3.4.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix the id\_\_ schemae in the right place [@shamoon](https://github.com/shamoon) ([#14604](https://github.com/paperless-ngx/paperless-ngx/pull/14604))
|
||||||
|
- Fix: drop redundant change perms checks on cards open button [@shamoon](https://github.com/shamoon) ([#14601](https://github.com/paperless-ngx/paperless-ngx/pull/14601))
|
||||||
|
- Tweak/fix: make new pdf controls mostly hidden [@shamoon](https://github.com/shamoon) ([#14595](https://github.com/paperless-ngx/paperless-ngx/pull/14595))
|
||||||
|
- Tweak/fix: move preview button in doc list [@shamoon](https://github.com/shamoon) ([#14594](https://github.com/paperless-ngx/paperless-ngx/pull/14594))
|
||||||
|
- Fix: checksum light mode display and cf dropdown mobile position [@shamoon](https://github.com/shamoon) ([#14590](https://github.com/paperless-ngx/paperless-ngx/pull/14590))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>6 changes</summary>
|
||||||
|
|
||||||
|
- Fix the id\_\_ schemae in the right place [@shamoon](https://github.com/shamoon) ([#14604](https://github.com/paperless-ngx/paperless-ngx/pull/14604))
|
||||||
|
- Fix: drop redundant change perms checks on cards open button [@shamoon](https://github.com/shamoon) ([#14601](https://github.com/paperless-ngx/paperless-ngx/pull/14601))
|
||||||
|
- Chore: Improve idle memory via more lazy imports [@stumpylog](https://github.com/stumpylog) ([#14599](https://github.com/paperless-ngx/paperless-ngx/pull/14599))
|
||||||
|
- Tweak/fix: make new pdf controls mostly hidden [@shamoon](https://github.com/shamoon) ([#14595](https://github.com/paperless-ngx/paperless-ngx/pull/14595))
|
||||||
|
- Tweak/fix: move preview button in doc list [@shamoon](https://github.com/shamoon) ([#14594](https://github.com/paperless-ngx/paperless-ngx/pull/14594))
|
||||||
|
- Fix: checksum light mode display and cf dropdown mobile position [@shamoon](https://github.com/shamoon) ([#14590](https://github.com/paperless-ngx/paperless-ngx/pull/14590))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 3.4.0
|
## paperless-ngx 3.4.0
|
||||||
|
|
||||||
### Features / Enhancements
|
### Features / Enhancements
|
||||||
|
|||||||
+22
-11
@@ -501,20 +501,20 @@ See the [Django project documentation on the settings](https://docs.djangoprojec
|
|||||||
|
|
||||||
#### [`PAPERLESS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_ALLOWED_HOSTS) {#PAPERLESS_ALLOWED_HOSTS}
|
#### [`PAPERLESS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_ALLOWED_HOSTS) {#PAPERLESS_ALLOWED_HOSTS}
|
||||||
|
|
||||||
: If you're planning on putting Paperless on the open internet, then
|
: If you expose Paperless to the internet, set `PAPERLESS_URL` to its public URL
|
||||||
you really should set this value to the domain name you're using.
|
or list its public hostname in `PAPERLESS_ALLOWED_HOSTS` to help protect against
|
||||||
Failing to do so leaves you open to HTTP host header attacks.
|
HTTP Host header attacks.
|
||||||
You can read more about this in [the Django project's documentation](https://docs.djangoproject.com/en/4.1/topics/security/#host-header-validation)
|
You can read more about this in [the Django project's documentation](https://docs.djangoproject.com/en/4.1/topics/security/#host-header-validation)
|
||||||
|
|
||||||
Just remember that this is a comma-separated list, so
|
Separate multiple hostnames with commas, as in `example.com,www.example.com`.
|
||||||
"example.com" is fine, as is "example.com,www.example.com", but
|
|
||||||
NOT " example.com" or "example.com,"
|
|
||||||
|
|
||||||
Can also be set using PAPERLESS_URL (see above).
|
- With neither setting configured, Paperless accepts any host.
|
||||||
|
- With only `PAPERLESS_URL` set, Paperless accepts the hostname from that URL
|
||||||
"localhost" is always allowed for docker healthcheck
|
and `localhost` (used by the Docker health check).
|
||||||
|
- `PAPERLESS_ALLOWED_HOSTS` adds hosts to `localhost` and the hostname from
|
||||||
Defaults to "\*", which is all hosts.
|
`PAPERLESS_URL`, if set. A blank list adds no hosts.
|
||||||
|
- With `PAPERLESS_ALLOWED_HOSTS=*`, Paperless accepts any host, even when
|
||||||
|
`PAPERLESS_URL` is set.
|
||||||
|
|
||||||
#### [`PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_CORS_ALLOWED_HOSTS) {#PAPERLESS_CORS_ALLOWED_HOSTS}
|
#### [`PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_CORS_ALLOWED_HOSTS) {#PAPERLESS_CORS_ALLOWED_HOSTS}
|
||||||
|
|
||||||
@@ -873,6 +873,14 @@ If both the [PAPERLESS_ACCOUNT_DEFAULT_GROUPS](#PAPERLESS_ACCOUNT_DEFAULT_GROUPS
|
|||||||
|
|
||||||
Defaults to 1209600 (2 weeks)
|
Defaults to 1209600 (2 weeks)
|
||||||
|
|
||||||
|
#### [`PAPERLESS_SESSION_CLEANUP_TASK_CRON=<cron expression>`](#PAPERLESS_SESSION_CLEANUP_TASK_CRON) {#PAPERLESS_SESSION_CLEANUP_TASK_CRON}
|
||||||
|
|
||||||
|
: Configures the schedule to remove expired sessions from the session store.
|
||||||
|
Django does not prune these automatically, so without this they would
|
||||||
|
accumulate indefinitely.
|
||||||
|
|
||||||
|
Defaults to `0 3 * * *`, once per day at 03:00.
|
||||||
|
|
||||||
#### [`PAPERLESS_TOKEN_THROTTLE_RATE=<rate>`](#PAPERLESS_TOKEN_THROTTLE_RATE) {#PAPERLESS_TOKEN_THROTTLE_RATE}
|
#### [`PAPERLESS_TOKEN_THROTTLE_RATE=<rate>`](#PAPERLESS_TOKEN_THROTTLE_RATE) {#PAPERLESS_TOKEN_THROTTLE_RATE}
|
||||||
|
|
||||||
: Rate limit for the API token authentication endpoint (`/api/token/`), used to mitigate brute-force login attempts.
|
: Rate limit for the API token authentication endpoint (`/api/token/`), used to mitigate brute-force login attempts.
|
||||||
@@ -1149,6 +1157,9 @@ they use underscores instead of dashes.
|
|||||||
be changed by trusted users. This applies to the `AppConfig` permission as well,
|
be changed by trusted users. This applies to the `AppConfig` permission as well,
|
||||||
which allows setting these arguments from the UI.
|
which allows setting these arguments from the UI.
|
||||||
|
|
||||||
|
Arguments paperless manages itself (`input_file_or_options`, `output_file`
|
||||||
|
and `sidecar`) are rejected by the API and ignored if set here.
|
||||||
|
|
||||||
Specify arguments as a JSON dictionary. Keep note of lower case
|
Specify arguments as a JSON dictionary. Keep note of lower case
|
||||||
booleans and double quoted parameter names and strings. Examples:
|
booleans and double quoted parameter names and strings. Examples:
|
||||||
|
|
||||||
|
|||||||
+17
-23
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "3.4.0"
|
version = "3.4.2"
|
||||||
description = """\
|
description = """\
|
||||||
A community-supported supercharged document management system: scan, index and archive all your physical documents\
|
A community-supported supercharged document management system: scan, index and archive all your physical documents\
|
||||||
"""
|
"""
|
||||||
@@ -26,22 +26,21 @@ dependencies = [
|
|||||||
"dateparser~=1.4",
|
"dateparser~=1.4",
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.13",
|
"django~=5.2.18",
|
||||||
"django-allauth[mfa,socialaccount]~=65.20.0",
|
"django-allauth[mfa,socialaccount]~=65.21.1",
|
||||||
"django-auditlog~=3.4.1",
|
"django-auditlog~=3.4.1",
|
||||||
"django-cachalot~=2.9.0",
|
"django-cachalot~=2.9.0",
|
||||||
"django-compression-middleware~=0.5.0",
|
"django-compression-middleware~=0.5.0",
|
||||||
"django-cors-headers~=4.9.0",
|
"django-cors-headers~=4.9.0",
|
||||||
"django-extensions~=4.1",
|
"django-extensions~=4.1",
|
||||||
"django-filter>=25.1,<27",
|
"django-filter>=26.2,<27",
|
||||||
"django-guardian>=3.3.3,<3.6",
|
"django-guardian>=3.5.2,<3.6",
|
||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=1.0.1",
|
||||||
"django-rich~=2.2.0",
|
"django-rich~=2.2.0",
|
||||||
"django-soft-delete~=1.0.18",
|
"django-soft-delete~=1.0.18",
|
||||||
"django-treenode>=0.24",
|
|
||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"drf-spectacular~=0.30",
|
"drf-spectacular~=0.30",
|
||||||
"drf-spectacular-sidecar>=2026.7.1,<2026.10",
|
"drf-spectacular-sidecar>=2026.10.1,<2026.11",
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"filelock>=3.32,<4.3",
|
"filelock>=3.32,<4.3",
|
||||||
"flower>=2.0.1,<2.3",
|
"flower>=2.0.1,<2.3",
|
||||||
@@ -76,7 +75,7 @@ dependencies = [
|
|||||||
"tantivy~=0.26.0",
|
"tantivy~=0.26.0",
|
||||||
"tika-client[httpx]~=1.0",
|
"tika-client[httpx]~=1.0",
|
||||||
"torch>=2.13,<2.15",
|
"torch>=2.13,<2.15",
|
||||||
"turbohtml~=1.10.0",
|
"turbohtml~=1.15.3",
|
||||||
"watchfiles>=1.2",
|
"watchfiles>=1.2",
|
||||||
"whitenoise~=6.11",
|
"whitenoise~=6.11",
|
||||||
"whoosh-compat[tantivy]==0.3",
|
"whoosh-compat[tantivy]==0.3",
|
||||||
@@ -87,9 +86,9 @@ mariadb = [
|
|||||||
"mysqlclient>=2.2.7,<2.4",
|
"mysqlclient>=2.2.7,<2.4",
|
||||||
]
|
]
|
||||||
postgres = [
|
postgres = [
|
||||||
"psycopg[c,pool]==3.3.4",
|
"psycopg[c,pool]==3.3.8",
|
||||||
# Direct dependency for proper resolution of the pre-built wheels
|
# Direct dependency for proper resolution of the pre-built wheels
|
||||||
"psycopg-c==3.3.4",
|
"psycopg-c==3.3.8",
|
||||||
"psycopg-pool==3.3.3",
|
"psycopg-pool==3.3.3",
|
||||||
]
|
]
|
||||||
webserver = [
|
webserver = [
|
||||||
@@ -112,14 +111,14 @@ lint = [
|
|||||||
testing = [
|
testing = [
|
||||||
"daphne",
|
"daphne",
|
||||||
"factory-boy~=3.3.1",
|
"factory-boy~=3.3.1",
|
||||||
"faker>=40.36,<40.40",
|
"faker>=40.36,<40.44",
|
||||||
"imagehash",
|
"imagehash",
|
||||||
"pytest~=9.1.1",
|
"pytest~=9.1.1",
|
||||||
"pytest-cov~=7.1.0",
|
"pytest-cov~=7.1.0",
|
||||||
"pytest-django>=4.12,<4.15",
|
"pytest-django>=4.12,<4.15",
|
||||||
"pytest-env~=1.7.0",
|
"pytest-env~=1.8.1",
|
||||||
"pytest-httpx",
|
"pytest-httpx",
|
||||||
"pytest-mock~=3.15.1",
|
"pytest-mock~=3.17.0",
|
||||||
"pytest-randomly~=5.0.0",
|
"pytest-randomly~=5.0.0",
|
||||||
"pytest-rerunfailures~=16.4",
|
"pytest-rerunfailures~=16.4",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
@@ -159,10 +158,10 @@ package = false
|
|||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
# Markers are chosen to select these almost exclusively when building the Docker image
|
# Markers are chosen to select these almost exclusively when building the Docker image
|
||||||
psycopg-c = [
|
psycopg-c = [
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.4/psycopg_c-3.3.4-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.8/psycopg_c-3.3.8-cp312-cp312-manylinux_2_38_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.4/psycopg_c-3.3.4-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.8/psycopg_c-3.3.8-cp312-cp312-manylinux_2_38_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.4/psycopg_c-3.3.4-cp314-cp314-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.14'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.8/psycopg_c-3.3.8-cp314-cp314-manylinux_2_38_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.14'" },
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.4/psycopg_c-3.3.4-cp314-cp314-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.14'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.8/psycopg_c-3.3.8-cp314-cp314-manylinux_2_38_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.14'" },
|
||||||
]
|
]
|
||||||
torch = [
|
torch = [
|
||||||
{ index = "pytorch-cpu" },
|
{ index = "pytorch-cpu" },
|
||||||
@@ -213,12 +212,7 @@ extend-select = [
|
|||||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
||||||
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
||||||
]
|
]
|
||||||
ignore = [
|
ignore = [ "DJ001", "ICN001", "PLC0415", "RUF012", "SIM105" ]
|
||||||
"DJ001",
|
|
||||||
"PLC0415",
|
|
||||||
"RUF012",
|
|
||||||
"SIM105",
|
|
||||||
]
|
|
||||||
# Migrations
|
# Migrations
|
||||||
per-file-ignores."*/migrations/*.py" = [
|
per-file-ignores."*/migrations/*.py" = [
|
||||||
"E501",
|
"E501",
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
"src/polyfills.ts"
|
"src/polyfills.ts"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"loader": {
|
||||||
|
".svg": "text"
|
||||||
|
},
|
||||||
"localize": true,
|
"localize": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/apple-touch-icon.png",
|
"src/apple-touch-icon.png",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ def configure_environment(instance_root: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def seed_database() -> None:
|
def seed_database() -> None:
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -63,11 +64,18 @@ def seed_database() -> None:
|
|||||||
username="playwright",
|
username="playwright",
|
||||||
password="playwright", # NOSONAR
|
password="playwright", # NOSONAR
|
||||||
)
|
)
|
||||||
User.objects.create_user(
|
viewer = User.objects.create_user(
|
||||||
username="viewer",
|
username="viewer",
|
||||||
password="viewer", # NOSONAR
|
password="viewer", # NOSONAR
|
||||||
)
|
)
|
||||||
|
|
||||||
|
viewer.user_permissions.add(
|
||||||
|
Permission.objects.get(
|
||||||
|
content_type__app_label="documents",
|
||||||
|
codename="view_uisettings",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True, owner=admin)
|
inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True, owner=admin)
|
||||||
quick_filter = Tag.objects.create(name="Another Sample Tag", owner=admin)
|
quick_filter = Tag.objects.create(name="Another Sample Tag", owner=admin)
|
||||||
Tag.objects.create(name="TagWithPartial", owner=admin)
|
Tag.objects.create(name="TagWithPartial", owner=admin)
|
||||||
|
|||||||
@@ -80,12 +80,21 @@ test('should show a list of notes', async ({ page }) => {
|
|||||||
|
|
||||||
test('should support quick filters', async ({ page }) => {
|
test('should support quick filters', async ({ page }) => {
|
||||||
await page.goto('/documents/1/details')
|
await page.goto('/documents/1/details')
|
||||||
|
// filter button is available before tags have loaded
|
||||||
|
const selectedTags = page.locator('pngx-input-tags .ng-value pngx-tag')
|
||||||
|
await expect(selectedTags).toHaveCount(2)
|
||||||
|
await expect(
|
||||||
|
selectedTags.getByText('Another Sample Tag', { exact: true })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(selectedTags.getByText('Inbox', { exact: true })).toBeVisible()
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'Filter documents with these Tags' })
|
.getByRole('button', { name: 'Filter documents with these Tags' })
|
||||||
.click()
|
.click()
|
||||||
await expect(page).toHaveURL(
|
await expect(page).toHaveURL((url) => {
|
||||||
/tags__id__all=2,1&sort=created&reverse=1&page=1/
|
const tags = url.searchParams.get('tags__id__all')?.split(',').sort()
|
||||||
)
|
return url.pathname === '/documents' && tags?.join(',') === '1,2'
|
||||||
|
})
|
||||||
|
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should finish reloading the preview after a remote document update', async ({
|
test('should finish reloading the preview after a remote document update', async ({
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ test('sorting', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Notes' }).click()
|
await page.getByRole('button', { name: 'Notes' }).click()
|
||||||
await expect(page).toHaveURL(/sort=num_notes/)
|
await expect(page).toHaveURL(/sort=num_notes/)
|
||||||
await page.getByRole('button', { name: 'Sort' }).click()
|
await page.getByRole('button', { name: 'Sort' }).click()
|
||||||
await page.locator('.w-100 > label > i-bs').first().click()
|
await page.locator('.w-100 > label > pngx-bs-icon').first().click()
|
||||||
await expect(page).not.toHaveURL(/reverse=1/)
|
await expect(page).not.toHaveURL(/reverse=1/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ test.use({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to edit settings', async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const settingsLoaded = page.waitForResponse((response) =>
|
||||||
|
response.url().endsWith('/api/ui_settings/')
|
||||||
|
)
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
|
expect((await settingsLoaded).status()).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not allow user to edit settings', async ({ page }) => {
|
||||||
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
|
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
|
||||||
await page.goto('/settings')
|
await page.goto('/settings')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
@@ -16,7 +23,6 @@ test('should not allow user to edit settings', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view documents', async ({ page }) => {
|
test('should not allow user to view documents', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('nav').getByRole('link', { name: 'Documents' })
|
page.locator('nav').getByRole('link', { name: 'Documents' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
@@ -31,7 +37,6 @@ test('should not allow user to view documents', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view correspondents', async ({ page }) => {
|
test('should not allow user to view correspondents', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
@@ -42,7 +47,6 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view tags', async ({ page }) => {
|
test('should not allow user to view tags', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
@@ -53,7 +57,6 @@ test('should not allow user to view tags', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view document types', async ({ page }) => {
|
test('should not allow user to view document types', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
@@ -64,7 +67,6 @@ test('should not allow user to view document types', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view storage paths', async ({ page }) => {
|
test('should not allow user to view storage paths', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
@@ -75,7 +77,6 @@ test('should not allow user to view storage paths', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view logs', async ({ page }) => {
|
test('should not allow user to view logs', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
|
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
|
||||||
await page.goto('/logs')
|
await page.goto('/logs')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
@@ -84,7 +85,6 @@ test('should not allow user to view logs', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view tasks', async ({ page }) => {
|
test('should not allow user to view tasks', async ({ page }) => {
|
||||||
await page.goto('/dashboard')
|
|
||||||
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
|
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
|
||||||
await page.goto('/tasks')
|
await page.goto('/tasks')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module.exports = {
|
|||||||
'abstract-paperless-service',
|
'abstract-paperless-service',
|
||||||
],
|
],
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!.*(\\.mjs$|tslib|lodash-es|normalize-diacritics|marked|@angular/common/locales/.*\\.js$))',
|
'node_modules/(?!.*(\\.mjs$|tslib|lodash-es|normalize-diacritics|marked|@angular/common/locales/.*\\.js$|bootstrap-icons/icons/.*\\.svg$))',
|
||||||
],
|
],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
...esmPreset.moduleNameMapper,
|
...esmPreset.moduleNameMapper,
|
||||||
|
|||||||
+333
-271
File diff suppressed because it is too large.
Load diff
+25
-25
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "3.4.0",
|
"version": "3.4.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -15,60 +15,60 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.2.2",
|
"@angular/cdk": "^22.2.4",
|
||||||
"@angular/common": "~22.2.2",
|
"@angular/common": "~22.2.3",
|
||||||
"@angular/compiler": "~22.2.2",
|
"@angular/compiler": "~22.2.3",
|
||||||
"@angular/core": "~22.2.2",
|
"@angular/core": "~22.2.3",
|
||||||
"@angular/forms": "~22.2.2",
|
"@angular/forms": "~22.2.3",
|
||||||
"@angular/localize": "~22.2.2",
|
"@angular/localize": "~22.2.3",
|
||||||
"@angular/platform-browser": "~22.2.2",
|
"@angular/platform-browser": "~22.2.3",
|
||||||
"@angular/router": "~22.2.2",
|
"@angular/router": "~22.2.3",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
||||||
"@ng-select/ng-select": "~24.2.1",
|
"@ng-select/ng-select": "~24.2.1",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"bootstrap-icons": "^1.13.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lodash-es": "^4.18.1",
|
"lodash-es": "^4.18.1",
|
||||||
"marked": "~18.0.14",
|
"marked": "~18.1.1",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
|
||||||
"ngx-color": "^10.1.0",
|
"ngx-color": "^10.1.0",
|
||||||
"ngx-cookie-service": "^22.0.0",
|
"ngx-cookie-service": "^22.0.0",
|
||||||
"ngx-device-detector": "^12.0.0",
|
"ngx-device-detector": "^12.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
|
||||||
"normalize-diacritics": "^5.0.0",
|
"normalize-diacritics": "^5.0.0",
|
||||||
"pdfjs-dist": "^6.3.289",
|
"pdfjs-dist": "^6.4.299",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"uuid": "^14.0.2"
|
"uuid": "^14.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/jest": "^22.0.1",
|
"@angular-builders/jest": "^22.0.1",
|
||||||
"@angular-devkit/core": "^22.2.2",
|
"@angular-devkit/core": "^22.2.4",
|
||||||
"@angular-devkit/schematics": "^22.2.2",
|
"@angular-devkit/schematics": "^22.2.4",
|
||||||
"@angular-eslint/builder": "22.6.0",
|
"@angular-eslint/builder": "22.6.0",
|
||||||
"@angular-eslint/eslint-plugin": "22.6.0",
|
"@angular-eslint/eslint-plugin": "22.6.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "22.6.0",
|
"@angular-eslint/eslint-plugin-template": "22.6.0",
|
||||||
"@angular-eslint/schematics": "22.6.0",
|
"@angular-eslint/schematics": "22.6.0",
|
||||||
"@angular-eslint/template-parser": "22.6.0",
|
"@angular-eslint/template-parser": "22.6.0",
|
||||||
"@angular/build": "22.2.2",
|
"@angular/build": "22.2.4",
|
||||||
"@angular/cli": "22.2.2",
|
"@angular/cli": "22.2.4",
|
||||||
"@angular/compiler-cli": "~22.2.2",
|
"@angular/compiler-cli": "~22.2.3",
|
||||||
"@playwright/test": "^1.62.1",
|
"@playwright/test": "^1.64.1",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^26.6.2",
|
"@types/node": "^26.6.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.70.1",
|
"@typescript-eslint/eslint-plugin": "^8.72.0",
|
||||||
"@typescript-eslint/parser": "^8.70.1",
|
"@typescript-eslint/parser": "^8.72.0",
|
||||||
"@typescript-eslint/utils": "^8.70.1",
|
"@typescript-eslint/utils": "^8.72.0",
|
||||||
"eslint": "^10.11.0",
|
"eslint": "^10.13.0",
|
||||||
"jest": "30.5.2",
|
"jest": "30.5.2",
|
||||||
"jest-environment-jsdom": "^30.5.2",
|
"jest-environment-jsdom": "^30.5.2",
|
||||||
"jest-junit": "^17.0.0",
|
"jest-junit": "^17.0.0",
|
||||||
"jest-preset-angular": "^17.0.3",
|
"jest-preset-angular": "^17.0.3",
|
||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"prettier": "^3.9.9",
|
"prettier": "^3.9.10",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"ts-node": "~10.9.2",
|
"ts-node": "~10.9.2",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
|
|||||||
Generated
+702
-664
File diff suppressed because it is too large.
Load diff
@@ -37,7 +37,7 @@ export const routes: Routes = [
|
|||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
component: DashboardComponent,
|
component: DashboardComponent,
|
||||||
data: {
|
data: {
|
||||||
componentName: 'AppFrameComponent',
|
componentName: 'DashboardComponent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { Router, RouterModule } from '@angular/router'
|
import { Router, RouterModule } from '@angular/router'
|
||||||
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import {
|
import {
|
||||||
provideUiTour,
|
provideUiTour,
|
||||||
TourNgBootstrap,
|
TourNgBootstrap,
|
||||||
@@ -47,7 +46,6 @@ describe('AppComponent', () => {
|
|||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
FileDropComponent,
|
FileDropComponent,
|
||||||
TourNgBootstrap,
|
TourNgBootstrap,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{{option.title}}
|
{{option.title}}
|
||||||
</h6>
|
</h6>
|
||||||
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
||||||
<i-bs name="info-circle"></i-bs>
|
<pngx-bs-icon name="info-circle"></pngx-bs-icon>
|
||||||
</a>
|
</a>
|
||||||
@if (isExternallyConfigured(option.config_key)) {
|
@if (isExternallyConfigured(option.config_key)) {
|
||||||
@if (isSet(option.key)) {
|
@if (isSet(option.key)) {
|
||||||
@@ -40,11 +40,11 @@
|
|||||||
@if (isSet(option.key)) {
|
@if (isSet(option.key)) {
|
||||||
@if (isExternallyConfigured(option.config_key)) {
|
@if (isExternallyConfigured(option.config_key)) {
|
||||||
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Use the externally configured value" i18n-title (click)="resetOption(option.key)">
|
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Use the externally configured value" i18n-title (click)="resetOption(option.key)">
|
||||||
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset to external</ng-container>
|
<pngx-bs-icon class="me-1" name="x"></pngx-bs-icon><ng-container i18n>Reset to external</ng-container>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
|
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
|
||||||
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
<pngx-bs-icon class="me-1" name="x"></pngx-bs-icon><ng-container i18n>Reset</ng-container>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
ConfigCategory,
|
ConfigCategory,
|
||||||
@@ -39,7 +38,6 @@ describe('ConfigComponent', () => {
|
|||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
ConfigComponent,
|
ConfigComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
first,
|
first,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import {
|
import {
|
||||||
ConfigCategory,
|
ConfigCategory,
|
||||||
ConfigOption,
|
ConfigOption,
|
||||||
@@ -52,7 +52,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ConfigComponent
|
export class ConfigComponent
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { BrowserModule, By } from '@angular/platform-browser'
|
import { BrowserModule, By } from '@angular/platform-browser'
|
||||||
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { LogService } from 'src/app/services/rest/log.service'
|
import { LogService } from 'src/app/services/rest/log.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
@@ -40,7 +39,6 @@ describe('LogsComponent', () => {
|
|||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
LogsComponent,
|
LogsComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
i18n-info
|
i18n-info
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
||||||
<i-bs class="me-2" name="airplane"></i-bs><ng-container i18n>Start tour</ng-container>
|
<pngx-bs-icon class="me-2" name="airplane"></pngx-bs-icon><ng-container i18n>Start tour</ng-container>
|
||||||
</button>
|
</button>
|
||||||
@if (canViewSystemStatus) {
|
@if (canViewSystemStatus) {
|
||||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
@if (!systemStatus()) {
|
@if (!systemStatus()) {
|
||||||
<div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
<pngx-bs-icon class="me-2" name="card-checklist"></pngx-bs-icon>
|
||||||
@if (systemStatusHasErrors) {
|
@if (systemStatusHasErrors) {
|
||||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||||
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
|
<pngx-bs-icon name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></pngx-bs-icon>
|
||||||
</span>
|
</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||||
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
|
<pngx-bs-icon name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></pngx-bs-icon>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
@if (permissionsService.isAdmin()) {
|
@if (permissionsService.isAdmin()) {
|
||||||
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||||
<ng-container i18n>Open Django Admin</ng-container>
|
<ng-container i18n>Open Django Admin</ng-container>
|
||||||
<i-bs class="ms-2" name="arrow-up-right"></i-bs>
|
<pngx-bs-icon class="ms-2" name="arrow-up-right"></pngx-bs-icon>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
||||||
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon><ng-container i18n>Reset</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
<div class="col d-flex flex-row align-items-start">
|
<div class="col d-flex flex-row align-items-start">
|
||||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||||
<button class="btn btn-sm btn-link text-muted me-auto p-0 ms-2" title="What's this?" i18n-title type="button" triggers="mouseenter:mouseleave" [ngbPopover]="updatesPopover" [autoClose]="true">
|
<button class="btn btn-sm btn-link text-muted me-auto p-0 ms-2" title="What's this?" i18n-title type="button" triggers="mouseenter:mouseleave" [ngbPopover]="updatesPopover" [autoClose]="true">
|
||||||
<i-bs name="question-circle"></i-bs>
|
<pngx-bs-icon name="question-circle"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
<ng-template #updatesPopover>
|
<ng-template #updatesPopover>
|
||||||
<p i18n>
|
<p i18n>
|
||||||
@@ -263,9 +263,9 @@
|
|||||||
<input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource">
|
<input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource">
|
||||||
<label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label>
|
<label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label>
|
||||||
<input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource">
|
<input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource">
|
||||||
<label class="btn btn-outline-primary" for="suggestionSourceML"><i-bs class="me-1" name="cpu"></i-bs><ng-container i18n>ML only</ng-container></label>
|
<label class="btn btn-outline-primary" for="suggestionSourceML"><pngx-bs-icon class="me-1" name="cpu"></pngx-bs-icon><ng-container i18n>ML only</ng-container></label>
|
||||||
<input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource">
|
<input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource">
|
||||||
<label class="btn btn-outline-primary" for="suggestionSourceAI"><i-bs class="me-1" name="stars"></i-bs><ng-container i18n>AI only</ng-container></label>
|
<label class="btn btn-outline-primary" for="suggestionSourceAI"><pngx-bs-icon class="me-1" name="stars"></pngx-bs-icon><ng-container i18n>AI only</ng-container></label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
NgbNavLink,
|
NgbNavLink,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
@@ -133,7 +132,6 @@ describe('SettingsComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import {
|
import {
|
||||||
SystemStatus,
|
SystemStatus,
|
||||||
@@ -135,7 +135,7 @@ const sidebarItemLabels: Record<HideableSidebarItemID, string> = {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsComponent
|
export class SettingsComponent
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
>
|
>
|
||||||
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||||
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
|
<pngx-bs-icon name="x" class="me-1"></pngx-bs-icon><ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
|
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
|
||||||
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}}
|
<pngx-bs-icon name="check2-all" class="me-1"></pngx-bs-icon>{{dismissButtonText}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissAllTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="totalTasks() === 0">
|
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissAllTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="totalTasks() === 0">
|
||||||
<i-bs name="check2-all" class="me-1"></i-bs><ng-container i18n>Dismiss all</ng-container>
|
<pngx-bs-icon name="check2-all" class="me-1"></pngx-bs-icon><ng-container i18n>Dismiss all</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<div class="form-check form-switch mb-0 ms-2">
|
<div class="form-check form-switch mb-0 ms-2">
|
||||||
<input id="autoRefreshSwitch" class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
<input id="autoRefreshSwitch" class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (filterText?.length) {
|
@if (filterText?.length) {
|
||||||
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()" aria-label="Clear search" i18n-aria-label>
|
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()" aria-label="Clear search" i18n-aria-label>
|
||||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<label class="visually-hidden" for="taskSearch" i18n>Search tasks</label>
|
<label class="visually-hidden" for="taskSearch" i18n>Search tasks</label>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
|
|
||||||
@if (isFiltered) {
|
@if (isFiltered) {
|
||||||
<button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()">
|
<button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()">
|
||||||
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
|
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon><small i18n>Reset filters</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
@if (hasMore) {
|
@if (hasMore) {
|
||||||
<ng-container i18n>{{tasks.length}} of {{count}} tasks</ng-container>
|
<ng-container i18n>{{tasks.length}} of {{count}} tasks</ng-container>
|
||||||
<button class="btn btn-link show-all p-0 ms-2 align-baseline" type="button" (click)="setSection(section)">
|
<button class="btn btn-link show-all p-0 ms-2 align-baseline" type="button" (click)="setSection(section)">
|
||||||
<ng-container i18n>Show all</ng-container><i-bs class="ms-1" width="1em" height="1em" name="arrow-right"></i-bs>
|
<ng-container i18n>Show all</ng-container><pngx-bs-icon class="ms-1" width="1em" height="1em" name="arrow-right"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<ng-container i18n>{count, plural, =1 {1 task} other {{{count}} tasks}}</ng-container>
|
<ng-container i18n>{count, plural, =1 {1 task} other {{{count}} tasks}}</ng-container>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
@if (taskShowsSeparateTypeLabel(task)) {
|
@if (taskShowsSeparateTypeLabel(task)) {
|
||||||
<span class="me-2">{{ task.task_type_display }}</span>
|
<span class="me-2">{{ task.task_type_display }}</span>
|
||||||
}
|
}
|
||||||
<span class="text-nowrap"><i-bs class="me-1" width="1em" height="1em" name="box-arrow-in-right"></i-bs>{{ task.trigger_source_display }}</span>
|
<span class="text-nowrap"><pngx-bs-icon class="me-1" width="1em" height="1em" name="box-arrow-in-right"></pngx-bs-icon>{{ task.trigger_source_display }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td>
|
<td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td>
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
}
|
}
|
||||||
@if (duplicateDocumentId(task)) {
|
@if (duplicateDocumentId(task)) {
|
||||||
<div class="small text-warning-emphasis d-flex align-items-center gap-1 mt-1">
|
<div class="small text-warning-emphasis d-flex align-items-center gap-1 mt-1">
|
||||||
<i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs>
|
<pngx-bs-icon class="lh-1" width="1em" height="1em" name="exclamation-triangle"></pngx-bs-icon>
|
||||||
<span>{{ duplicateTaskLabel(task) }}</span>
|
<span>{{ duplicateTaskLabel(task) }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -213,18 +213,18 @@
|
|||||||
}
|
}
|
||||||
<td class="d-lg-none">
|
<td class="d-lg-none">
|
||||||
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();" aria-label="View task details" i18n-aria-label>
|
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();" aria-label="View task details" i18n-aria-label>
|
||||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
<pngx-bs-icon width="1.2em" height="1.2em" name="info-circle"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions-col">
|
<td class="actions-col">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
||||||
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
|
<pngx-bs-icon name="check" class="me-1"></pngx-bs-icon><ng-container i18n>Dismiss</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
@if (task.related_document_ids?.[0]) {
|
@if (task.related_document_ids?.[0]) {
|
||||||
<a class="btn btn-sm btn-outline-primary" [routerLink]="['/documents', task.related_document_ids[0]]" (click)="dismissTask(task)">
|
<a class="btn btn-sm btn-outline-primary" [routerLink]="['/documents', task.related_document_ids[0]]" (click)="dismissTask(task)">
|
||||||
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
|
<pngx-bs-icon name="file-text" class="me-1"></pngx-bs-icon><ng-container i18n>Open Document</ng-container>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import {
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { Router } from '@angular/router'
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
convertToParamMap,
|
||||||
|
ParamMap,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { BehaviorSubject, of, throwError } from 'rxjs'
|
||||||
import { of, throwError } from 'rxjs'
|
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import {
|
import {
|
||||||
PaperlessTask,
|
PaperlessTask,
|
||||||
@@ -160,6 +164,7 @@ const sectionCountResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('TasksComponent', () => {
|
describe('TasksComponent', () => {
|
||||||
|
let queryParams: BehaviorSubject<ParamMap>
|
||||||
let component: TasksComponent
|
let component: TasksComponent
|
||||||
let fixture: ComponentFixture<TasksComponent>
|
let fixture: ComponentFixture<TasksComponent>
|
||||||
let tasksService: TasksService
|
let tasksService: TasksService
|
||||||
@@ -170,11 +175,11 @@ describe('TasksComponent', () => {
|
|||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
queryParams = new BehaviorSubject(convertToParamMap({}))
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
NgbModule,
|
NgbModule,
|
||||||
RouterTestingModule.withRoutes(routes),
|
RouterTestingModule.withRoutes(routes),
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
TasksComponent,
|
TasksComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
@@ -183,6 +188,17 @@ describe('TasksComponent', () => {
|
|||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
queryParamMap: queryParams.asObservable(),
|
||||||
|
snapshot: {
|
||||||
|
get queryParamMap() {
|
||||||
|
return queryParams.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: PermissionsService,
|
provide: PermissionsService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@@ -221,7 +237,11 @@ describe('TasksComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// answers task list requests with the fixture tasks matching their status filter
|
// answers task list requests with the fixture tasks matching their status filter
|
||||||
function flushTaskLists(pageSize: number, count?: number) {
|
function flushTaskLists(
|
||||||
|
pageSize: number,
|
||||||
|
count?: number,
|
||||||
|
filteredTasks: PaperlessTask[] = tasks
|
||||||
|
) {
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.match(
|
.match(
|
||||||
(req) =>
|
(req) =>
|
||||||
@@ -230,7 +250,9 @@ describe('TasksComponent', () => {
|
|||||||
)
|
)
|
||||||
.forEach((req) => {
|
.forEach((req) => {
|
||||||
const statuses = req.request.params.getAll('status')
|
const statuses = req.request.params.getAll('status')
|
||||||
const results = tasks.filter((task) => statuses.includes(task.status))
|
const results = filteredTasks.filter((task) =>
|
||||||
|
statuses.includes(task.status)
|
||||||
|
)
|
||||||
req.flush({ count: count ?? results.length, results })
|
req.flush({ count: count ?? results.length, results })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -240,6 +262,71 @@ describe('TasksComponent', () => {
|
|||||||
flushTaskLists(25, count)
|
flushTaskLists(25, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('consumes a task link on load into the normal resettable Task ID filter', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(tasksService, 'list')
|
||||||
|
.mockReturnValue(of({ count: 0, results: [] }))
|
||||||
|
jest
|
||||||
|
.spyOn(tasksService, 'statusCounts')
|
||||||
|
.mockReturnValue(of(sectionCountResponse))
|
||||||
|
jest.spyOn(router, 'navigate').mockResolvedValue(true)
|
||||||
|
queryParams.next(convertToParamMap({ task_id: 'linked-task' }))
|
||||||
|
fixture.destroy()
|
||||||
|
fixture = TestBed.createComponent(TasksComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(router.navigate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(
|
||||||
|
[],
|
||||||
|
expect.objectContaining({
|
||||||
|
queryParams: { task_id: null },
|
||||||
|
replaceUrl: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(component.filterTargetName).toBe('Task ID')
|
||||||
|
expect(component.filterText).toBe('linked-task')
|
||||||
|
expect(component.isFiltered).toBe(true)
|
||||||
|
component.setSection(TaskSection.InProgress)
|
||||||
|
expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, {
|
||||||
|
task_id: 'linked-task',
|
||||||
|
status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started],
|
||||||
|
})
|
||||||
|
component.resetFilters()
|
||||||
|
expect(component.filterText).toBe('')
|
||||||
|
expect(component.isFiltered).toBe(false)
|
||||||
|
expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, {
|
||||||
|
acknowledged: false,
|
||||||
|
status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started],
|
||||||
|
})
|
||||||
|
expect(router.navigate).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports manually filtering by Task ID and clearing the text filter', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(tasksService, 'list')
|
||||||
|
.mockReturnValue(of({ count: 0, results: [] }))
|
||||||
|
jest
|
||||||
|
.spyOn(tasksService, 'statusCounts')
|
||||||
|
.mockReturnValue(of(sectionCountResponse))
|
||||||
|
component.setSection(TaskSection.InProgress)
|
||||||
|
component.setFilterTarget(TaskFilterTargetID.TaskId)
|
||||||
|
component.filterText = tasks[0].task_id
|
||||||
|
jest.advanceTimersByTime(150)
|
||||||
|
expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, {
|
||||||
|
task_id: tasks[0].task_id,
|
||||||
|
status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started],
|
||||||
|
})
|
||||||
|
component.pagedTasks.set(tasks)
|
||||||
|
expect(component.tasksForSection(TaskSection.NeedsAttention)).toEqual([
|
||||||
|
tasks[0],
|
||||||
|
])
|
||||||
|
component.resetFilter()
|
||||||
|
expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, {
|
||||||
|
acknowledged: false,
|
||||||
|
status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should display task sections with counts', () => {
|
it('should display task sections with counts', () => {
|
||||||
expect(component.selectedSection).toBe(TaskSection.All)
|
expect(component.selectedSection).toBe(TaskSection.All)
|
||||||
expect(component.selectedTaskType).toBeNull()
|
expect(component.selectedTaskType).toBeNull()
|
||||||
@@ -773,6 +860,7 @@ describe('TasksComponent', () => {
|
|||||||
input.nativeElement.value = '191092'
|
input.nativeElement.value = '191092'
|
||||||
input.nativeElement.dispatchEvent(new Event('input'))
|
input.nativeElement.dispatchEvent(new Event('input'))
|
||||||
jest.advanceTimersByTime(150)
|
jest.advanceTimersByTime(150)
|
||||||
|
flushTaskLists(10, undefined, [tasks[1]])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterText).toEqual('191092')
|
expect(component.filterText).toEqual('191092')
|
||||||
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
|
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
|
||||||
@@ -784,6 +872,7 @@ describe('TasksComponent', () => {
|
|||||||
component.setSection(TaskSection.InProgress)
|
component.setSection(TaskSection.InProgress)
|
||||||
component.filterText = 'system'
|
component.filterText = 'system'
|
||||||
jest.advanceTimersByTime(150)
|
jest.advanceTimersByTime(150)
|
||||||
|
flushTaskLists(25, undefined, [tasks[6]])
|
||||||
|
|
||||||
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
|
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
|
||||||
expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
|
expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
|
||||||
@@ -817,6 +906,7 @@ describe('TasksComponent', () => {
|
|||||||
input.nativeElement.value = 'duplicate'
|
input.nativeElement.value = 'duplicate'
|
||||||
input.nativeElement.dispatchEvent(new Event('input'))
|
input.nativeElement.dispatchEvent(new Event('input'))
|
||||||
jest.advanceTimersByTime(150)
|
jest.advanceTimersByTime(150)
|
||||||
|
flushTaskLists(25, undefined, [tasks[0], tasks[1]])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterText).toEqual('duplicate')
|
expect(component.filterText).toEqual('duplicate')
|
||||||
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
|
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
|
||||||
@@ -824,6 +914,53 @@ describe('TasksComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show server-matched names when the displayed source is translated', () => {
|
||||||
|
component.setSection(TaskSection.InProgress)
|
||||||
|
flushTaskLists(25)
|
||||||
|
component.filterText = 'email'
|
||||||
|
jest.advanceTimersByTime(150)
|
||||||
|
|
||||||
|
const request = httpTestingController.expectOne(
|
||||||
|
(req) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/` &&
|
||||||
|
req.params.get('name') === 'email' &&
|
||||||
|
req.params.get('page_size') === '25'
|
||||||
|
)
|
||||||
|
const translatedTask = {
|
||||||
|
...tasks[5],
|
||||||
|
trigger_source_display: 'E-Mail',
|
||||||
|
}
|
||||||
|
request.flush({ count: 1, results: [translatedTask] })
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.totalTasks()).toBe(1)
|
||||||
|
expect(component.visibleTasks).toEqual([translatedTask])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show server-matched duplicates when the displayed result is translated', () => {
|
||||||
|
component.setSection(TaskSection.NeedsAttention)
|
||||||
|
flushTaskLists(25)
|
||||||
|
jest
|
||||||
|
.spyOn(component, 'taskResultMessage')
|
||||||
|
.mockReturnValue('Duplikat von Document #311')
|
||||||
|
component.setFilterTarget(TaskFilterTargetID.Result)
|
||||||
|
component.filterText = 'duplicate'
|
||||||
|
jest.advanceTimersByTime(150)
|
||||||
|
|
||||||
|
const request = httpTestingController.expectOne(
|
||||||
|
(req) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/` &&
|
||||||
|
req.params.get('result') === 'duplicate' &&
|
||||||
|
req.params.get('page_size') === '25'
|
||||||
|
)
|
||||||
|
request.flush({ count: 1, results: [tasks[1]] })
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.totalTasks()).toBe(1)
|
||||||
|
expect(component.visibleTasks).toEqual([tasks[1]])
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('191092.pdf')
|
||||||
|
})
|
||||||
|
|
||||||
it('should prefer explicit reason in the result message', () => {
|
it('should prefer explicit reason in the result message', () => {
|
||||||
expect(
|
expect(
|
||||||
component.taskResultMessage({
|
component.taskResultMessage({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { JsonPipe, NgTemplateOutlet } from '@angular/common'
|
import { JsonPipe, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'
|
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { Router, RouterLink } from '@angular/router'
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import {
|
import {
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
timer,
|
timer,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import {
|
import {
|
||||||
PaperlessTask,
|
PaperlessTask,
|
||||||
PaperlessTaskStatus,
|
PaperlessTaskStatus,
|
||||||
@@ -47,11 +47,13 @@ export enum TaskSection {
|
|||||||
export enum TaskFilterTargetID {
|
export enum TaskFilterTargetID {
|
||||||
Name,
|
Name,
|
||||||
Result,
|
Result,
|
||||||
|
TaskId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILTER_TARGETS = [
|
const FILTER_TARGETS = [
|
||||||
{ id: TaskFilterTargetID.Name, name: $localize`Name` },
|
{ id: TaskFilterTargetID.Name, name: $localize`Name` },
|
||||||
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
|
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
|
||||||
|
{ id: TaskFilterTargetID.TaskId, name: $localize`Task ID` },
|
||||||
]
|
]
|
||||||
|
|
||||||
const SECTION_LABELS = {
|
const SECTION_LABELS = {
|
||||||
@@ -151,7 +153,7 @@ const TRIGGER_SOURCE_OPTIONS: Array<{
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TasksComponent
|
export class TasksComponent
|
||||||
@@ -161,6 +163,7 @@ export class TasksComponent
|
|||||||
tasksService = inject(TasksService)
|
tasksService = inject(TasksService)
|
||||||
private modalService = inject(NgbModal)
|
private modalService = inject(NgbModal)
|
||||||
private readonly router = inject(Router)
|
private readonly router = inject(Router)
|
||||||
|
private readonly route = inject(ActivatedRoute)
|
||||||
private readonly toastService = inject(ToastService)
|
private readonly toastService = inject(ToastService)
|
||||||
|
|
||||||
readonly TaskSection = TaskSection
|
readonly TaskSection = TaskSection
|
||||||
@@ -275,6 +278,17 @@ export class TasksComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
const taskId = this.route.snapshot.queryParamMap.get('task_id')
|
||||||
|
if (taskId) {
|
||||||
|
this.filterTargetID = TaskFilterTargetID.TaskId
|
||||||
|
this._filterText = taskId
|
||||||
|
this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: { task_id: null },
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
replaceUrl: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
this.tasksService.reload()
|
this.tasksService.reload()
|
||||||
this.reloadPage()
|
this.reloadPage()
|
||||||
timer(5000, 5000)
|
timer(5000, 5000)
|
||||||
@@ -636,17 +650,12 @@ export class TasksComponent
|
|||||||
|
|
||||||
const query = this._filterText.toLowerCase()
|
const query = this._filterText.toLowerCase()
|
||||||
|
|
||||||
if (this.filterTargetID == TaskFilterTargetID.Name) {
|
if (this.filterTargetID === TaskFilterTargetID.TaskId) {
|
||||||
return [
|
return task.task_id.toLowerCase() === query
|
||||||
this.taskDisplayName(task),
|
|
||||||
task.task_type_display,
|
|
||||||
task.trigger_source_display,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.some((value) => value.toLowerCase().includes(query))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.taskResultMessage(task)?.toLowerCase().includes(query) ?? false
|
// Text filtering now handled API-side
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private tasksForOptionCounts({
|
private tasksForOptionCounts({
|
||||||
@@ -706,9 +715,15 @@ export class TasksComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._filterText.length) {
|
if (this._filterText.length) {
|
||||||
params[
|
const parameter = {
|
||||||
this.filterTargetID === TaskFilterTargetID.Name ? 'name' : 'result'
|
[TaskFilterTargetID.Name]: 'name',
|
||||||
] = this._filterText
|
[TaskFilterTargetID.Result]: 'result',
|
||||||
|
[TaskFilterTargetID.TaskId]: 'task_id',
|
||||||
|
}[this.filterTargetID]
|
||||||
|
params[parameter] = this._filterText
|
||||||
|
if (this.filterTargetID === TaskFilterTargetID.TaskId) {
|
||||||
|
delete params.acknowledged
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
i18n-info
|
i18n-info
|
||||||
infoLink="usage/#document-trash">
|
infoLink="usage/#document-trash">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments().size === 0">
|
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments().size === 0">
|
||||||
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
|
<pngx-bs-icon name="x" class="me-1"></pngx-bs-icon><ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
|
||||||
<i-bs name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore selected</ng-container>
|
<pngx-bs-icon name="arrow-counterclockwise" class="me-1"></pngx-bs-icon><ng-container i18n>Restore selected</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
|
||||||
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Delete selected</ng-container>
|
<pngx-bs-icon name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete selected</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash().length === 0">
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash().length === 0">
|
||||||
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Empty trash</ng-container>
|
<pngx-bs-icon name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Empty trash</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<td scope="row">
|
<td scope="row">
|
||||||
{{ document.title }}
|
{{ document.title }}
|
||||||
<pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
|
<pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
|
||||||
<i-bs name="eye"></i-bs>
|
<pngx-bs-icon name="eye"></pngx-bs-icon>
|
||||||
</pngx-preview-popup>
|
</pngx-preview-popup>
|
||||||
</td>
|
</td>
|
||||||
<td scope="row" class="d-none d-sm-table-cell" i18n>{{ getDaysRemaining(document) }} days</td>
|
<td scope="row" class="d-none d-sm-table-cell" i18n>{{ getDaysRemaining(document) }} days</td>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<div class="btn-group d-block d-sm-none">
|
<div class="btn-group d-block d-sm-none">
|
||||||
<div ngbDropdown container="body" class="d-inline-block">
|
<div ngbDropdown container="body" class="d-inline-block">
|
||||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle aria-label="Actions" i18n-aria-label>
|
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle aria-label="Actions" i18n-aria-label>
|
||||||
<i-bs name="three-dots-vertical"></i-bs>
|
<pngx-bs-icon name="three-dots-vertical"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||||
<button (click)="restore(document)" ngbDropdownItem i18n>Restore</button>
|
<button (click)="restore(document)" ngbDropdownItem i18n>Restore</button>
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="btn-group d-none d-sm-block">
|
<div class="btn-group d-none d-sm-block">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();">
|
<button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();">
|
||||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></pngx-bs-icon><ng-container i18n>Restore</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();">
|
<button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();">
|
||||||
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { TrashService } from 'src/app/services/trash.service'
|
import { TrashService } from 'src/app/services/trash.service'
|
||||||
@@ -48,7 +47,6 @@ describe('TrashComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
TrashComponent,
|
TrashComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
NgbModal,
|
NgbModal,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { takeUntil, tap } from 'rxjs'
|
import { takeUntil, tap } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
@@ -29,7 +29,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TrashComponent
|
export class TrashComponent
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<h4 class="d-flex">
|
<h4 class="d-flex">
|
||||||
<ng-container i18n>Users</ng-container>
|
<ng-container i18n>Users</ng-container>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
|
||||||
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add User</ng-container>
|
<pngx-bs-icon name="plus-circle" class="me-1"></pngx-bs-icon><ng-container i18n>Add User</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
||||||
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><ng-container i18n>Edit</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
||||||
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<h4 class="mt-4 d-flex">
|
<h4 class="mt-4 d-flex">
|
||||||
<ng-container i18n>Groups</ng-container>
|
<ng-container i18n>Groups</ng-container>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
||||||
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add Group</ng-container>
|
<pngx-bs-icon name="plus-circle" class="me-1"></pngx-bs-icon><ng-container i18n>Add Group</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@@ -70,10 +70,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||||
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><ng-container i18n>Edit</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||||
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
@@ -41,7 +40,6 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
|
||||||
providers: [
|
providers: [
|
||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subject, first, takeUntil } from 'rxjs'
|
import { Subject, first, takeUntil } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@@ -29,7 +29,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
|
|||||||
imports: [
|
imports: [
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UsersAndGroupsComponent
|
export class UsersAndGroupsComponent
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
(click)="closeMobileSearch(); toggleMenuCollapsed()">
|
(click)="closeMobileSearch(); toggleMenuCollapsed()">
|
||||||
<i-bs width="1.5em" height="1.5em" name="list"></i-bs>
|
<pngx-bs-icon width="1.5em" height="1.5em" name="list"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
<div class="navbar-brand-container order-sm-0">
|
<div class="navbar-brand-container order-sm-0">
|
||||||
<a class="navbar-brand d-flex align-items-center me-0 ps-md-3 py-0"
|
<a class="navbar-brand d-flex align-items-center me-0 ps-md-3 py-0"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<div *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }" class="btn-group header-upload-group flex-shrink-0">
|
<div *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }" class="btn-group header-upload-group flex-shrink-0">
|
||||||
<button class="btn btn-sm header-upload border-0 d-flex align-items-center justify-content-center py-0 px-2" type="button"
|
<button class="btn btn-sm header-upload border-0 d-flex align-items-center justify-content-center py-0 px-2" type="button"
|
||||||
(click)="fileUpload.click()" aria-label="Upload documents" i18n-aria-label>
|
(click)="fileUpload.click()" aria-label="Upload documents" i18n-aria-label>
|
||||||
<i-bs class="d-flex" width="1.1em" height="1.1em" name="file-earmark-arrow-up"></i-bs><span class="ms-2 d-sm-none d-md-inline" i18n>Upload</span>
|
<pngx-bs-icon class="d-flex" width="1.1em" height="1.1em" name="file-earmark-arrow-up"></pngx-bs-icon><span class="ms-2 d-sm-none d-md-inline" i18n>Upload</span>
|
||||||
</button>
|
</button>
|
||||||
@if (uploadsInProgress > 0) {
|
@if (uploadsInProgress > 0) {
|
||||||
<button class="btn btn-sm header-upload border-0 d-flex align-items-center py-0 px-2" type="button"
|
<button class="btn btn-sm header-upload border-0 d-flex align-items-center py-0 px-2" type="button"
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
||||||
<li ngbDropdown class="nav-item dropdown">
|
<li ngbDropdown class="nav-item dropdown">
|
||||||
<button class="btn navbar-action border-0 me-1 me-md-2" id="userDropdown" ngbDropdownToggle aria-label="User menu" i18n-aria-label>
|
<button class="btn navbar-action border-0 me-1 me-md-2" id="userDropdown" ngbDropdownToggle aria-label="User menu" i18n-aria-label>
|
||||||
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
<pngx-bs-icon width="1.3em" height="1.3em" name="person-circle"></pngx-bs-icon>
|
||||||
<span class="small ms-2 d-none d-sm-inline">
|
<span class="small ms-2 d-none d-sm-inline">
|
||||||
{{this.settingsService.displayName}}
|
{{this.settingsService.displayName}}
|
||||||
</span>
|
</span>
|
||||||
@@ -63,19 +63,19 @@
|
|||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
</div>
|
</div>
|
||||||
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
||||||
<i-bs class="me-2" name="person"></i-bs><ng-container i18n>My Profile</ng-container>
|
<pngx-bs-icon class="me-2" name="person"></pngx-bs-icon><ng-container i18n>My Profile</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
|
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
|
||||||
*pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }">
|
*pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }">
|
||||||
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
|
<pngx-bs-icon class="me-2" name="gear"></pngx-bs-icon><ng-container i18n>Settings</ng-container>
|
||||||
</a>
|
</a>
|
||||||
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
|
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
|
||||||
<i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
|
<pngx-bs-icon class="me-2" name="door-open"></pngx-bs-icon><ng-container i18n>Logout</ng-container>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
|
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://docs.paperless-ngx.com">
|
href="https://docs.paperless-ngx.com">
|
||||||
<i-bs class="me-2" name="question-circle"></i-bs><ng-container i18n>Documentation</ng-container>
|
<pngx-bs-icon class="me-2" name="question-circle"></pngx-bs-icon><ng-container i18n>Documentation</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
@if (canSaveSettings) {
|
@if (canSaveSettings) {
|
||||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()" [aria-label]="slimSidebarEnabled ? 'Expand sidebar' : 'Collapse sidebar'" i18n-aria-label>
|
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()" [aria-label]="slimSidebarEnabled ? 'Expand sidebar' : 'Collapse sidebar'" i18n-aria-label>
|
||||||
@if (slimSidebarEnabled) {
|
@if (slimSidebarEnabled) {
|
||||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
<pngx-bs-icon width="0.9em" height="0.9em" name="chevron-double-right"></pngx-bs-icon>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
<pngx-bs-icon width="0.9em" height="0.9em" name="chevron-double-left"></pngx-bs-icon>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="house"></i-bs><span class="nav-link-label"><ng-container i18n>Dashboard</ng-container></span>
|
<pngx-bs-icon class="me-2" name="house"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Dashboard</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarItems()) {
|
@if (settingsService.organizingSidebarItems()) {
|
||||||
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Dashboard" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Dashboard, $event)"></pngx-input-switch>
|
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Dashboard" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Dashboard, $event)"></pngx-input-switch>
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
(click)="closeMenu()"
|
(click)="closeMenu()"
|
||||||
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="files"></i-bs><span class="nav-link-label"><ng-container i18n>Documents</ng-container></span>
|
<pngx-bs-icon class="me-2" name="files"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Documents</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
||||||
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" [name]="view.icon || 'funnel'"></i-bs><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
|
<pngx-bs-icon class="me-2" [name]="view.icon || 'funnel'"></pngx-bs-icon><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
|
||||||
@if (showSidebarCounts && !slimSidebarEnabled) {
|
@if (showSidebarCounts && !slimSidebarEnabled) {
|
||||||
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
||||||
<div class="position-absolute end-0 top-0 px-1 py-2" [class.me-n2]="slimSidebarEnabled" cdkDragHandle>
|
<div class="position-absolute end-0 top-0 px-1 py-2" [class.me-n2]="slimSidebarEnabled" cdkDragHandle>
|
||||||
<i-bs name="grip-vertical"></i-bs>
|
<pngx-bs-icon name="grip-vertical"></pngx-bs-icon>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
@@ -174,10 +174,10 @@
|
|||||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
||||||
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="file-text"></i-bs><span>{{d.title | documentTitle}}</span>
|
<pngx-bs-icon class="me-2" name="file-text"></pngx-bs-icon><span>{{d.title | documentTitle}}</span>
|
||||||
<span class="close flex-column justify-content-center"
|
<span class="close flex-column justify-content-center"
|
||||||
(click)="$event.preventDefault(); $event.stopPropagation(); closeDocument(d)">
|
(click)="$event.preventDefault(); $event.stopPropagation(); closeDocument(d)">
|
||||||
<i-bs name="x"></i-bs>
|
<pngx-bs-icon name="x"></pngx-bs-icon>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
[class.text-truncate]="!slimSidebarEnabled" (click)="closeAll()"
|
[class.text-truncate]="!slimSidebarEnabled" (click)="closeAll()"
|
||||||
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="x"></i-bs><span class="nav-link-label"><ng-container i18n>Close all</ng-container></span>
|
<pngx-bs-icon class="me-2" name="x"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Close all</ng-container></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
[routerLinkActiveOptions]="{ exact: !(slimSidebarEnabled || attributesSectionsCollapsed) }" (click)="closeMenu()"
|
[routerLinkActiveOptions]="{ exact: !(slimSidebarEnabled || attributesSectionsCollapsed) }" (click)="closeMenu()"
|
||||||
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs name="stack"></i-bs><span class="nav-link-label ms-2"><ng-container i18n>Attributes</ng-container></span>
|
<pngx-bs-icon name="stack"></pngx-bs-icon><span class="nav-link-label ms-2"><ng-container i18n>Attributes</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (!slimSidebarEnabled && canSaveSettings) {
|
@if (!slimSidebarEnabled && canSaveSettings) {
|
||||||
<button
|
<button
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
|
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
|
||||||
i18n-aria-label
|
i18n-aria-label
|
||||||
>
|
>
|
||||||
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
|
<pngx-bs-icon [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -228,27 +228,27 @@
|
|||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
|
||||||
<a class="nav-link" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-2" name="tags"></i-bs><span class="nav-link-label"><ng-container i18n>Tags</ng-container></span>
|
<pngx-bs-icon class="me-2" name="tags"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Tags</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
<a class="nav-link" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-2" name="person"></i-bs><span class="nav-link-label"><ng-container i18n>Correspondents</ng-container></span>
|
<pngx-bs-icon class="me-2" name="person"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Correspondents</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
<a class="nav-link" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-2" name="hash"></i-bs><span class="nav-link-label"><ng-container i18n>Document types</ng-container></span>
|
<pngx-bs-icon class="me-2" name="hash"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Document types</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<a class="nav-link" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-2" name="folder"></i-bs><span class="nav-link-label"><ng-container i18n>Storage paths</ng-container></span>
|
<pngx-bs-icon class="me-2" name="folder"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Storage paths</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<a class="nav-link" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-2" name="ui-radios"></i-bs><span class="nav-link-label"><ng-container i18n>Custom fields</ng-container></span>
|
<pngx-bs-icon class="me-2" name="ui-radios"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Custom fields</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="window-stack"></i-bs><span class="nav-link-label"><ng-container i18n>Saved Views</ng-container></span>
|
<pngx-bs-icon class="me-2" name="window-stack"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Saved Views</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarItems()) {
|
@if (settingsService.organizingSidebarItems()) {
|
||||||
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Saved Views" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.SavedViews, $event)"></pngx-input-switch>
|
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Saved Views" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.SavedViews, $event)"></pngx-input-switch>
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="share-links" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="share-links" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Share links" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Share links" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="link"></i-bs><span class="nav-link-label"><ng-container i18n>Share links</ng-container></span>
|
<pngx-bs-icon class="me-2" name="link"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Share links</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarItems()) {
|
@if (settingsService.organizingSidebarItems()) {
|
||||||
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Share Links" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.ShareLinks, $event)"></pngx-input-switch>
|
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Share Links" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.ShareLinks, $event)"></pngx-input-switch>
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="boxes"></i-bs><span class="nav-link-label"><ng-container i18n>Workflows</ng-container></span>
|
<pngx-bs-icon class="me-2" name="boxes"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Workflows</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarItems()) {
|
@if (settingsService.organizingSidebarItems()) {
|
||||||
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Workflows" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Workflows, $event)"></pngx-input-switch>
|
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Workflows" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Workflows, $event)"></pngx-input-switch>
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="envelope"></i-bs><span class="nav-link-label"><ng-container i18n>Mail</ng-container></span>
|
<pngx-bs-icon class="me-2" name="envelope"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Mail</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarItems()) {
|
@if (settingsService.organizingSidebarItems()) {
|
||||||
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Mail" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Mail, $event)"></pngx-input-switch>
|
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Mail" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Mail, $event)"></pngx-input-switch>
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
|
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="trash"></i-bs><span class="nav-link-label"><ng-container i18n>Trash</ng-container></span>
|
<pngx-bs-icon class="me-2" name="trash"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Trash</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -320,21 +320,21 @@
|
|||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="gear"></i-bs><span class="nav-link-label"><ng-container i18n>Settings</ng-container></span>
|
<pngx-bs-icon class="me-2" name="gear"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Settings</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
||||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="sliders2-vertical"></i-bs><span class="nav-link-label"><ng-container i18n>Configuration</ng-container></span>
|
<pngx-bs-icon class="me-2" name="sliders2-vertical"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Configuration</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="people"></i-bs><span class="nav-link-label"><ng-container i18n>Users & Groups</ng-container></span>
|
<pngx-bs-icon class="me-2" name="people"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Users & Groups</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link"
|
<li class="nav-item app-link"
|
||||||
@@ -343,7 +343,7 @@
|
|||||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="list-task"></i-bs><span class="nav-link-label"><ng-container i18n>Tasks</ng-container></span>
|
<pngx-bs-icon class="me-2" name="list-task"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Tasks</ng-container></span>
|
||||||
@if (tasksService.needsAttentionTasksCount > 0 && !slimSidebarEnabled) {
|
@if (tasksService.needsAttentionTasksCount > 0 && !slimSidebarEnabled) {
|
||||||
<span class="badge bg-danger ms-2 d-inline flex-shrink-0">{{tasksService.needsAttentionTasksCount}}</span>
|
<span class="badge bg-danger ms-2 d-inline flex-shrink-0">{{tasksService.needsAttentionTasksCount}}</span>
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="text-left"></i-bs><span class="nav-link-label"><ng-container i18n>Logs</ng-container></span>
|
<pngx-bs-icon class="me-2" name="text-left"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Logs</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="d-flex me-2" name="question-circle"></i-bs><span><ng-container i18n>Documentation</ng-container></span>
|
<pngx-bs-icon class="d-flex me-2" name="question-circle"></pngx-bs-icon><span><ng-container i18n>Documentation</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarItems()) {
|
@if (settingsService.organizingSidebarItems()) {
|
||||||
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Documentation" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Documentation)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Documentation, $event)"></pngx-input-switch>
|
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Documentation" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Documentation)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Documentation, $event)"></pngx-input-switch>
|
||||||
@@ -408,7 +408,7 @@
|
|||||||
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
||||||
container="body">
|
container="body">
|
||||||
<i-bs width="1.2em" height="1.2em" name="info-circle" class="me-1"></i-bs>
|
<pngx-bs-icon width="1.2em" height="1.2em" name="info-circle" class="me-1"></pngx-bs-icon>
|
||||||
@if (appRemoteVersion()?.update_available) {
|
@if (appRemoteVersion()?.update_available) {
|
||||||
<ng-container i18n>Update available</ng-container>
|
<ng-container i18n>Update available</ng-container>
|
||||||
}
|
}
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
<a *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" class="small text-decoration-none" routerLink="/settings" fragment="update-checking" aria-label="Configure update checking" i18n-aria-label
|
<a *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" class="small text-decoration-none" routerLink="/settings" fragment="update-checking" aria-label="Configure update checking" i18n-aria-label
|
||||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
||||||
container="body">
|
container="body">
|
||||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
<pngx-bs-icon width="1.2em" height="1.2em" name="info-circle"></pngx-bs-icon>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ main {
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
||||||
i-bs {
|
pngx-bs-icon {
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i-bs {
|
pngx-bs-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}
|
}
|
||||||
@@ -279,7 +279,7 @@ main {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
> i-bs {
|
> pngx-bs-icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,11 +373,11 @@ main {
|
|||||||
width: 1.8rem;
|
width: 1.8rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
i-bs {
|
pngx-bs-icon {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover i-bs {
|
&:hover pngx-bs-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +392,7 @@ main {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
i-bs {
|
pngx-bs-icon {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,7 +665,7 @@ main {
|
|||||||
.navbar .dropdown-menu {
|
.navbar .dropdown-menu {
|
||||||
font-size: 0.875rem; // body size
|
font-size: 0.875rem; // body size
|
||||||
|
|
||||||
a i-bs, button i-bs {
|
a pngx-bs-icon, button pngx-bs-icon {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,7 +693,7 @@ main {
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .navItemDrag .position-absolute i-bs {
|
::ng-deep .navItemDrag .position-absolute pngx-bs-icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import { BrowserModule, By } from '@angular/platform-browser'
|
|||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, Subject, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
|
import { BulkOperationStatus } from 'src/app/data/bulk-operation'
|
||||||
|
import { PaperlessTaskStatus } from 'src/app/data/paperless-task'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { HideableSidebarItemID, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { HideableSidebarItemID, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@@ -22,10 +23,11 @@ import {
|
|||||||
DjangoMessageLevel,
|
DjangoMessageLevel,
|
||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import {
|
import {
|
||||||
PermissionType,
|
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
@@ -96,6 +98,7 @@ describe('AppFrameComponent', () => {
|
|||||||
let savedViewSpy
|
let savedViewSpy
|
||||||
let modalService: NgbModal
|
let modalService: NgbModal
|
||||||
let maybeRefreshSpy
|
let maybeRefreshSpy
|
||||||
|
let bulkEvents: Subject<BulkOperationStatus>
|
||||||
let tasksService: TasksService
|
let tasksService: TasksService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -108,7 +111,6 @@ describe('AppFrameComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
AppFrameComponent,
|
AppFrameComponent,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
GlobalSearchComponent,
|
GlobalSearchComponent,
|
||||||
@@ -184,6 +186,10 @@ describe('AppFrameComponent', () => {
|
|||||||
savedViewSpy = jest.spyOn(savedViewService, 'reload')
|
savedViewSpy = jest.spyOn(savedViewService, 'reload')
|
||||||
maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts')
|
maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts')
|
||||||
|
|
||||||
|
bulkEvents = new Subject<BulkOperationStatus>()
|
||||||
|
jest
|
||||||
|
.spyOn(TestBed.inject(WebsocketStatusService), 'onBulkOperationStatus')
|
||||||
|
.mockReturnValue(bulkEvents.asObservable())
|
||||||
fixture = TestBed.createComponent(AppFrameComponent)
|
fixture = TestBed.createComponent(AppFrameComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
|
|
||||||
@@ -192,6 +198,78 @@ describe('AppFrameComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('refreshes and notifies on completion without client-side task tracking', () => {
|
||||||
|
const list = TestBed.inject(DocumentListViewService)
|
||||||
|
const reload = jest.spyOn(list, 'reload').mockImplementation(() => {})
|
||||||
|
jest.spyOn(list, 'reduceSelectionToFilter').mockImplementation(() => {})
|
||||||
|
const tasksReload = jest
|
||||||
|
.spyOn(tasksService, 'reload')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
const success = jest.spyOn(toastService, 'showSuccess')
|
||||||
|
const refresh = jest
|
||||||
|
.spyOn(openDocumentsService, 'refreshDocument')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
jest
|
||||||
|
.spyOn(openDocumentsService, 'getOpenDocuments')
|
||||||
|
.mockReturnValue([document])
|
||||||
|
tasksReload.mockClear()
|
||||||
|
maybeRefreshSpy.mockClear()
|
||||||
|
bulkEvents.next({
|
||||||
|
task_id: 'bulk-task',
|
||||||
|
operation: 'set_permissions',
|
||||||
|
status: PaperlessTaskStatus.Started,
|
||||||
|
})
|
||||||
|
expect(reload).not.toHaveBeenCalled()
|
||||||
|
bulkEvents.next({
|
||||||
|
task_id: 'bulk-task',
|
||||||
|
operation: 'set_permissions',
|
||||||
|
status: PaperlessTaskStatus.Success,
|
||||||
|
})
|
||||||
|
expect(reload).toHaveBeenCalledTimes(1)
|
||||||
|
expect(reload).toHaveBeenCalledWith(null, false)
|
||||||
|
expect(refresh).toHaveBeenCalledWith(document.id)
|
||||||
|
expect(maybeRefreshSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(tasksReload).toHaveBeenCalledTimes(1)
|
||||||
|
expect(success).toHaveBeenCalledWith('Bulk operation completed.')
|
||||||
|
fixture.destroy()
|
||||||
|
bulkEvents.next({
|
||||||
|
task_id: 'bulk-task',
|
||||||
|
operation: 'set_permissions',
|
||||||
|
status: PaperlessTaskStatus.Success,
|
||||||
|
})
|
||||||
|
expect(reload).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refreshes and reports failed or revoked bulk operations', () => {
|
||||||
|
const list = TestBed.inject(DocumentListViewService)
|
||||||
|
const reload = jest.spyOn(list, 'reload').mockImplementation(() => {})
|
||||||
|
jest.spyOn(list, 'reduceSelectionToFilter').mockImplementation(() => {})
|
||||||
|
jest.spyOn(tasksService, 'reload').mockImplementation(() => {})
|
||||||
|
const error = jest.spyOn(toastService, 'showError')
|
||||||
|
const success = jest.spyOn(toastService, 'showSuccess')
|
||||||
|
for (const status of [
|
||||||
|
PaperlessTaskStatus.Failure,
|
||||||
|
PaperlessTaskStatus.Revoked,
|
||||||
|
]) {
|
||||||
|
bulkEvents.next({
|
||||||
|
task_id: 'bulk-task',
|
||||||
|
operation: 'set_permissions',
|
||||||
|
status,
|
||||||
|
error:
|
||||||
|
status === PaperlessTaskStatus.Revoked
|
||||||
|
? 'Task expired before execution.'
|
||||||
|
: 'Could not update',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
expect(reload).toHaveBeenCalledTimes(2)
|
||||||
|
expect(error).toHaveBeenCalledWith(expect.any(String), 'Could not update')
|
||||||
|
expect(error).toHaveBeenCalledWith(
|
||||||
|
'Bulk operation revoked.',
|
||||||
|
'Task expired before execution.'
|
||||||
|
)
|
||||||
|
expect(success).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('should initialize the saved view service', () => {
|
it('should initialize the saved view service', () => {
|
||||||
expect(savedViewSpy).toHaveBeenCalled()
|
expect(savedViewSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import {
|
|||||||
import { NgClass } from '@angular/common'
|
import { NgClass } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
HostListener,
|
HostListener,
|
||||||
inject,
|
inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
signal,
|
signal,
|
||||||
viewChild,
|
viewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
@@ -23,11 +25,12 @@ import {
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Observable } from 'rxjs'
|
import { filter, Observable } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
|
import { PaperlessTaskStatus } from 'src/app/data/paperless-task'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import {
|
import {
|
||||||
CollapsibleSection,
|
CollapsibleSection,
|
||||||
@@ -41,6 +44,7 @@ import {
|
|||||||
DjangoMessageLevel,
|
DjangoMessageLevel,
|
||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
@@ -89,7 +93,7 @@ const SCROLL_THRESHOLD = 16
|
|||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
TourNgBootstrap,
|
TourNgBootstrap,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -101,6 +105,8 @@ export class AppFrameComponent
|
|||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, ComponentCanDeactivate
|
implements OnInit, ComponentCanDeactivate
|
||||||
{
|
{
|
||||||
|
private destroyRef = inject(DestroyRef)
|
||||||
|
private documentList = inject(DocumentListViewService)
|
||||||
router = inject(Router)
|
router = inject(Router)
|
||||||
private activatedRoute = inject(ActivatedRoute)
|
private activatedRoute = inject(ActivatedRoute)
|
||||||
private openDocumentsService = inject(OpenDocumentsService)
|
private openDocumentsService = inject(OpenDocumentsService)
|
||||||
@@ -164,6 +170,46 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.websocketStatusService
|
||||||
|
.onBulkOperationStatus()
|
||||||
|
.pipe(
|
||||||
|
filter(({ status }) =>
|
||||||
|
[
|
||||||
|
PaperlessTaskStatus.Success,
|
||||||
|
PaperlessTaskStatus.Failure,
|
||||||
|
PaperlessTaskStatus.Revoked,
|
||||||
|
].includes(status)
|
||||||
|
),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
.subscribe(({ status, error }) => {
|
||||||
|
this.documentList.reload(null, false)
|
||||||
|
this.documentList.reduceSelectionToFilter()
|
||||||
|
// maybe someday we can optimize to only refresh affected documents
|
||||||
|
this.openDocumentsService.getOpenDocuments().forEach((doc) => {
|
||||||
|
this.openDocumentsService.refreshDocument(doc.id)
|
||||||
|
})
|
||||||
|
this.savedViewService.maybeRefreshDocumentCounts()
|
||||||
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.PaperlessTask
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.tasksService.reload()
|
||||||
|
}
|
||||||
|
if (status === PaperlessTaskStatus.Success) {
|
||||||
|
this.toastService.showSuccess($localize`Bulk operation completed.`)
|
||||||
|
} else if (status === PaperlessTaskStatus.Revoked) {
|
||||||
|
this.toastService.showError($localize`Bulk operation revoked.`, error)
|
||||||
|
} else {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Bulk operation failed. Some changes may already have been applied.`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.lastScrollY = window.scrollY
|
this.lastScrollY = window.scrollY
|
||||||
this.detectClassicScrollbars()
|
this.detectClassicScrollbars()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
<div ngbDropdown #resultsDropdown="ngbDropdown" (openChange)="onDropdownOpenChange">
|
<div ngbDropdown #resultsDropdown="ngbDropdown" (openChange)="onDropdownOpenChange">
|
||||||
<form class="form-inline position-relative">
|
<form class="form-inline position-relative">
|
||||||
<i-bs width="1em" height="1em" name="search"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="search"></pngx-bs-icon>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="form-control form-control-sm">
|
<div class="form-control form-control-sm">
|
||||||
<input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query"
|
<input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<ng-container i18n>Search</ng-container>
|
<ng-container i18n>Search</ng-container>
|
||||||
}
|
}
|
||||||
<i-bs width="1em" height="1em" name="arrow-right-short"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="arrow-right-short"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1"
|
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1"
|
||||||
(click)="primaryAction(type, item, $event)"
|
(click)="primaryAction(type, item, $event)"
|
||||||
(mouseenter)="onItemHover($event)">
|
(mouseenter)="onItemHover($event)">
|
||||||
<i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs>
|
<pngx-bs-icon width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></pngx-bs-icon>
|
||||||
<div class="text-truncate">
|
<div class="text-truncate">
|
||||||
{{item[nameProp]}}
|
{{item[nameProp]}}
|
||||||
@if (date) {
|
@if (date) {
|
||||||
@@ -49,13 +49,13 @@
|
|||||||
[disabled]="disablePrimaryButton(type, item)"
|
[disabled]="disablePrimaryButton(type, item)"
|
||||||
(mouseenter)="onButtonHover($event)">
|
(mouseenter)="onButtonHover($event)">
|
||||||
@if (type === DataType.Document) {
|
@if (type === DataType.Document) {
|
||||||
<i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
|
<pngx-bs-icon width="1em" height="1em" name="file-earmark-richtext" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
|
||||||
} @else if (type === DataType.SavedView) {
|
} @else if (type === DataType.SavedView) {
|
||||||
<i-bs width="1em" height="1em" name="eye" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
|
<pngx-bs-icon width="1em" height="1em" name="eye" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
|
||||||
} @else if (type === DataType.Workflow || type === DataType.CustomField || type === DataType.Group || type === DataType.User || type === DataType.MailAccount || type === DataType.MailRule) {
|
} @else if (type === DataType.Workflow || type === DataType.CustomField || type === DataType.Group || type === DataType.User || type === DataType.MailAccount || type === DataType.MailRule) {
|
||||||
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
|
<pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs width="1em" height="1em" name="filter" class="me-1"></i-bs><span><ng-container i18n>Filter documents</ng-container></span>
|
<pngx-bs-icon width="1em" height="1em" name="filter" class="me-1"></pngx-bs-icon><span><ng-container i18n>Filter documents</ng-container></span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) {
|
@if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) {
|
||||||
@@ -65,9 +65,9 @@
|
|||||||
[disabled]="disableSecondaryButton(type, item)"
|
[disabled]="disableSecondaryButton(type, item)"
|
||||||
(mouseenter)="onButtonHover($event)">
|
(mouseenter)="onButtonHover($event)">
|
||||||
@if (type === DataType.Document) {
|
@if (type === DataType.Document) {
|
||||||
<i-bs width="1em" height="1em" name="download" class="me-1"></i-bs><span><ng-container i18n>Download</ng-container></span>
|
<pngx-bs-icon width="1em" height="1em" name="download" class="me-1"></pngx-bs-icon><span><ng-container i18n>Download</ng-container></span>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
|
<pngx-bs-icon width="1em" height="1em" name="file-earmark-richtext" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
form {
|
form {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> i-bs[name="search"] {
|
> pngx-bs-icon[name="search"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.6rem;
|
left: 0.6rem;
|
||||||
top: .25rem;
|
top: .25rem;
|
||||||
@@ -14,7 +14,7 @@ form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
i-bs[name="search"],
|
pngx-bs-icon[name="search"],
|
||||||
.badge {
|
.badge {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ form {
|
|||||||
|
|
||||||
.dropdown.show {
|
.dropdown.show {
|
||||||
// Hide the search icon when the dropdown is shown
|
// Hide the search icon when the dropdown is shown
|
||||||
i-bs[name="search"] {
|
pngx-bs-icon[name="search"] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { DataType } from 'src/app/data/datatype'
|
import { DataType } from 'src/app/data/datatype'
|
||||||
import {
|
import {
|
||||||
@@ -138,7 +137,6 @@ describe('GlobalSearchComponent', () => {
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
GlobalSearchComponent,
|
GlobalSearchComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subject, debounceTime, distinctUntilChanged, filter } from 'rxjs'
|
import { Subject, debounceTime, distinctUntilChanged, filter } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { DataType } from 'src/app/data/datatype'
|
import { DataType } from 'src/app/data/datatype'
|
||||||
import {
|
import {
|
||||||
FILTER_FULLTEXT_QUERY,
|
FILTER_FULLTEXT_QUERY,
|
||||||
@@ -65,7 +65,7 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e
|
|||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<span class="notification-count badge rounded-pill z-3 pe-none position-absolute" [class.bg-danger]="hasUnreadError()" [class.bg-secondary]="!hasUnreadError()">{{ unread().length }}</span>
|
<span class="notification-count badge rounded-pill z-3 pe-none position-absolute" [class.bg-danger]="hasUnreadError()" [class.bg-secondary]="!hasUnreadError()">{{ unread().length }}</span>
|
||||||
}
|
}
|
||||||
<button class="btn navbar-action border-0" id="notificationsDropdown" ngbDropdownToggle aria-label="Notifications" i18n-aria-label>
|
<button class="btn navbar-action border-0" id="notificationsDropdown" ngbDropdownToggle aria-label="Notifications" i18n-aria-label>
|
||||||
<i-bs width="1.3em" height="1.3em" name="bell"></i-bs>
|
<pngx-bs-icon width="1.3em" height="1.3em" name="bell"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu class="dropdown-menu-end shadow p-2" aria-labelledby="notificationsDropdown">
|
<div ngbDropdownMenu class="dropdown-menu-end shadow p-2" aria-labelledby="notificationsDropdown">
|
||||||
<div class="d-flex align-items-center px-1 mb-2">
|
<div class="d-flex align-items-center px-1 mb-2">
|
||||||
@@ -12,13 +12,13 @@
|
|||||||
@if (toasts().length) {
|
@if (toasts().length) {
|
||||||
<button class="btn btn-link btn-sm p-0 ms-auto text-body-secondary text-decoration-none clear-all"
|
<button class="btn btn-link btn-sm p-0 ms-auto text-body-secondary text-decoration-none clear-all"
|
||||||
(click)="toastService.clearToasts()">
|
(click)="toastService.clearToasts()">
|
||||||
<i-bs width="1em" height="1em" name="check2-all" class="me-1"></i-bs><ng-container i18n>Clear All</ng-container>
|
<pngx-bs-icon width="1em" height="1em" name="check2-all" class="me-1"></pngx-bs-icon><ng-container i18n>Clear All</ng-container>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (toasts().length === 0) {
|
@if (toasts().length === 0) {
|
||||||
<div class="text-center text-body-secondary small py-4">
|
<div class="text-center text-body-secondary small py-4">
|
||||||
<i-bs width="1.5em" height="1.5em" name="bell" class="d-block mx-auto mb-2 opacity-50"></i-bs>
|
<pngx-bs-icon width="1.5em" height="1.5em" name="bell" class="d-block mx-auto mb-2 opacity-50"></pngx-bs-icon>
|
||||||
<ng-container i18n>No notifications</ng-container>
|
<ng-container i18n>No notifications</ng-container>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -1,7 +1,6 @@
|
|||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
|
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
|
||||||
import { ToastsDropdownComponent } from './toasts-dropdown.component'
|
import { ToastsDropdownComponent } from './toasts-dropdown.component'
|
||||||
@@ -41,10 +40,7 @@ describe('ToastsDropdownComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [ToastsDropdownComponent],
|
||||||
ToastsDropdownComponent,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subscription, interval, map } from 'rxjs'
|
import { Subscription, interval, map } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
|
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
|
||||||
import { ToastComponent } from '../../common/toast/toast.component'
|
import { ToastComponent } from '../../common/toast/toast.component'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ import { ToastComponent } from '../../common/toast/toast.component'
|
|||||||
ToastComponent,
|
ToastComponent,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ToastsDropdownComponent {
|
export class ToastsDropdownComponent {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
<li ngbDropdown class="nav-item" display="dynamic" placement="bottom-end" [popperOptions]="popperOptions" (openChange)="onOpenChange($event)">
|
<li ngbDropdown class="nav-item" display="dynamic" placement="bottom-end" [popperOptions]="popperOptions" (openChange)="onOpenChange($event)">
|
||||||
<button class="btn navbar-action border-0" id="chatDropdown" ngbDropdownToggle aria-label="Chat" i18n-aria-label>
|
<button class="btn navbar-action border-0" id="chatDropdown" ngbDropdownToggle aria-label="Chat" i18n-aria-label>
|
||||||
<i-bs width="1.3em" height="1.3em" name="chatSquareDots"></i-bs>
|
<pngx-bs-icon width="1.3em" height="1.3em" name="chatSquareDots"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu class="dropdown-menu-end shadow p-0 overflow-hidden" aria-labelledby="chatDropdown">
|
<div ngbDropdownMenu class="dropdown-menu-end shadow p-0 overflow-hidden" aria-labelledby="chatDropdown">
|
||||||
<div class="d-flex align-items-center border-bottom px-3 py-2">
|
<div class="d-flex align-items-center border-bottom px-3 py-2">
|
||||||
<i-bs width="1em" height="1em" name="stars" class="text-primary me-2"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="stars" class="text-primary me-2"></pngx-bs-icon>
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
@if (documentId()) {
|
@if (documentId()) {
|
||||||
<ng-container i18n>Ask about this document</ng-container>
|
<ng-container i18n>Ask about this document</ng-container>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</span>
|
</span>
|
||||||
@if (messages().length) {
|
@if (messages().length) {
|
||||||
<button class="btn btn-sm btn-link text-muted ms-auto p-0" type="button" (click)="clearChat()" title="Clear chat" i18n-title>
|
<button class="btn btn-sm btn-link text-muted ms-auto p-0" type="button" (click)="clearChat()" title="Clear chat" i18n-title>
|
||||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="arrow-counterclockwise"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -42,11 +42,16 @@
|
|||||||
<span class="text-break">{{ message.content }}</span>
|
<span class="text-break">{{ message.content }}</span>
|
||||||
}
|
}
|
||||||
@if (message.role === 'assistant' && !message.isStreaming && message.references?.length) {
|
@if (message.role === 'assistant' && !message.isStreaming && message.references?.length) {
|
||||||
<div class="chat-references list-group mt-3">
|
<div class="chat-references mt-2 pt-2 border-top">
|
||||||
<div class="list-group-item text-muted small py-1" i18n>Sources</div>
|
<div class="d-flex align-items-center text-muted mb-1">
|
||||||
@for (reference of message.references; track reference.id) {
|
<span i18n>Sources</span>
|
||||||
<a class="list-group-item list-group-item-action d-flex align-items-center text-primary" [routerLink]="['/documents', reference.id]" [title]="reference.title">
|
@if (hiddenReferenceCount(message) > 0 && !expandedReferences().has(message)) {
|
||||||
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-2 flex-shrink-0"></i-bs><span class="text-truncate">{{ reference.title }}</span>
|
<button type="button" class="badge rounded-pill bg-body-secondary text-body-secondary fw-normal border-0 ms-2" (click)="expandReferences(message)" i18n>+{{ hiddenReferenceCount(message) }} more</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@for (reference of visibleReferences(message); track reference.id) {
|
||||||
|
<a class="reference-link d-flex align-items-center lh-sm text-primary text-decoration-none py-1" [routerLink]="['/documents', reference.id]" [title]="reference.title">
|
||||||
|
<pngx-bs-icon width="0.8em" height="0.8em" name="file-text" class="me-2 flex-shrink-0"></pngx-bs-icon><span class="text-truncate">{{ reference.title }}</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,11 +77,11 @@
|
|||||||
/>
|
/>
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="stopStreaming()" title="Stop" i18n-title aria-label="Stop" i18n-aria-label>
|
<button class="btn btn-outline-secondary" type="button" (click)="stopStreaming()" title="Stop" i18n-title aria-label="Stop" i18n-aria-label>
|
||||||
<i-bs width="1em" height="1em" name="x-lg"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="x-lg"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button class="btn btn-primary" type="button" (click)="sendMessage()" title="Send" i18n-title aria-label="Send" i18n-aria-label>
|
<button class="btn btn-primary" type="button" (click)="sendMessage()" title="Send" i18n-title aria-label="Send" i18n-aria-label>
|
||||||
<i-bs width="1em" height="1em" name="arrow-return-left"></i-bs>
|
<pngx-bs-icon width="1em" height="1em" name="arrow-return-left"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message .flex-grow-1 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
white-space: initial;
|
white-space: initial;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ElementRef } from '@angular/core'
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NavigationEnd, Router } from '@angular/router'
|
import { NavigationEnd, Router } from '@angular/router'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
CHAT_METADATA_DELIMITER,
|
CHAT_METADATA_DELIMITER,
|
||||||
@@ -22,11 +21,7 @@ describe('ChatComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [RouterTestingModule, ChatComponent],
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
RouterTestingModule,
|
|
||||||
ChatComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import {
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NavigationEnd, Router, RouterModule } from '@angular/router'
|
import { NavigationEnd, Router, RouterModule } from '@angular/router'
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { filter, map, Subscription } from 'rxjs'
|
import { filter, map, Subscription } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { MarkdownPipe } from 'src/app/pipes/markdown.pipe'
|
import { MarkdownPipe } from 'src/app/pipes/markdown.pipe'
|
||||||
import {
|
import {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
|
ChatReference,
|
||||||
ChatService,
|
ChatService,
|
||||||
parseChatResponse,
|
parseChatResponse,
|
||||||
} from 'src/app/services/chat.service'
|
} from 'src/app/services/chat.service'
|
||||||
@@ -27,7 +28,7 @@ const SLOW_RESPONSE_DELAY_MS = 8000
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
MarkdownPipe,
|
MarkdownPipe,
|
||||||
],
|
],
|
||||||
@@ -40,8 +41,29 @@ export class ChatComponent implements OnInit {
|
|||||||
readonly input = signal('')
|
readonly input = signal('')
|
||||||
readonly documentId = signal<number>(undefined)
|
readonly documentId = signal<number>(undefined)
|
||||||
readonly slowResponse = signal(false)
|
readonly slowResponse = signal(false)
|
||||||
|
readonly expandedReferences = signal<Set<ChatMessage>>(new Set())
|
||||||
public popperOptions = pngxPopperOptions
|
public popperOptions = pngxPopperOptions
|
||||||
|
|
||||||
|
static readonly COLLAPSED_REFERENCE_COUNT = 2
|
||||||
|
|
||||||
|
public visibleReferences(message: ChatMessage): ChatReference[] {
|
||||||
|
if (this.expandedReferences().has(message)) {
|
||||||
|
return message.references
|
||||||
|
}
|
||||||
|
return message.references.slice(0, ChatComponent.COLLAPSED_REFERENCE_COUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
public hiddenReferenceCount(message: ChatMessage): number {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
message.references.length - ChatComponent.COLLAPSED_REFERENCE_COUNT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public expandReferences(message: ChatMessage): void {
|
||||||
|
this.expandedReferences.update((expanded) => new Set(expanded).add(message))
|
||||||
|
}
|
||||||
|
|
||||||
private chatService: ChatService = inject(ChatService)
|
private chatService: ChatService = inject(ChatService)
|
||||||
private router: Router = inject(Router)
|
private router: Router = inject(Router)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { BootstrapIconComponent } from './bootstrap-icon.component'
|
||||||
|
|
||||||
|
describe('BootstrapIconComponent', () => {
|
||||||
|
let fixture: ComponentFixture<BootstrapIconComponent>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [BootstrapIconComponent],
|
||||||
|
})
|
||||||
|
fixture = TestBed.createComponent(BootstrapIconComponent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render the named icon as inline svg', () => {
|
||||||
|
fixture.componentRef.setInput('name', 'arrow-down-up')
|
||||||
|
fixture.detectChanges()
|
||||||
|
const svg: SVGElement = fixture.nativeElement.querySelector('svg')
|
||||||
|
expect(svg.classList).toContain('bi-arrow-down-up')
|
||||||
|
expect(svg.getAttribute('width')).toEqual('16')
|
||||||
|
expect(svg.getAttribute('height')).toEqual('16')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply width and height', () => {
|
||||||
|
fixture.componentRef.setInput('name', 'x-lg')
|
||||||
|
fixture.componentRef.setInput('width', '1.2em')
|
||||||
|
fixture.componentRef.setInput('height', 2)
|
||||||
|
fixture.detectChanges()
|
||||||
|
const svg: SVGElement = fixture.nativeElement.querySelector('svg')
|
||||||
|
expect(svg.getAttribute('width')).toEqual('1.2em')
|
||||||
|
expect(svg.getAttribute('height')).toEqual('2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow width or height to inject attributes', () => {
|
||||||
|
fixture.componentRef.setInput('name', 'x-lg')
|
||||||
|
fixture.componentRef.setInput('width', '1" onload="alert(1)')
|
||||||
|
fixture.componentRef.setInput('height', '1" onclick="alert(1)')
|
||||||
|
fixture.detectChanges()
|
||||||
|
const svg: SVGElement = fixture.nativeElement.querySelector('svg')
|
||||||
|
expect(svg.hasAttribute('onload')).toBe(false)
|
||||||
|
expect(svg.hasAttribute('onclick')).toBe(false)
|
||||||
|
expect(svg.getAttribute('width')).toEqual('1" onload="alert(1)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update when the name changes', () => {
|
||||||
|
fixture.componentRef.setInput('name', 'x-lg')
|
||||||
|
fixture.detectChanges()
|
||||||
|
fixture.componentRef.setInput('name', 'sliders2-vertical')
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.querySelector('svg').classList).toContain(
|
||||||
|
'bi-sliders2-vertical'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render nothing and warn for an unregistered icon', () => {
|
||||||
|
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
|
fixture.componentRef.setInput('name', 'not-an-icon')
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.innerHTML).toEqual('')
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith('Icon not found: not-an-icon')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
effect,
|
||||||
|
ElementRef,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { icons } from './icons'
|
||||||
|
|
||||||
|
/** Converts icon name e.g. `arrow-down-up` to its registry key. */
|
||||||
|
const toKey = (name: string) =>
|
||||||
|
name.trim().replace(/-(.)/g, (_, char: string) => char.toUpperCase())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an icon from the official bootstrap-icons package as inline SVG.
|
||||||
|
* Icons must be registered in `./icons.ts`.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-bs-icon',
|
||||||
|
template: '',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class BootstrapIconComponent {
|
||||||
|
private readonly elementRef = inject(ElementRef<HTMLElement>)
|
||||||
|
|
||||||
|
readonly name = input<string>()
|
||||||
|
readonly width = input<string | number>()
|
||||||
|
readonly height = input<string | number>()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
const element = this.elementRef.nativeElement
|
||||||
|
element.innerHTML = this.svg()
|
||||||
|
const svg = element.firstElementChild
|
||||||
|
if (!svg) return
|
||||||
|
const width = this.width()
|
||||||
|
const height = this.height()
|
||||||
|
if (width) svg.setAttribute('width', String(width))
|
||||||
|
if (height) svg.setAttribute('height', String(height))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private svg(): string {
|
||||||
|
const name = this.name()
|
||||||
|
if (!name) return ''
|
||||||
|
const svg = icons[toKey(name)]
|
||||||
|
if (!svg) {
|
||||||
|
console.warn(`Icon not found: ${name}`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return svg
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import airplane from 'bootstrap-icons/icons/airplane.svg'
|
||||||
|
import archive from 'bootstrap-icons/icons/archive.svg'
|
||||||
|
import arrowClockwise from 'bootstrap-icons/icons/arrow-clockwise.svg'
|
||||||
|
import arrowCounterclockwise from 'bootstrap-icons/icons/arrow-counterclockwise.svg'
|
||||||
|
import arrowDownUp from 'bootstrap-icons/icons/arrow-down-up.svg'
|
||||||
|
import arrowDown from 'bootstrap-icons/icons/arrow-down.svg'
|
||||||
|
import arrowLeft from 'bootstrap-icons/icons/arrow-left.svg'
|
||||||
|
import arrowRepeat from 'bootstrap-icons/icons/arrow-repeat.svg'
|
||||||
|
import arrowReturnLeft from 'bootstrap-icons/icons/arrow-return-left.svg'
|
||||||
|
import arrowRightShort from 'bootstrap-icons/icons/arrow-right-short.svg'
|
||||||
|
import arrowRight from 'bootstrap-icons/icons/arrow-right.svg'
|
||||||
|
import arrowUpRight from 'bootstrap-icons/icons/arrow-up-right.svg'
|
||||||
|
import asterisk from 'bootstrap-icons/icons/asterisk.svg'
|
||||||
|
import bank from 'bootstrap-icons/icons/bank.svg'
|
||||||
|
import basket from 'bootstrap-icons/icons/basket.svg'
|
||||||
|
import bell from 'bootstrap-icons/icons/bell.svg'
|
||||||
|
import bodyText from 'bootstrap-icons/icons/body-text.svg'
|
||||||
|
import bookmark from 'bootstrap-icons/icons/bookmark.svg'
|
||||||
|
import boxArrowInRight from 'bootstrap-icons/icons/box-arrow-in-right.svg'
|
||||||
|
import boxArrowUpRight from 'bootstrap-icons/icons/box-arrow-up-right.svg'
|
||||||
|
import boxArrowUp from 'bootstrap-icons/icons/box-arrow-up.svg'
|
||||||
|
import boxes from 'bootstrap-icons/icons/boxes.svg'
|
||||||
|
import braces from 'bootstrap-icons/icons/braces.svg'
|
||||||
|
import briefcase from 'bootstrap-icons/icons/briefcase.svg'
|
||||||
|
import building from 'bootstrap-icons/icons/building.svg'
|
||||||
|
import calculator from 'bootstrap-icons/icons/calculator.svg'
|
||||||
|
import calendarEventFill from 'bootstrap-icons/icons/calendar-event-fill.svg'
|
||||||
|
import calendarEvent from 'bootstrap-icons/icons/calendar-event.svg'
|
||||||
|
import calendar from 'bootstrap-icons/icons/calendar.svg'
|
||||||
|
import camera from 'bootstrap-icons/icons/camera.svg'
|
||||||
|
import cardChecklist from 'bootstrap-icons/icons/card-checklist.svg'
|
||||||
|
import cardHeading from 'bootstrap-icons/icons/card-heading.svg'
|
||||||
|
import caretDown from 'bootstrap-icons/icons/caret-down.svg'
|
||||||
|
import caretUp from 'bootstrap-icons/icons/caret-up.svg'
|
||||||
|
import cash from 'bootstrap-icons/icons/cash.svg'
|
||||||
|
import chatLeftText from 'bootstrap-icons/icons/chat-left-text.svg'
|
||||||
|
import chatSquareDots from 'bootstrap-icons/icons/chat-square-dots.svg'
|
||||||
|
import checkAll from 'bootstrap-icons/icons/check-all.svg'
|
||||||
|
import checkCircleFill from 'bootstrap-icons/icons/check-circle-fill.svg'
|
||||||
|
import checkCircle from 'bootstrap-icons/icons/check-circle.svg'
|
||||||
|
import checkLg from 'bootstrap-icons/icons/check-lg.svg'
|
||||||
|
import check from 'bootstrap-icons/icons/check.svg'
|
||||||
|
import check2All from 'bootstrap-icons/icons/check2-all.svg'
|
||||||
|
import chevronDoubleLeft from 'bootstrap-icons/icons/chevron-double-left.svg'
|
||||||
|
import chevronDoubleRight from 'bootstrap-icons/icons/chevron-double-right.svg'
|
||||||
|
import chevronDown from 'bootstrap-icons/icons/chevron-down.svg'
|
||||||
|
import chevronRight from 'bootstrap-icons/icons/chevron-right.svg'
|
||||||
|
import chevronUp from 'bootstrap-icons/icons/chevron-up.svg'
|
||||||
|
import circle from 'bootstrap-icons/icons/circle.svg'
|
||||||
|
import clipboardCheckFill from 'bootstrap-icons/icons/clipboard-check-fill.svg'
|
||||||
|
import clipboardCheck from 'bootstrap-icons/icons/clipboard-check.svg'
|
||||||
|
import clipboardFill from 'bootstrap-icons/icons/clipboard-fill.svg'
|
||||||
|
import clipboard from 'bootstrap-icons/icons/clipboard.svg'
|
||||||
|
import clockHistory from 'bootstrap-icons/icons/clock-history.svg'
|
||||||
|
import cpu from 'bootstrap-icons/icons/cpu.svg'
|
||||||
|
import creditCard from 'bootstrap-icons/icons/credit-card.svg'
|
||||||
|
import dashCircle from 'bootstrap-icons/icons/dash-circle.svg'
|
||||||
|
import dash from 'bootstrap-icons/icons/dash.svg'
|
||||||
|
import diagram3 from 'bootstrap-icons/icons/diagram-3.svg'
|
||||||
|
import dice5 from 'bootstrap-icons/icons/dice-5.svg'
|
||||||
|
import doorOpen from 'bootstrap-icons/icons/door-open.svg'
|
||||||
|
import download from 'bootstrap-icons/icons/download.svg'
|
||||||
|
import envelopeAtFill from 'bootstrap-icons/icons/envelope-at-fill.svg'
|
||||||
|
import envelopeAt from 'bootstrap-icons/icons/envelope-at.svg'
|
||||||
|
import envelope from 'bootstrap-icons/icons/envelope.svg'
|
||||||
|
import exclamationCircleFill from 'bootstrap-icons/icons/exclamation-circle-fill.svg'
|
||||||
|
import exclamationTriangleFill from 'bootstrap-icons/icons/exclamation-triangle-fill.svg'
|
||||||
|
import exclamationTriangle from 'bootstrap-icons/icons/exclamation-triangle.svg'
|
||||||
|
import eye from 'bootstrap-icons/icons/eye.svg'
|
||||||
|
import fileEarmarkArrowUp from 'bootstrap-icons/icons/file-earmark-arrow-up.svg'
|
||||||
|
import fileEarmarkCheck from 'bootstrap-icons/icons/file-earmark-check.svg'
|
||||||
|
import fileEarmarkDiff from 'bootstrap-icons/icons/file-earmark-diff.svg'
|
||||||
|
import fileEarmarkFill from 'bootstrap-icons/icons/file-earmark-fill.svg'
|
||||||
|
import fileEarmarkLock from 'bootstrap-icons/icons/file-earmark-lock.svg'
|
||||||
|
import fileEarmarkMedical from 'bootstrap-icons/icons/file-earmark-medical.svg'
|
||||||
|
import fileEarmarkMinus from 'bootstrap-icons/icons/file-earmark-minus.svg'
|
||||||
|
import fileEarmarkPerson from 'bootstrap-icons/icons/file-earmark-person.svg'
|
||||||
|
import fileEarmarkPlus from 'bootstrap-icons/icons/file-earmark-plus.svg'
|
||||||
|
import fileEarmarkRichtext from 'bootstrap-icons/icons/file-earmark-richtext.svg'
|
||||||
|
import fileEarmarkSpreadsheet from 'bootstrap-icons/icons/file-earmark-spreadsheet.svg'
|
||||||
|
import fileEarmark from 'bootstrap-icons/icons/file-earmark.svg'
|
||||||
|
import fileText from 'bootstrap-icons/icons/file-text.svg'
|
||||||
|
import files from 'bootstrap-icons/icons/files.svg'
|
||||||
|
import filter from 'bootstrap-icons/icons/filter.svg'
|
||||||
|
import floppy from 'bootstrap-icons/icons/floppy.svg'
|
||||||
|
import folderFill from 'bootstrap-icons/icons/folder-fill.svg'
|
||||||
|
import folder from 'bootstrap-icons/icons/folder.svg'
|
||||||
|
import funnel from 'bootstrap-icons/icons/funnel.svg'
|
||||||
|
import gear from 'bootstrap-icons/icons/gear.svg'
|
||||||
|
import globe2 from 'bootstrap-icons/icons/globe2.svg'
|
||||||
|
import google from 'bootstrap-icons/icons/google.svg'
|
||||||
|
import grid from 'bootstrap-icons/icons/grid.svg'
|
||||||
|
import gripVertical from 'bootstrap-icons/icons/grip-vertical.svg'
|
||||||
|
import hash from 'bootstrap-icons/icons/hash.svg'
|
||||||
|
import hddStack from 'bootstrap-icons/icons/hdd-stack.svg'
|
||||||
|
import heart from 'bootstrap-icons/icons/heart.svg'
|
||||||
|
import house from 'bootstrap-icons/icons/house.svg'
|
||||||
|
import inbox from 'bootstrap-icons/icons/inbox.svg'
|
||||||
|
import infoCircle from 'bootstrap-icons/icons/info-circle.svg'
|
||||||
|
import journalBookmarkFill from 'bootstrap-icons/icons/journal-bookmark-fill.svg'
|
||||||
|
import journals from 'bootstrap-icons/icons/journals.svg'
|
||||||
|
import lightbulb from 'bootstrap-icons/icons/lightbulb.svg'
|
||||||
|
import link from 'bootstrap-icons/icons/link.svg'
|
||||||
|
import listNested from 'bootstrap-icons/icons/list-nested.svg'
|
||||||
|
import listTask from 'bootstrap-icons/icons/list-task.svg'
|
||||||
|
import listUl from 'bootstrap-icons/icons/list-ul.svg'
|
||||||
|
import list from 'bootstrap-icons/icons/list.svg'
|
||||||
|
import microsoft from 'bootstrap-icons/icons/microsoft.svg'
|
||||||
|
import newspaper from 'bootstrap-icons/icons/newspaper.svg'
|
||||||
|
import nodePlus from 'bootstrap-icons/icons/node-plus.svg'
|
||||||
|
import paperclip from 'bootstrap-icons/icons/paperclip.svg'
|
||||||
|
import pencil from 'bootstrap-icons/icons/pencil.svg'
|
||||||
|
import peopleFill from 'bootstrap-icons/icons/people-fill.svg'
|
||||||
|
import people from 'bootstrap-icons/icons/people.svg'
|
||||||
|
import personCircle from 'bootstrap-icons/icons/person-circle.svg'
|
||||||
|
import personFillLock from 'bootstrap-icons/icons/person-fill-lock.svg'
|
||||||
|
import personFill from 'bootstrap-icons/icons/person-fill.svg'
|
||||||
|
import personLock from 'bootstrap-icons/icons/person-lock.svg'
|
||||||
|
import personSquare from 'bootstrap-icons/icons/person-square.svg'
|
||||||
|
import person from 'bootstrap-icons/icons/person.svg'
|
||||||
|
import playFill from 'bootstrap-icons/icons/play-fill.svg'
|
||||||
|
import plusCircle from 'bootstrap-icons/icons/plus-circle.svg'
|
||||||
|
import plus from 'bootstrap-icons/icons/plus.svg'
|
||||||
|
import printer from 'bootstrap-icons/icons/printer.svg'
|
||||||
|
import questionCircle from 'bootstrap-icons/icons/question-circle.svg'
|
||||||
|
import receipt from 'bootstrap-icons/icons/receipt.svg'
|
||||||
|
import safe from 'bootstrap-icons/icons/safe.svg'
|
||||||
|
import scissors from 'bootstrap-icons/icons/scissors.svg'
|
||||||
|
import search from 'bootstrap-icons/icons/search.svg'
|
||||||
|
import send from 'bootstrap-icons/icons/send.svg'
|
||||||
|
import shop from 'bootstrap-icons/icons/shop.svg'
|
||||||
|
import slashCircle from 'bootstrap-icons/icons/slash-circle.svg'
|
||||||
|
import sliders2Vertical from 'bootstrap-icons/icons/sliders2-vertical.svg'
|
||||||
|
import sortAlphaDown from 'bootstrap-icons/icons/sort-alpha-down.svg'
|
||||||
|
import sortAlphaUpAlt from 'bootstrap-icons/icons/sort-alpha-up-alt.svg'
|
||||||
|
import stack from 'bootstrap-icons/icons/stack.svg'
|
||||||
|
import stars from 'bootstrap-icons/icons/stars.svg'
|
||||||
|
import tagFill from 'bootstrap-icons/icons/tag-fill.svg'
|
||||||
|
import tag from 'bootstrap-icons/icons/tag.svg'
|
||||||
|
import tags from 'bootstrap-icons/icons/tags.svg'
|
||||||
|
import telephone from 'bootstrap-icons/icons/telephone.svg'
|
||||||
|
import textIndentLeft from 'bootstrap-icons/icons/text-indent-left.svg'
|
||||||
|
import textLeft from 'bootstrap-icons/icons/text-left.svg'
|
||||||
|
import threeDotsVertical from 'bootstrap-icons/icons/three-dots-vertical.svg'
|
||||||
|
import threeDots from 'bootstrap-icons/icons/three-dots.svg'
|
||||||
|
import trash from 'bootstrap-icons/icons/trash.svg'
|
||||||
|
import truck from 'bootstrap-icons/icons/truck.svg'
|
||||||
|
import uiRadios from 'bootstrap-icons/icons/ui-radios.svg'
|
||||||
|
import unlock from 'bootstrap-icons/icons/unlock.svg'
|
||||||
|
import upcScan from 'bootstrap-icons/icons/upc-scan.svg'
|
||||||
|
import wallet2 from 'bootstrap-icons/icons/wallet2.svg'
|
||||||
|
import windowStack from 'bootstrap-icons/icons/window-stack.svg'
|
||||||
|
import xCircle from 'bootstrap-icons/icons/x-circle.svg'
|
||||||
|
import xLg from 'bootstrap-icons/icons/x-lg.svg'
|
||||||
|
import x from 'bootstrap-icons/icons/x.svg'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<pngx-bs-icon>`. Only icons registered here are bundled but can add
|
||||||
|
* new ones (any icon from the bootstrap-icons package) as needed.
|
||||||
|
*/
|
||||||
|
export const icons: Record<string, string> = {
|
||||||
|
airplane,
|
||||||
|
archive,
|
||||||
|
arrowClockwise,
|
||||||
|
arrowCounterclockwise,
|
||||||
|
arrowDown,
|
||||||
|
arrowDownUp,
|
||||||
|
arrowLeft,
|
||||||
|
arrowRepeat,
|
||||||
|
arrowReturnLeft,
|
||||||
|
arrowRight,
|
||||||
|
arrowRightShort,
|
||||||
|
arrowUpRight,
|
||||||
|
asterisk,
|
||||||
|
bank,
|
||||||
|
basket,
|
||||||
|
bell,
|
||||||
|
braces,
|
||||||
|
bodyText,
|
||||||
|
bookmark,
|
||||||
|
boxArrowInRight,
|
||||||
|
boxArrowUp,
|
||||||
|
boxArrowUpRight,
|
||||||
|
boxes,
|
||||||
|
briefcase,
|
||||||
|
building,
|
||||||
|
calculator,
|
||||||
|
calendar,
|
||||||
|
calendarEvent,
|
||||||
|
calendarEventFill,
|
||||||
|
camera,
|
||||||
|
cardChecklist,
|
||||||
|
cardHeading,
|
||||||
|
caretDown,
|
||||||
|
caretUp,
|
||||||
|
chatLeftText,
|
||||||
|
chatSquareDots,
|
||||||
|
check,
|
||||||
|
check2All,
|
||||||
|
checkAll,
|
||||||
|
checkCircle,
|
||||||
|
checkCircleFill,
|
||||||
|
checkLg,
|
||||||
|
chevronDoubleLeft,
|
||||||
|
chevronDoubleRight,
|
||||||
|
chevronDown,
|
||||||
|
chevronRight,
|
||||||
|
chevronUp,
|
||||||
|
circle,
|
||||||
|
clipboard,
|
||||||
|
clipboardCheck,
|
||||||
|
clipboardCheckFill,
|
||||||
|
clipboardFill,
|
||||||
|
clockHistory,
|
||||||
|
cpu,
|
||||||
|
cash,
|
||||||
|
creditCard,
|
||||||
|
dash,
|
||||||
|
dashCircle,
|
||||||
|
diagram3,
|
||||||
|
dice5,
|
||||||
|
doorOpen,
|
||||||
|
download,
|
||||||
|
envelope,
|
||||||
|
envelopeAt,
|
||||||
|
envelopeAtFill,
|
||||||
|
exclamationCircleFill,
|
||||||
|
exclamationTriangle,
|
||||||
|
exclamationTriangleFill,
|
||||||
|
eye,
|
||||||
|
fileEarmark,
|
||||||
|
fileEarmarkArrowUp,
|
||||||
|
fileEarmarkCheck,
|
||||||
|
fileEarmarkDiff,
|
||||||
|
fileEarmarkFill,
|
||||||
|
fileEarmarkLock,
|
||||||
|
fileEarmarkMedical,
|
||||||
|
fileEarmarkMinus,
|
||||||
|
fileEarmarkPerson,
|
||||||
|
fileEarmarkPlus,
|
||||||
|
fileEarmarkRichtext,
|
||||||
|
fileEarmarkSpreadsheet,
|
||||||
|
files,
|
||||||
|
fileText,
|
||||||
|
filter,
|
||||||
|
floppy,
|
||||||
|
folder,
|
||||||
|
folderFill,
|
||||||
|
funnel,
|
||||||
|
gear,
|
||||||
|
globe2,
|
||||||
|
google,
|
||||||
|
grid,
|
||||||
|
gripVertical,
|
||||||
|
hash,
|
||||||
|
hddStack,
|
||||||
|
heart,
|
||||||
|
house,
|
||||||
|
inbox,
|
||||||
|
infoCircle,
|
||||||
|
journalBookmarkFill,
|
||||||
|
journals,
|
||||||
|
lightbulb,
|
||||||
|
link,
|
||||||
|
list,
|
||||||
|
listNested,
|
||||||
|
listTask,
|
||||||
|
listUl,
|
||||||
|
microsoft,
|
||||||
|
newspaper,
|
||||||
|
nodePlus,
|
||||||
|
pencil,
|
||||||
|
paperclip,
|
||||||
|
people,
|
||||||
|
peopleFill,
|
||||||
|
person,
|
||||||
|
personCircle,
|
||||||
|
personFill,
|
||||||
|
personFillLock,
|
||||||
|
personLock,
|
||||||
|
personSquare,
|
||||||
|
playFill,
|
||||||
|
plus,
|
||||||
|
plusCircle,
|
||||||
|
printer,
|
||||||
|
questionCircle,
|
||||||
|
receipt,
|
||||||
|
safe,
|
||||||
|
scissors,
|
||||||
|
search,
|
||||||
|
send,
|
||||||
|
slashCircle,
|
||||||
|
shop,
|
||||||
|
sliders2Vertical,
|
||||||
|
sortAlphaDown,
|
||||||
|
sortAlphaUpAlt,
|
||||||
|
stack,
|
||||||
|
stars,
|
||||||
|
tagFill,
|
||||||
|
tag,
|
||||||
|
tags,
|
||||||
|
textIndentLeft,
|
||||||
|
textLeft,
|
||||||
|
telephone,
|
||||||
|
threeDots,
|
||||||
|
threeDotsVertical,
|
||||||
|
trash,
|
||||||
|
truck,
|
||||||
|
uiRadios,
|
||||||
|
unlock,
|
||||||
|
upcScan,
|
||||||
|
wallet2,
|
||||||
|
windowStack,
|
||||||
|
x,
|
||||||
|
xCircle,
|
||||||
|
xLg,
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
@if (active()) {
|
@if (active()) {
|
||||||
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
||||||
@if (!isNumbered() && selected()) {
|
@if (!isNumbered() && selected()) {
|
||||||
<i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs>
|
<pngx-bs-icon class="check" width="1em" height="1em" name="check-lg"></pngx-bs-icon>
|
||||||
}
|
}
|
||||||
@if (isNumbered()) {
|
@if (isNumbered()) {
|
||||||
<div class="number">{{number()}}<span class="visually-hidden">selected</span></div>
|
<div class="number">{{number()}}<span class="visually-hidden">selected</span></div>
|
||||||
}
|
}
|
||||||
<i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs>
|
<pngx-bs-icon class="x" width=".9em" height="1em" name="x-lg"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||||
|
|
||||||
describe('ClearableBadgeComponent', () => {
|
describe('ClearableBadgeComponent', () => {
|
||||||
@@ -8,10 +7,7 @@ describe('ClearableBadgeComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [ClearableBadgeComponent],
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Component, computed, EventEmitter, input, Output } from '@angular/core'
|
import { Component, computed, EventEmitter, input, Output } from '@angular/core'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-clearable-badge',
|
selector: 'pngx-clearable-badge',
|
||||||
templateUrl: './clearable-badge.component.html',
|
templateUrl: './clearable-badge.component.html',
|
||||||
styleUrls: ['./clearable-badge.component.scss'],
|
styleUrls: ['./clearable-badge.component.scss'],
|
||||||
imports: [NgxBootstrapIconsModule],
|
imports: [BootstrapIconComponent],
|
||||||
})
|
})
|
||||||
export class ClearableBadgeComponent {
|
export class ClearableBadgeComponent {
|
||||||
readonly number = input<number>(undefined)
|
readonly number = input<number>(undefined)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
popoverClass="popover-slim"
|
popoverClass="popover-slim"
|
||||||
>
|
>
|
||||||
@if (iconName) {
|
@if (iconName) {
|
||||||
<i-bs [class.me-1]="label" name="{{iconName}}"></i-bs>
|
<pngx-bs-icon [class.me-1]="label" name="{{iconName}}"></pngx-bs-icon>
|
||||||
}
|
}
|
||||||
<ng-container>{{label}}</ng-container>
|
<ng-container>{{label}}</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { ConfirmButtonComponent } from './confirm-button.component'
|
import { ConfirmButtonComponent } from './confirm-button.component'
|
||||||
|
|
||||||
describe('ConfirmButtonComponent', () => {
|
describe('ConfirmButtonComponent', () => {
|
||||||
@@ -10,11 +9,7 @@ describe('ConfirmButtonComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [NgbPopoverModule, ConfirmButtonComponent],
|
||||||
NgbPopoverModule,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
ConfirmButtonComponent,
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ConfirmButtonComponent)
|
fixture = TestBed.createComponent(ConfirmButtonComponent)
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-confirm-button',
|
selector: 'pngx-confirm-button',
|
||||||
templateUrl: './confirm-button.component.html',
|
templateUrl: './confirm-button.component.html',
|
||||||
styleUrl: './confirm-button.component.scss',
|
styleUrl: './confirm-button.component.scss',
|
||||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule],
|
imports: [NgbPopoverModule, BootstrapIconComponent],
|
||||||
})
|
})
|
||||||
export class ConfirmButtonComponent {
|
export class ConfirmButtonComponent {
|
||||||
@Input()
|
@Input()
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
@let document = getDocument(documentID);
|
@let document = getDocument(documentID);
|
||||||
@if (document) {
|
@if (document) {
|
||||||
<li class="list-group-item d-flex align-items-center" cdkDrag>
|
<li class="list-group-item d-flex align-items-center" cdkDrag>
|
||||||
<i-bs name="grip-vertical" class="me-2"></i-bs>
|
<pngx-bs-icon name="grip-vertical" class="me-2"></pngx-bs-icon>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div>
|
<div>
|
||||||
@if (document.correspondent) {
|
@if (document.correspondent) {
|
||||||
|
|||||||
+1
-5
@@ -2,7 +2,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { MergeAsVersionsConfirmDialogComponent } from './merge-as-versions-confirm-dialog.component'
|
import { MergeAsVersionsConfirmDialogComponent } from './merge-as-versions-confirm-dialog.component'
|
||||||
@@ -14,10 +13,7 @@ describe('MergeAsVersionsConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [MergeAsVersionsConfirmDialogComponent],
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
MergeAsVersionsConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ import {
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { AsyncPipe } from '@angular/common'
|
||||||
import { Component, OnInit, computed, inject, signal } from '@angular/core'
|
import { Component, OnInit, computed, inject, signal } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { takeUntil } from 'rxjs'
|
import { takeUntil } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
@@ -24,7 +24,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MergeAsVersionsConfirmDialogComponent
|
export class MergeAsVersionsConfirmDialogComponent
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
@let document = getDocument(documentID);
|
@let document = getDocument(documentID);
|
||||||
@if (document) {
|
@if (document) {
|
||||||
<li class="list-group-item d-flex align-items-center" cdkDrag>
|
<li class="list-group-item d-flex align-items-center" cdkDrag>
|
||||||
<i-bs name="grip-vertical" class="me-2"></i-bs>
|
<pngx-bs-icon name="grip-vertical" class="me-2"></pngx-bs-icon>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div>
|
<div>
|
||||||
@if (document.correspondent) {
|
@if (document.correspondent) {
|
||||||
|
|||||||
+1
-7
@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
||||||
@@ -15,12 +14,7 @@ describe('MergeConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [ReactiveFormsModule, FormsModule, MergeConfirmDialogComponent],
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FormsModule,
|
|
||||||
MergeConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ import {
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { AsyncPipe } from '@angular/common'
|
||||||
import { Component, OnInit, inject, signal } from '@angular/core'
|
import { Component, OnInit, inject, signal } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { takeUntil } from 'rxjs'
|
import { takeUntil } from 'rxjs'
|
||||||
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
@@ -26,7 +26,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule,
|
BootstrapIconComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MergeConfirmDialogComponent
|
export class MergeConfirmDialogComponent
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@
|
|||||||
(ngModelChange)="onUpdateDocumentChange($event)"
|
(ngModelChange)="onUpdateDocumentChange($event)"
|
||||||
/>
|
/>
|
||||||
<label class="btn btn-outline-primary btn-sm" for="removeReplace">
|
<label class="btn btn-outline-primary btn-sm" for="removeReplace">
|
||||||
<i-bs name="pencil"></i-bs>
|
<pngx-bs-icon name="pencil"></pngx-bs-icon>
|
||||||
<span class="ms-2" i18n>Replace current document</span>
|
<span class="ms-2" i18n>Replace current document</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
(ngModelChange)="onUpdateDocumentChange($event)"
|
(ngModelChange)="onUpdateDocumentChange($event)"
|
||||||
/>
|
/>
|
||||||
<label class="btn btn-outline-primary btn-sm" for="removeCreate">
|
<label class="btn btn-outline-primary btn-sm" for="removeCreate">
|
||||||
<i-bs name="plus"></i-bs>
|
<pngx-bs-icon name="plus"></pngx-bs-icon>
|
||||||
<span class="ms-2" i18n>Create new document</span>
|
<span class="ms-2" i18n>Create new document</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-5
@@ -1,7 +1,6 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component'
|
import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component'
|
||||||
|
|
||||||
describe('PasswordRemovalConfirmDialogComponent', () => {
|
describe('PasswordRemovalConfirmDialogComponent', () => {
|
||||||
@@ -11,10 +10,7 @@ describe('PasswordRemovalConfirmDialogComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
providers: [NgbActiveModal],
|
providers: [NgbActiveModal],
|
||||||
imports: [
|
imports: [PasswordRemovalConfirmDialogComponent],
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
PasswordRemovalConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
|
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
|
||||||
|
|||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-password-removal-confirm-dialog',
|
selector: 'pngx-password-removal-confirm-dialog',
|
||||||
templateUrl: './password-removal-confirm-dialog.component.html',
|
templateUrl: './password-removal-confirm-dialog.component.html',
|
||||||
styleUrls: ['./password-removal-confirm-dialog.component.scss'],
|
styleUrls: ['./password-removal-confirm-dialog.component.scss'],
|
||||||
imports: [FormsModule, NgxBootstrapIconsModule],
|
imports: [FormsModule, BootstrapIconComponent],
|
||||||
})
|
})
|
||||||
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
|
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
updateDocument: boolean = true
|
updateDocument: boolean = true
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2 d-flex justify-content-end">
|
<div class="col-2 d-flex justify-content-end">
|
||||||
<button class="btn btn-secondary mt-auto" (click)="rotate(false)" aria-label="Rotate counterclockwise" i18n-aria-label>
|
<button class="btn btn-secondary mt-auto" (click)="rotate(false)" aria-label="Rotate counterclockwise" i18n-aria-label>
|
||||||
<i-bs name="arrow-counterclockwise"></i-bs>
|
<pngx-bs-icon name="arrow-counterclockwise"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8 d-flex align-items-center">
|
<div class="col-8 d-flex align-items-center">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-2 d-flex">
|
<div class="col-2 d-flex">
|
||||||
<button class="btn btn-secondary mt-auto" (click)="rotate()" aria-label="Rotate clockwise" i18n-aria-label>
|
<button class="btn btn-secondary mt-auto" (click)="rotate()" aria-label="Rotate clockwise" i18n-aria-label>
|
||||||
<i-bs name="arrow-clockwise"></i-bs>
|
<pngx-bs-icon name="arrow-clockwise"></pngx-bs-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-5
@@ -2,7 +2,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
||||||
|
|
||||||
describe('RotateConfirmDialogComponent', () => {
|
describe('RotateConfirmDialogComponent', () => {
|
||||||
@@ -11,10 +10,7 @@ describe('RotateConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [RotateConfirmDialogComponent],
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
RotateConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { NgStyle } from '@angular/common'
|
import { NgStyle } from '@angular/common'
|
||||||
import { Component, inject, signal } from '@angular/core'
|
import { Component, inject, signal } from '@angular/core'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|||||||
selector: 'pngx-rotate-confirm-dialog',
|
selector: 'pngx-rotate-confirm-dialog',
|
||||||
templateUrl: './rotate-confirm-dialog.component.html',
|
templateUrl: './rotate-confirm-dialog.component.html',
|
||||||
styleUrl: './rotate-confirm-dialog.component.scss',
|
styleUrl: './rotate-confirm-dialog.component.scss',
|
||||||
imports: [NgStyle, NgxBootstrapIconsModule],
|
imports: [NgStyle, BootstrapIconComponent],
|
||||||
})
|
})
|
||||||
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
documentService = inject(DocumentService)
|
documentService = inject(DocumentService)
|
||||||
|
|||||||
Loaded 100 of 474 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user