Compare commits

..
Author SHA1 Message Date
stumpylog 5be95f8fcc Chore: make the backend test suite pass on PostgreSQL
Remove SQLite-only assumptions from the tests: hard-coded primary keys,
NULL ordering, the database vendor, and the CPU-dependent default process
count. Keep the test connection open inside the test transaction, since
close_old_connections() and connections.close_all() really close it on
PostgreSQL.
2026-10-30 12:55:37 -07:00
462 changed files with 13624 additions and 16584 deletions

No files matched your search

+34 -34
View File
@@ -1,53 +1,53 @@
name: Install apt packages name: Install apt packages
description: > description: >
Installs apt packages with bounded download retries and Ubuntu mirror fallback. Caches downloaded packages as an optimization; apt resolves dependencies on every run. 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.
inputs: inputs:
packages: packages:
description: Space separated list of package names to install description: Space separated list of packages to install
required: true required: true
attempts: attempts:
description: Maximum number of download attempts description: Maximum number of attempts
default: "3" default: "3"
attempt-timeout: attempt-timeout:
description: Seconds before a single download attempt is killed description: Seconds before a single 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 }}
CACHE_DIR: ${{ runner.temp }}/apt-packages run: |
run: bash "$GITHUB_ACTION_PATH/install.sh" SUDO=""
# Save immediately: a later test failure must not discard downloaded packages. if [ "$(id -u)" -ne 0 ]; then
- name: Save apt packages SUDO="sudo"
if: steps.cache.outputs.cache-hit != 'true' fi
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: attempt_install() {
path: | $SUDO apt-get update -q
${{ runner.temp }}/apt-packages/*.deb # Intentionally unquoted, this is a list of package names
${{ runner.temp }}/apt-packages/.keep $SUDO apt-get install -q --yes --no-install-recommends $PACKAGES
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
-56
View File
@@ -1,56 +0,0 @@
#!/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
+5 -7
View File
@@ -8,8 +8,7 @@ on:
- 'translations**' - 'translations**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones group: backend-${{ github.event.pull_request.number || github.ref }}
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"
@@ -77,7 +76,6 @@ 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'
@@ -100,11 +98,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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
@@ -170,11 +168,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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.DEFAULT_PYTHON }}" python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+7 -8
View File
@@ -13,8 +13,7 @@ on:
- main - main
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones group: docker-${{ github.event.pull_request.number || github.ref }}
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
@@ -109,7 +108,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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -155,7 +154,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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: digests-${{ matrix.arch }} name: digests-${{ matrix.arch }}
path: /tmp/digests/digest-${{ matrix.arch }}.txt path: /tmp/digests/digest-${{ matrix.arch }}.txt
@@ -173,7 +172,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digest-*.txt pattern: digest-*.txt
@@ -185,20 +184,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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
+3 -4
View File
@@ -6,8 +6,7 @@ on:
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones group: docs-${{ github.event.pull_request.number || github.ref }}
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
@@ -75,11 +74,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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+9 -11
View File
@@ -8,8 +8,7 @@ on:
- 'translations**' - 'translations**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones group: frontend-${{ github.event.pull_request.number || github.ref }}
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:
@@ -69,7 +68,6 @@ 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'
@@ -87,7 +85,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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -119,7 +117,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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -158,7 +156,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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -193,7 +191,7 @@ jobs:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
container: mcr.microsoft.com/playwright:v1.64.1-resolute container: mcr.microsoft.com/playwright:v1.62.1-resolute
env: env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -207,18 +205,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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: '3.12' python-version: '3.12'
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
version: '0.13.x' version: '0.13.x'
enable-cache: false enable-cache: false
@@ -262,7 +260,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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
+3 -4
View File
@@ -7,8 +7,7 @@ on:
branches-ignore: branches-ignore:
- 'translations**' - 'translations**'
concurrency: concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones group: lint-${{ github.event.pull_request.number || github.ref }}
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
@@ -22,8 +21,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Install Python - name: Install Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "3.14" python-version: "3.14"
- name: Run prek - name: Run prek
uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0 uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
+9 -9
View File
@@ -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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0 uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.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@73f83b114970a3485e1423ecc1fb93d887c9b096 # v1.10.7 uses: shogo82148/actions-upload-release-asset@aaba0f56bdbc1071f4af234d5cb16055e8a400de # v1.10.4
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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
+3 -4
View File
@@ -8,8 +8,7 @@ on:
- 'translations**' - 'translations**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones group: static-analysis-${{ github.event.pull_request.number || github.ref }}
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
@@ -27,7 +26,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
semgrep: semgrep:
name: Semgrep CE name: Semgrep CE
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
@@ -45,7 +44,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@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
if: always() if: always()
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+2 -2
View File
@@ -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@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0 uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.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@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0 uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
+2 -2
View File
@@ -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@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
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@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+1 -1
View File
@@ -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@959dc74ac104447731cba99a016f85d0ae719788 # v3.5.0 uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true
+1 -1
View File
@@ -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@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0 uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1 uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
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"
+1 -1
View File
@@ -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.7-python3.14-trixie-slim@sha256:e80c05a310400cfc90af00b5b7271ef9ad24816e3265e1162af8a678e1aad099 AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.13.5-python3.14-trixie-slim@sha256:d8e732839dfc4225a1ff698803b478b2223c05636c055d3add41ab9dbec0f315 AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
+1 -1
View File
@@ -66,7 +66,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 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 - metadata names visible in a document's custom storage path, 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
+3 -3
View File
@@ -4,7 +4,7 @@
# correct networking for the tests # correct networking for the tests
services: services:
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.38 image: docker.io/gotenberg/gotenberg:8.37
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.15 image: docker.io/greenmail/standalone:2.1.14
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.7-alpine image: docker.io/nginx:1.31.6-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.38 image: docker.io/gotenberg/gotenberg:8.37
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.38 image: docker.io/gotenberg/gotenberg:8.37
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.38 image: docker.io/gotenberg/gotenberg:8.37
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.

After

Width:  |  Height:  |  Size: 30 KiB

-57
View File
@@ -1,57 +0,0 @@
# 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,6 +39,7 @@ 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"
) )
@@ -10,11 +10,9 @@ 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 --skip-checks exec python3 manage.py document_consumer
else else
exec s6-setuidgid paperless python3 manage.py document_consumer --skip-checks exec s6-setuidgid paperless python3 manage.py document_consumer
fi fi
fi fi
@@ -3,9 +3,8 @@
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 --skip-checks --app paperless beat --loglevel INFO exec /usr/local/bin/celery --app paperless beat --loglevel INFO
else else
exec s6-setuidgid paperless /usr/local/bin/celery --skip-checks --app paperless beat --loglevel INFO exec s6-setuidgid paperless /usr/local/bin/celery --app paperless beat --loglevel INFO
fi fi
@@ -3,9 +3,8 @@
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 --skip-checks --app paperless worker --loglevel INFO --without-mingle --without-gossip exec /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
else else
exec s6-setuidgid paperless /usr/local/bin/celery --skip-checks --app paperless worker --loglevel INFO --without-mingle --without-gossip exec s6-setuidgid paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
fi fi
-91
View File
@@ -1,96 +1,5 @@
# 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
+5 -30
View File
@@ -493,7 +493,7 @@ fields and permissions, which will be merged.
#### Types {#workflow-trigger-types} #### Types {#workflow-trigger-types}
Currently, there are five events that correspond to workflow trigger 'types': Currently, there are four events that correspond to workflow trigger 'types':
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption 1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
folder or API), file path, file name, mail rule folder or API), file path, file name, mail rule
@@ -505,10 +505,8 @@ Currently, there are five events that correspond to workflow trigger 'types':
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document 4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
offsets will trigger after the date, negative offsets will trigger before). offsets will trigger after the date, negative offsets will trigger before).
5. **Manual**: a trigger that only runs when you explicitly choose to run the workflow against one or more existing documents. See
[running a workflow manually](#running-a-workflow-manually) below.
The following flow diagram illustrates the document trigger types: The following flow diagram illustrates the four document trigger types:
```mermaid ```mermaid
flowchart TD flowchart TD
@@ -528,10 +526,6 @@ flowchart TD
matching matching
trigger(s)"} trigger(s)"}
manual{"Matching
'Manual'
trigger(s)"}
A[New Document] --> consumption A[New Document] --> consumption
consumption --> |Yes| C[Workflow Actions Run] consumption --> |Yes| C[Workflow Actions Run]
consumption --> |No| D consumption --> |No| D
@@ -549,10 +543,6 @@ flowchart TD
scheduled --> |Yes| N[Workflow Actions Run] scheduled --> |Yes| N[Workflow Actions Run]
scheduled --> |No| O[Document Saved] scheduled --> |No| O[Document Saved]
N --> O N --> O
P[User Runs Workflow] --> manual
manual --> |Yes| Q[Workflow Actions Run]
manual --> |No| R[Document Saved]
Q --> R
``` ```
#### Filters {#workflow-trigger-filters} #### Filters {#workflow-trigger-filters}
@@ -564,9 +554,9 @@ Workflows allow you to filter by:
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for - File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
example, automatically assigning documents to different owners based on the upload directory. example, automatically assigning documents to different owners based on the upload directory.
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source. - Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
- Content matching (`Added`, `Updated`, `Scheduled` and `Manual` triggers only). Filter document content using the matching settings. - Content matching (`Added`, `Updated` and `Scheduled` triggers only). Filter document content using the matching settings.
There are also 'advanced' filters available for `Added`, `Updated`, `Scheduled` and `Manual` triggers: There are also 'advanced' filters available for `Added`, `Updated` and `Scheduled` triggers:
- Any Tags: Filter for documents with any of the specified tags. - Any Tags: Filter for documents with any of the specified tags.
- All Tags: Filter for documents with all of the specified tags. - All Tags: Filter for documents with all of the specified tags.
@@ -579,21 +569,6 @@ There are also 'advanced' filters available for `Added`, `Updated`, `Scheduled`
- Not Storage paths: Filter documents without any of these storage paths. - Not Storage paths: Filter documents without any of these storage paths.
- Custom field query: Filter documents with a custom field query (the same as used for the document list filters). - Custom field query: Filter documents with a custom field query (the same as used for the document list filters).
#### Running a workflow manually {#running-a-workflow-manually}
A workflow with a **Manual** trigger does not run automatically. Instead, it can be run on demand against existing
documents from the web UI:
- From the document list, select one or more documents and choose **Run workflow** from the "Actions" menu.
- From an individual document, choose **Run workflow** from the "Actions" menu.
You will be asked which workflow to run. By default the workflow's trigger filters still apply, so documents that do
not match are skipped. You can instead choose to **run on all selected documents**, which ignores the trigger filters
and runs the workflow's actions on every selected document.
Manual runs happen in the background. As with scheduled workflows, the document list is not refreshed automatically when
they finish, so you may need to reload the list to see the results.
### Workflow Actions ### Workflow Actions
#### Types {#workflow-action-types} #### Types {#workflow-action-types}
@@ -674,7 +649,7 @@ new version of the same document, this time with normal OCR/text extraction appl
a password-protected file added with this trigger will briefly exist as an un-OCR'd version before a password-protected file added with this trigger will briefly exist as an un-OCR'd version before
the properly processed version is created. the properly processed version is created.
**Document Added**, **Document Updated**, **Scheduled**, **Manual**: these triggers run against a document that **Document Added**, **Document Updated**, **Scheduled**: these triggers run against a document that
already exists, so password removal happens immediately: the decrypted content is queued for already exists, so password removal happens immediately: the decrypted content is queued for
consumption as a new version right away. Note that if the document's initial consumption also consumption as a new version right away. Note that if the document's initial consumption also
happened while it was still encrypted, that original version will likewise be missing OCR text. happened while it was still encrypted, that original version will likewise be missing OCR text.
+11 -10
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "3.4.2" version = "3.4.0"
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,21 +26,22 @@ 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.18", "django~=5.2.13",
"django-allauth[mfa,socialaccount]~=65.21.1", "django-allauth[mfa,socialaccount]~=65.20.0",
"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>=26.2,<27", "django-filter>=25.1,<27",
"django-guardian>=3.5.2,<3.6", "django-guardian>=3.3.3,<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.10.1,<2026.11", "drf-spectacular-sidecar>=2026.7.1,<2026.10",
"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",
@@ -75,7 +76,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.15.3", "turbohtml~=1.10.0",
"watchfiles>=1.2", "watchfiles>=1.2",
"whitenoise~=6.11", "whitenoise~=6.11",
"whoosh-compat[tantivy]==0.3", "whoosh-compat[tantivy]==0.3",
@@ -111,14 +112,14 @@ lint = [
testing = [ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"faker>=40.36,<40.44", "faker>=40.36,<40.40",
"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.8.1", "pytest-env~=1.7.0",
"pytest-httpx", "pytest-httpx",
"pytest-mock~=3.17.0", "pytest-mock~=3.15.1",
"pytest-randomly~=5.0.0", "pytest-randomly~=5.0.0",
"pytest-rerunfailures~=16.4", "pytest-rerunfailures~=16.4",
"pytest-sugar", "pytest-sugar",
-3
View File
@@ -69,9 +69,6 @@
"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",
+1 -9
View File
@@ -43,7 +43,6 @@ 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
@@ -64,18 +63,11 @@ def seed_database() -> None:
username="playwright", username="playwright",
password="playwright", # NOSONAR password="playwright", # NOSONAR
) )
viewer = User.objects.create_user( 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,21 +80,12 @@ 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((url) => { await expect(page).toHaveURL(
const tags = url.searchParams.get('tags__id__all')?.split(',').sort() /tags__id__all=2,1&sort=created&reverse=1&page=1/
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 > pngx-bs-icon').first().click() await page.locator('.w-100 > label > i-bs').first().click()
await expect(page).not.toHaveURL(/reverse=1/) await expect(page).not.toHaveURL(/reverse=1/)
}) })
@@ -6,15 +6,8 @@ test.use({
}, },
}) })
test.beforeEach(async ({ page }) => {
const settingsLoaded = page.waitForResponse((response) =>
response.url().endsWith('/api/ui_settings/')
)
await page.goto('/dashboard')
expect((await settingsLoaded).status()).toBe(200)
})
test('should not allow user to edit settings', async ({ page }) => { test('should not allow user to edit settings', async ({ page }) => {
await page.goto('/dashboard')
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(
@@ -23,6 +16,7 @@ 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()
@@ -37,6 +31,7 @@ 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()
@@ -47,6 +42,7 @@ 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()
@@ -57,6 +53,7 @@ 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()
@@ -67,6 +64,7 @@ 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()
@@ -77,6 +75,7 @@ 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(
@@ -85,6 +84,7 @@ 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(
+1 -1
View File
@@ -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$|bootstrap-icons/icons/.*\\.svg$))', 'node_modules/(?!.*(\\.mjs$|tslib|lodash-es|normalize-diacritics|marked|@angular/common/locales/.*\\.js$))',
], ],
moduleNameMapper: { moduleNameMapper: {
...esmPreset.moduleNameMapper, ...esmPreset.moduleNameMapper,
+135 -155
View File
@@ -5,14 +5,14 @@
<trans-unit id="ngb.alert.close" datatype="html"> <trans-unit id="ngb.alert.close" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/alert/alert.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">38,39</context> <context context-type="linenumber">38,39</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.slide-number" datatype="html"> <trans-unit id="ngb.carousel.slide-number" datatype="html">
<source> Slide <x id="INTERPOLATION" equiv-text="source?: NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="s._cycleToSelec"/> </source> <source> Slide <x id="INTERPOLATION" equiv-text="xt(source?: Ngb"/> of <x id="INTERPOLATION_1" equiv-text="this._cycleToSe"/> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">386,393</context> <context context-type="linenumber">386,393</context>
</context-group> </context-group>
<note priority="1" from="description">Currently selected slide number read by screen reader</note> <note priority="1" from="description">Currently selected slide number read by screen reader</note>
@@ -20,219 +20,219 @@
<trans-unit id="ngb.carousel.previous" datatype="html"> <trans-unit id="ngb.carousel.previous" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">425,426</context> <context context-type="linenumber">425,426</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.next" datatype="html"> <trans-unit id="ngb.carousel.next" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">452,453</context> <context context-type="linenumber">452,453</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-month" datatype="html"> <trans-unit id="ngb.datepicker.select-month" datatype="html">
<source>Select month</source> <source>Select month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-year" datatype="html"> <trans-unit id="ngb.datepicker.select-year" datatype="html">
<source>Select year</source> <source>Select year</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.previous-month" datatype="html"> <trans-unit id="ngb.datepicker.previous-month" datatype="html">
<source>Previous month</source> <source>Previous month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,76</context> <context context-type="linenumber">73,76</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,76</context> <context context-type="linenumber">73,76</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.next-month" datatype="html"> <trans-unit id="ngb.datepicker.next-month" datatype="html">
<source>Next month</source> <source>Next month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,74</context> <context context-type="linenumber">73,74</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,74</context> <context context-type="linenumber">73,74</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first" datatype="html"> <trans-unit id="ngb.pagination.first" datatype="html">
<source>««</source> <source>««</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous" datatype="html"> <trans-unit id="ngb.pagination.previous" datatype="html">
<source>«</source> <source>«</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html"> <trans-unit id="ngb.pagination.next" datatype="html">
<source>»</source> <source>»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277</context> <context context-type="linenumber">277</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last" datatype="html"> <trans-unit id="ngb.pagination.last" datatype="html">
<source>»»</source> <source>»»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277</context> <context context-type="linenumber">277</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first-aria" datatype="html"> <trans-unit id="ngb.pagination.first-aria" datatype="html">
<source>First</source> <source>First</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous-aria" datatype="html"> <trans-unit id="ngb.pagination.previous-aria" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next-aria" datatype="html"> <trans-unit id="ngb.pagination.next-aria" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last-aria" datatype="html"> <trans-unit id="ngb.pagination.last-aria" datatype="html">
<source>Last</source> <source>Last</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.progressbar.value" datatype="html"> <trans-unit id="ngb.progressbar.value" datatype="html">
<source><x id="INTERPOLATION"/></source> <source><x id="INTERPOLATION"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/progressbar/progressbar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="linenumber">39,40</context> <context context-type="linenumber">39,40</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html"> <trans-unit id="ngb.timepicker.HH" datatype="html">
<source>HH</source> <source>HH</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.hours" datatype="html"> <trans-unit id="ngb.timepicker.hours" datatype="html">
<source>Hours</source> <source>Hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html"> <trans-unit id="ngb.timepicker.MM" datatype="html">
<source>MM</source> <source>MM</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.minutes" datatype="html"> <trans-unit id="ngb.timepicker.minutes" datatype="html">
<source>Minutes</source> <source>Minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-hours" datatype="html"> <trans-unit id="ngb.timepicker.increment-hours" datatype="html">
<source>Increment hours</source> <source>Increment hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html"> <trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
<source>Decrement hours</source> <source>Decrement hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html"> <trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
<source>Increment minutes</source> <source>Increment minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html"> <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
<source>Decrement minutes</source> <source>Decrement minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.SS" datatype="html"> <trans-unit id="ngb.timepicker.SS" datatype="html">
<source>SS</source> <source>SS</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.seconds" datatype="html"> <trans-unit id="ngb.timepicker.seconds" datatype="html">
<source>Seconds</source> <source>Seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html"> <trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
<source>Increment seconds</source> <source>Increment seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html"> <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
<source>Decrement seconds</source> <source>Decrement seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.PM" datatype="html"> <trans-unit id="ngb.timepicker.PM" datatype="html">
<source><x id="INTERPOLATION"/></source> <source><x id="INTERPOLATION"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html"> <trans-unit id="ngb.toast.close-aria" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/toast/toast.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/toast/toast.ts</context>
<context context-type="linenumber">80</context> <context context-type="linenumber">80</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -687,11 +687,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">441,442</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">443,444</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
@@ -839,7 +835,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">32</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -863,7 +859,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">474</context> <context context-type="linenumber">460</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -1141,7 +1137,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -1366,7 +1362,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1918</context> <context context-type="linenumber">1913</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1577733187050997705" datatype="html"> <trans-unit id="1577733187050997705" datatype="html">
@@ -1406,7 +1402,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">240,241</context> <context context-type="linenumber">239,240</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -1478,10 +1474,6 @@
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">166</context> <context context-type="linenumber">166</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="309314153079578337" datatype="html"> <trans-unit id="309314153079578337" datatype="html">
<source>Default Permissions</source> <source>Default Permissions</source>
@@ -1741,7 +1733,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">249,250</context> <context context-type="linenumber">248,249</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1776,7 +1768,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">258,259</context> <context context-type="linenumber">257,258</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1819,7 +1811,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">222,223</context> <context context-type="linenumber">221,222</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -2297,7 +2289,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">267,268</context> <context context-type="linenumber">266,267</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
@@ -3401,11 +3393,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1532</context> <context context-type="linenumber">1527</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1919</context> <context context-type="linenumber">1914</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -4123,7 +4115,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">391</context> <context context-type="linenumber">388</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7515883357904500238" datatype="html"> <trans-unit id="7515883357904500238" datatype="html">
@@ -4152,7 +4144,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1481</context> <context context-type="linenumber">1476</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -4296,7 +4288,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1972</context> <context context-type="linenumber">1967</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6661109599266152398" datatype="html"> <trans-unit id="6661109599266152398" datatype="html">
@@ -4307,7 +4299,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1973</context> <context context-type="linenumber">1968</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5162686434580248853" datatype="html"> <trans-unit id="5162686434580248853" datatype="html">
@@ -4318,7 +4310,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1974</context> <context context-type="linenumber">1969</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6665634854532231106" datatype="html"> <trans-unit id="6665634854532231106" datatype="html">
@@ -4397,7 +4389,7 @@
<source>Create new field</source> <source>Create new field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6973528734330066202" datatype="html"> <trans-unit id="6973528734330066202" datatype="html">
@@ -4794,7 +4786,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">276,277</context> <context context-type="linenumber">275,276</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -6575,7 +6567,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1485</context> <context context-type="linenumber">1480</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -6612,7 +6604,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">219,220</context> <context context-type="linenumber">218,219</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@@ -6796,21 +6788,21 @@
<source>Email sent</source> <source>Email sent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">53</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4632604705213766411" datatype="html"> <trans-unit id="4632604705213766411" datatype="html">
<source>Error emailing documents</source> <source>Error emailing documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3742745894977668908" datatype="html"> <trans-unit id="3742745894977668908" datatype="html">
<source>Error emailing document</source> <source>Error emailing document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">59</context> <context context-type="linenumber">60</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6381578200008167206" datatype="html"> <trans-unit id="6381578200008167206" datatype="html">
@@ -7156,14 +7148,14 @@
<source>Copy ID</source> <source>Copy ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">7</context> <context context-type="linenumber">6</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5392341774767336507" datatype="html"> <trans-unit id="5392341774767336507" datatype="html">
<source>Copied!</source> <source>Copied!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">8</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7198,7 +7190,7 @@
<source>Read more</source> <source>Read more</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">26,27</context> <context context-type="linenumber">24,25</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -8418,7 +8410,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">321</context> <context context-type="linenumber">320</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="78870852467682010" datatype="html"> <trans-unit id="78870852467682010" datatype="html">
@@ -8433,7 +8425,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">363</context> <context context-type="linenumber">360</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="157572966557284263" datatype="html"> <trans-unit id="157572966557284263" datatype="html">
@@ -8448,7 +8440,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">370</context> <context context-type="linenumber">367</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="883965278435032344" datatype="html"> <trans-unit id="883965278435032344" datatype="html">
@@ -8466,14 +8458,14 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">391</context> <context context-type="linenumber">388</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="872092479747931526" datatype="html"> <trans-unit id="872092479747931526" datatype="html">
<source>No documents</source> <source>No documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context> <context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context>
<context context-type="linenumber">191,194</context> <context context-type="linenumber">189,192</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1069523139277190436" datatype="html"> <trans-unit id="1069523139277190436" datatype="html">
@@ -8596,7 +8588,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">497</context> <context context-type="linenumber">483</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -8709,7 +8701,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">168</context> <context context-type="linenumber">483</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5028777105388019087" datatype="html"> <trans-unit id="5028777105388019087" datatype="html">
@@ -8871,82 +8863,63 @@
<source>Save &amp; next</source> <source>Save &amp; next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">446</context> <context context-type="linenumber">443</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">457</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4910102545766233758" datatype="html"> <trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source> <source>Save &amp; close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">448</context> <context context-type="linenumber">445</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">459</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3823219296477075982" datatype="html"> <trans-unit id="3823219296477075982" datatype="html">
<source>Discard</source> <source>Discard</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">450</context> <context context-type="linenumber">447,448</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">451,452</context>
</context-group>
</trans-unit>
<trans-unit id="5587846594415285055" datatype="html">
<source>More save options</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">454</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1309556917227148591" datatype="html"> <trans-unit id="1309556917227148591" datatype="html">
<source>Document loading...</source> <source>Document loading...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">469</context> <context context-type="linenumber">455</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2266163016683537825" datatype="html"> <trans-unit id="2266163016683537825" datatype="html">
<source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages()}}"/></source> <source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages()}}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">498,499</context> <context context-type="linenumber">484,485</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="944767796496567082" datatype="html"> <trans-unit id="944767796496567082" datatype="html">
<source>Zoom out</source> <source>Zoom out</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">502</context> <context context-type="linenumber">488</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6421460271971606186" datatype="html"> <trans-unit id="6421460271971606186" datatype="html">
<source>Zoom</source> <source>Zoom</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">505</context> <context context-type="linenumber">491</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6500061599200357251" datatype="html"> <trans-unit id="6500061599200357251" datatype="html">
<source>Zoom in</source> <source>Zoom in</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">512</context> <context context-type="linenumber">498</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8191371354890763172" datatype="html"> <trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source> <source>Enter Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">546</context> <context context-type="linenumber">532</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5758784066858623886" datatype="html"> <trans-unit id="5758784066858623886" datatype="html">
@@ -9142,14 +9115,14 @@
<source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash?</source> <source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1482</context> <context context-type="linenumber">1477</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="282586936710748252" datatype="html"> <trans-unit id="282586936710748252" datatype="html">
<source>Documents can be restored prior to permanent deletion.</source> <source>Documents can be restored prior to permanent deletion.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1483</context> <context context-type="linenumber">1478</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -9160,21 +9133,21 @@
<source>Moved &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash</source> <source>Moved &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1501</context> <context context-type="linenumber">1496</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7295637485862454066" datatype="html"> <trans-unit id="7295637485862454066" datatype="html">
<source>Error deleting document</source> <source>Error deleting document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1508</context> <context context-type="linenumber">1503</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="619486176823357521" datatype="html"> <trans-unit id="619486176823357521" datatype="html">
<source>Reprocess confirm</source> <source>Reprocess confirm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1528</context> <context context-type="linenumber">1523</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -9185,102 +9158,102 @@
<source>This operation will permanently recreate the archive file for this document.</source> <source>This operation will permanently recreate the archive file for this document.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1529</context> <context context-type="linenumber">1524</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="302054111564709516" datatype="html"> <trans-unit id="302054111564709516" datatype="html">
<source>The archive file will be re-generated with the current settings.</source> <source>The archive file will be re-generated with the current settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1530</context> <context context-type="linenumber">1525</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4700389117298802932" datatype="html"> <trans-unit id="4700389117298802932" datatype="html">
<source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source> <source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1543</context> <context context-type="linenumber">1538</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4409560272830824468" datatype="html"> <trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source> <source>Error executing operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1554</context> <context context-type="linenumber">1549</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6030453331794586802" datatype="html"> <trans-unit id="6030453331794586802" datatype="html">
<source>Error downloading document</source> <source>Error downloading document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1618</context> <context context-type="linenumber">1613</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4458954481601077369" datatype="html"> <trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source> <source>Page Fit</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1696</context> <context context-type="linenumber">1691</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4663705961777238777" datatype="html"> <trans-unit id="4663705961777238777" datatype="html">
<source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source> <source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1939</context> <context context-type="linenumber">1934</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9043972994040261999" datatype="html"> <trans-unit id="9043972994040261999" datatype="html">
<source>Error executing PDF edit operation</source> <source>Error executing PDF edit operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1951</context> <context context-type="linenumber">1946</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6172690334763056188" datatype="html"> <trans-unit id="6172690334763056188" datatype="html">
<source>Please enter the current password before attempting to remove it.</source> <source>Please enter the current password before attempting to remove it.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1962</context> <context context-type="linenumber">1957</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="968660764814228922" datatype="html"> <trans-unit id="968660764814228922" datatype="html">
<source>Password removal operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source> <source>Password removal operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1996</context> <context context-type="linenumber">1991</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2282118435712883014" datatype="html"> <trans-unit id="2282118435712883014" datatype="html">
<source>Error executing password removal operation</source> <source>Error executing password removal operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2010</context> <context context-type="linenumber">2005</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3740891324955700797" datatype="html"> <trans-unit id="3740891324955700797" datatype="html">
<source>Print failed.</source> <source>Print failed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2060</context> <context context-type="linenumber">2055</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6457245677384603573" datatype="html"> <trans-unit id="6457245677384603573" datatype="html">
<source>Error loading document for printing.</source> <source>Error loading document for printing.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2069</context> <context context-type="linenumber">2064</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6085793215710522488" datatype="html"> <trans-unit id="6085793215710522488" datatype="html">
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2152</context> <context context-type="linenumber">2147</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2158</context> <context context-type="linenumber">2153</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4958946940233632319" datatype="html"> <trans-unit id="4958946940233632319" datatype="html">
@@ -9946,7 +9919,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">339</context> <context context-type="linenumber">336</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="106713086593101376" datatype="html"> <trans-unit id="106713086593101376" datatype="html">
@@ -10261,21 +10234,21 @@
<source>Sort by title</source> <source>Sort by title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">214</context> <context context-type="linenumber">213</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6232673011753681091" datatype="html"> <trans-unit id="6232673011753681091" datatype="html">
<source>Sort by owner</source> <source>Sort by owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">227</context> <context context-type="linenumber">226</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3715596725146409911" datatype="html"> <trans-unit id="3715596725146409911" datatype="html">
<source>Owner</source> <source>Owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">231,232</context> <context context-type="linenumber">230,231</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -10290,49 +10263,49 @@
<source>Sort by notes</source> <source>Sort by notes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">236</context> <context context-type="linenumber">235</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5499001829734502606" datatype="html"> <trans-unit id="5499001829734502606" datatype="html">
<source>Sort by document type</source> <source>Sort by document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">245</context> <context context-type="linenumber">244</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6213829731736042759" datatype="html"> <trans-unit id="6213829731736042759" datatype="html">
<source>Sort by storage path</source> <source>Sort by storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">254</context> <context context-type="linenumber">253</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3406167410329973166" datatype="html"> <trans-unit id="3406167410329973166" datatype="html">
<source>Sort by created date</source> <source>Sort by created date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">263</context> <context context-type="linenumber">262</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3769035778779263084" datatype="html"> <trans-unit id="3769035778779263084" datatype="html">
<source>Sort by added date</source> <source>Sort by added date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">272</context> <context context-type="linenumber">271</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4874754501044009042" datatype="html"> <trans-unit id="4874754501044009042" datatype="html">
<source>Sort by number of pages</source> <source>Sort by number of pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">281</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3817498941817715969" datatype="html"> <trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source> <source>Pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">285,286</context> <context context-type="linenumber">284,285</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -10351,28 +10324,28 @@
<source> Shared </source> <source> Shared </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">289</context> <context context-type="linenumber">288</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5083658411133224968" datatype="html"> <trans-unit id="5083658411133224968" datatype="html">
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source> <source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">295,296</context> <context context-type="linenumber">294,295</context>
</context-group>
</trans-unit>
<trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">327</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2179847500064178686" datatype="html"> <trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source> <source>Edit document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">335</context> <context context-type="linenumber">328</context>
</context-group>
</trans-unit>
<trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">329</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4512084577073831437" datatype="html"> <trans-unit id="4512084577073831437" datatype="html">
@@ -10404,31 +10377,31 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2155249406916744630" datatype="html"> <trans-unit id="2155249406916744630" datatype="html">
<source>View &quot;<x id="PH" equiv-text="view.name ?? title"/>&quot; saved successfully.</source> <source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">423</context> <context context-type="linenumber">422</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4646273665293421938" datatype="html"> <trans-unit id="4646273665293421938" datatype="html">
<source>Failed to save view &quot;<x id="PH" equiv-text="title"/>&quot;.</source> <source>Failed to save view &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot;.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">429</context> <context context-type="linenumber">428</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6837554170707123455" datatype="html"> <trans-unit id="6837554170707123455" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source> <source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">498</context> <context context-type="linenumber">497</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6028096992841030074" datatype="html"> <trans-unit id="6028096992841030074" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully, but could not update visibility settings.</source> <source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully, but could not update visibility settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">504</context> <context context-type="linenumber">503</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="739880801667335279" datatype="html"> <trans-unit id="739880801667335279" datatype="html">
@@ -11478,6 +11451,13 @@
<context context-type="linenumber">4,5</context> <context context-type="linenumber">4,5</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8420539325839715135" datatype="html">
<source><x id="START_TAG_I_BS" ctype="x-i_bs" equiv-text="&lt;i-bs class=&quot;me-1&quot; name=&quot;person-fill-lock&quot;&gt;"/><x id="CLOSE_TAG_I_BS" ctype="x-i_bs" equiv-text="&lt;/i-bs&gt;"/>Permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">46,47</context>
</context-group>
</trans-unit>
<trans-unit id="6338800642797811873" datatype="html"> <trans-unit id="6338800642797811873" datatype="html">
<source>Documents page size</source> <source>Documents page size</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -13478,14 +13458,14 @@
<source>Prev</source> <source>Prev</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">167</context> <context context-type="linenumber">482</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1241348629231510663" datatype="html"> <trans-unit id="1241348629231510663" datatype="html">
<source>End</source> <source>End</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">169</context> <context context-type="linenumber">484</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
</body> </body>
+25 -25
View File
@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "3.4.2", "version": "3.4.0",
"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.4", "@angular/cdk": "^22.2.2",
"@angular/common": "~22.2.3", "@angular/common": "~22.2.2",
"@angular/compiler": "~22.2.3", "@angular/compiler": "~22.2.2",
"@angular/core": "~22.2.3", "@angular/core": "~22.2.2",
"@angular/forms": "~22.2.3", "@angular/forms": "~22.2.2",
"@angular/localize": "~22.2.3", "@angular/localize": "~22.2.2",
"@angular/platform-browser": "~22.2.3", "@angular/platform-browser": "~22.2.2",
"@angular/router": "~22.2.3", "@angular/router": "~22.2.2",
"@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.1.1", "marked": "~18.0.14",
"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.4.299", "pdfjs-dist": "^6.3.289",
"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.3" "uuid": "^14.0.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "^22.0.1", "@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.2.4", "@angular-devkit/core": "^22.2.2",
"@angular-devkit/schematics": "^22.2.4", "@angular-devkit/schematics": "^22.2.2",
"@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.4", "@angular/build": "22.2.2",
"@angular/cli": "22.2.4", "@angular/cli": "22.2.2",
"@angular/compiler-cli": "~22.2.3", "@angular/compiler-cli": "~22.2.2",
"@playwright/test": "^1.64.1", "@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.6.5", "@types/node": "^26.6.2",
"@typescript-eslint/eslint-plugin": "^8.72.0", "@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.72.0", "@typescript-eslint/parser": "^8.70.1",
"@typescript-eslint/utils": "^8.72.0", "@typescript-eslint/utils": "^8.70.1",
"eslint": "^10.13.0", "eslint": "^10.11.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.10", "prettier": "^3.9.9",
"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"
+664 -702
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -37,7 +37,7 @@ export const routes: Routes = [
path: 'dashboard', path: 'dashboard',
component: DashboardComponent, component: DashboardComponent,
data: { data: {
componentName: 'DashboardComponent', componentName: 'AppFrameComponent',
}, },
}, },
{ {
+2
View File
@@ -3,6 +3,7 @@ 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,
@@ -46,6 +47,7 @@ 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">
<pngx-bs-icon name="info-circle"></pngx-bs-icon> <i-bs name="info-circle"></i-bs>
</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)">
<pngx-bs-icon class="me-1" name="x"></pngx-bs-icon><ng-container i18n>Reset to external</ng-container> <i-bs class="me-1" name="x"></i-bs><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)">
<pngx-bs-icon class="me-1" name="x"></pngx-bs-icon><ng-container i18n>Reset</ng-container> <i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
</button> </button>
} }
} }
@@ -6,6 +6,7 @@ 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,
@@ -38,6 +39,7 @@ describe('ConfigComponent', () => {
NgSelectModule, NgSelectModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
ConfigComponent, ConfigComponent,
TextComponent, TextComponent,
SelectComponent, SelectComponent,
@@ -9,6 +9,7 @@ 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,
@@ -16,7 +17,6 @@ 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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
export class ConfigComponent export class ConfigComponent
@@ -8,6 +8,7 @@ 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'
@@ -39,6 +40,7 @@ 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()">
<pngx-bs-icon class="me-2" name="airplane"></pngx-bs-icon><ng-container i18n>Start tour</ng-container> <i-bs class="me-2" name="airplane"></i-bs><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 {
<pngx-bs-icon class="me-2" name="card-checklist"></pngx-bs-icon> <i-bs class="me-2" name="card-checklist"></i-bs>
@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">
<pngx-bs-icon name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></pngx-bs-icon> <i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
</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">
<pngx-bs-icon name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></pngx-bs-icon> <i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
</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>
<pngx-bs-icon class="ms-2" name="arrow-up-right"></pngx-bs-icon> <i-bs class="ms-2" name="arrow-up-right"></i-bs>
</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()">
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon><ng-container i18n>Reset</ng-container> <i-bs width="1em" height="1em" name="x"></i-bs><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">
<pngx-bs-icon name="question-circle"></pngx-bs-icon> <i-bs name="question-circle"></i-bs>
</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"><pngx-bs-icon class="me-1" name="cpu"></pngx-bs-icon><ng-container i18n>ML only</ng-container></label> <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>
<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"><pngx-bs-icon class="me-1" name="stars"></pngx-bs-icon><ng-container i18n>AI only</ng-container></label> <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>
</fieldset> </fieldset>
</div> </div>
</div> </div>
@@ -15,6 +15,7 @@ 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'
@@ -132,6 +133,7 @@ describe('SettingsComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbAlertModule, NgbAlertModule,
NgSelectModule, NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
NgbModalModule, NgbModalModule,
DragDropModule, DragDropModule,
SettingsComponent, SettingsComponent,
@@ -23,6 +23,7 @@ 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,
@@ -33,7 +34,6 @@ 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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
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">
<pngx-bs-icon name="x" class="me-1"></pngx-bs-icon><ng-container i18n>Clear selection</ng-container> <i-bs name="x" class="me-1"></i-bs><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">
<pngx-bs-icon name="check2-all" class="me-1"></pngx-bs-icon>{{dismissButtonText}} <i-bs name="check2-all" class="me-1"></i-bs>{{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">
<pngx-bs-icon name="check2-all" class="me-1"></pngx-bs-icon><ng-container i18n>Dismiss all</ng-container> <i-bs name="check2-all" class="me-1"></i-bs><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>
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon> <i-bs width="1em" height="1em" name="x"></i-bs>
</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()">
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon><small i18n>Reset filters</small> <i-bs width="1em" height="1em" name="x"></i-bs><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><pngx-bs-icon class="ms-1" width="1em" height="1em" name="arrow-right"></pngx-bs-icon> <ng-container i18n>Show all</ng-container><i-bs class="ms-1" width="1em" height="1em" name="arrow-right"></i-bs>
</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"><pngx-bs-icon class="me-1" width="1em" height="1em" name="box-arrow-in-right"></pngx-bs-icon>{{ task.trigger_source_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>
</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">
<pngx-bs-icon class="lh-1" width="1em" height="1em" name="exclamation-triangle"></pngx-bs-icon> <i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs>
<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>
<pngx-bs-icon width="1.2em" height="1.2em" name="info-circle"></pngx-bs-icon> <i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</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 }">
<pngx-bs-icon name="check" class="me-1"></pngx-bs-icon><ng-container i18n>Dismiss</ng-container> <i-bs name="check" class="me-1"></i-bs><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)">
<pngx-bs-icon name="file-text" class="me-1"></pngx-bs-icon><ng-container i18n>Open Document</ng-container> <i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
</a> </a>
} }
</ng-container> </ng-container>
@@ -10,6 +10,7 @@ import { By } from '@angular/platform-browser'
import { Router } from '@angular/router' import { 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 { 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 {
@@ -173,6 +174,7 @@ describe('TasksComponent', () => {
imports: [ imports: [
NgbModule, NgbModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
FormsModule, FormsModule,
TasksComponent, TasksComponent,
PageHeaderComponent, PageHeaderComponent,
@@ -9,6 +9,7 @@ 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,
@@ -21,7 +22,6 @@ 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,
@@ -151,7 +151,7 @@ const TRIGGER_SOURCE_OPTIONS: Array<{
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgbPopoverModule, NgbPopoverModule,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
export class TasksComponent export class TasksComponent
@@ -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">
<pngx-bs-icon name="x" class="me-1"></pngx-bs-icon><ng-container i18n>Clear selection</ng-container> <i-bs name="x" class="me-1"></i-bs><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">
<pngx-bs-icon name="arrow-counterclockwise" class="me-1"></pngx-bs-icon><ng-container i18n>Restore selected</ng-container> <i-bs name="arrow-counterclockwise" class="me-1"></i-bs><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">
<pngx-bs-icon name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete selected</ng-container> <i-bs name="trash" class="me-1"></i-bs><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">
<pngx-bs-icon name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Empty trash</ng-container> <i-bs name="trash" class="me-1"></i-bs><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>
<pngx-bs-icon name="eye"></pngx-bs-icon> <i-bs name="eye"></i-bs>
</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>
<pngx-bs-icon name="three-dots-vertical"></pngx-bs-icon> <i-bs name="three-dots-vertical"></i-bs>
</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();">
<pngx-bs-icon width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></pngx-bs-icon><ng-container i18n>Restore</ng-container> <i-bs width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></i-bs><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();">
<pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container> <i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</td> </td>
@@ -9,6 +9,7 @@ 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'
@@ -47,6 +48,7 @@ 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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
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 }">
<pngx-bs-icon name="plus-circle" class="me-1"></pngx-bs-icon><ng-container i18n>Add User</ng-container> <i-bs name="plus-circle" class="me-1"></i-bs><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 }">
<pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><ng-container i18n>Edit</ng-container> <i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><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 }">
<pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container> <i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><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 }">
<pngx-bs-icon name="plus-circle" class="me-1"></pngx-bs-icon><ng-container i18n>Add Group</ng-container> <i-bs name="plus-circle" class="me-1"></i-bs><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 }">
<pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><ng-container i18n>Edit</ng-container> <i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><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 }">
<pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container> <i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>
@@ -3,6 +3,7 @@ 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'
@@ -40,6 +41,7 @@ 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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
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()">
<pngx-bs-icon width="1.5em" height="1.5em" name="list"></pngx-bs-icon> <i-bs width="1.5em" height="1.5em" name="list"></i-bs>
</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>
<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> <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>
</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>
<pngx-bs-icon width="1.3em" height="1.3em" name="person-circle"></pngx-bs-icon> <i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
<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()">
<pngx-bs-icon class="me-2" name="person"></pngx-bs-icon><ng-container i18n>My Profile</ng-container> <i-bs class="me-2" name="person"></i-bs><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 }">
<pngx-bs-icon class="me-2" name="gear"></pngx-bs-icon><ng-container i18n>Settings</ng-container> <i-bs class="me-2" name="gear"></i-bs><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()">
<pngx-bs-icon class="me-2" name="door-open"></pngx-bs-icon><ng-container i18n>Logout</ng-container> <i-bs class="me-2" name="door-open"></i-bs><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">
<pngx-bs-icon class="me-2" name="question-circle"></pngx-bs-icon><ng-container i18n>Documentation</ng-container> <i-bs class="me-2" name="question-circle"></i-bs><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) {
<pngx-bs-icon width="0.9em" height="0.9em" name="chevron-double-right"></pngx-bs-icon> <i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
} @else { } @else {
<pngx-bs-icon width="0.9em" height="0.9em" name="chevron-double-left"></pngx-bs-icon> <i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
} }
</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">
<pngx-bs-icon class="me-2" name="house"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Dashboard</ng-container></span> <i-bs class="me-2" name="house"></i-bs><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">
<pngx-bs-icon class="me-2" name="files"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Documents</ng-container></span> <i-bs class="me-2" name="files"></i-bs><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">
<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> <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>
@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>
<pngx-bs-icon name="grip-vertical"></pngx-bs-icon> <i-bs name="grip-vertical"></i-bs>
</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">
<pngx-bs-icon class="me-2" name="file-text"></pngx-bs-icon><span>{{d.title | documentTitle}}</span> <i-bs class="me-2" name="file-text"></i-bs><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)">
<pngx-bs-icon name="x"></pngx-bs-icon> <i-bs name="x"></i-bs>
</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">
<pngx-bs-icon class="me-2" name="x"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Close all</ng-container></span> <i-bs class="me-2" name="x"></i-bs><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">
<pngx-bs-icon name="stack"></pngx-bs-icon><span class="nav-link-label ms-2"><ng-container i18n>Attributes</ng-container></span> <i-bs name="stack"></i-bs><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
> >
<pngx-bs-icon [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></pngx-bs-icon> <i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
</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()">
<pngx-bs-icon class="me-2" name="tags"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Tags</ng-container></span> <i-bs class="me-2" name="tags"></i-bs><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()">
<pngx-bs-icon class="me-2" name="person"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Correspondents</ng-container></span> <i-bs class="me-2" name="person"></i-bs><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()">
<pngx-bs-icon class="me-2" name="hash"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Document types</ng-container></span> <i-bs class="me-2" name="hash"></i-bs><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()">
<pngx-bs-icon class="me-2" name="folder"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Storage paths</ng-container></span> <i-bs class="me-2" name="folder"></i-bs><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()">
<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> <i-bs class="me-2" name="ui-radios"></i-bs><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">
<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> <i-bs class="me-2" name="window-stack"></i-bs><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">
<pngx-bs-icon class="me-2" name="link"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Share links</ng-container></span> <i-bs class="me-2" name="link"></i-bs><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">
<pngx-bs-icon class="me-2" name="boxes"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Workflows</ng-container></span> <i-bs class="me-2" name="boxes"></i-bs><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">
<pngx-bs-icon class="me-2" name="envelope"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Mail</ng-container></span> <i-bs class="me-2" name="envelope"></i-bs><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">
<pngx-bs-icon class="me-2" name="trash"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Trash</ng-container></span> <i-bs class="me-2" name="trash"></i-bs><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">
<pngx-bs-icon class="me-2" name="gear"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Settings</ng-container></span> <i-bs class="me-2" name="gear"></i-bs><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">
<pngx-bs-icon class="me-2" name="sliders2-vertical"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Configuration</ng-container></span> <i-bs class="me-2" name="sliders2-vertical"></i-bs><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">
<pngx-bs-icon class="me-2" name="people"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Users & Groups</ng-container></span> <i-bs class="me-2" name="people"></i-bs><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">
<pngx-bs-icon class="me-2" name="list-task"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Tasks</ng-container></span> <i-bs class="me-2" name="list-task"></i-bs><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">
<pngx-bs-icon class="me-2" name="text-left"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Logs</ng-container></span> <i-bs class="me-2" name="text-left"></i-bs><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">
<pngx-bs-icon class="d-flex me-2" name="question-circle"></pngx-bs-icon><span><ng-container i18n>Documentation</ng-container></span> <i-bs class="d-flex me-2" name="question-circle"></i-bs><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">
<pngx-bs-icon width="1.2em" height="1.2em" name="info-circle" class="me-1"></pngx-bs-icon> <i-bs width="1.2em" height="1.2em" name="info-circle" class="me-1"></i-bs>
@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">
<pngx-bs-icon width="1.2em" height="1.2em" name="info-circle"></pngx-bs-icon> <i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</a> </a>
} }
</div> </div>
@@ -142,7 +142,7 @@ main {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
pngx-bs-icon { i-bs {
margin-right: 0 !important; margin-right: 0 !important;
} }
} }
@@ -268,7 +268,7 @@ main {
} }
} }
pngx-bs-icon { i-bs {
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;
> pngx-bs-icon { > i-bs {
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -373,11 +373,11 @@ main {
width: 1.8rem; width: 1.8rem;
height: 100%; height: 100%;
pngx-bs-icon { i-bs {
opacity: 0.5; opacity: 0.5;
} }
&:hover pngx-bs-icon { &:hover i-bs {
opacity: 1; opacity: 1;
} }
} }
@@ -392,7 +392,7 @@ main {
text-decoration: underline; text-decoration: underline;
} }
pngx-bs-icon { i-bs {
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 pngx-bs-icon, button pngx-bs-icon { a i-bs, button i-bs {
opacity: 0.6; opacity: 0.6;
} }
} }
@@ -693,7 +693,7 @@ main {
cursor: move; cursor: move;
} }
::ng-deep .navItemDrag .position-absolute pngx-bs-icon { ::ng-deep .navItemDrag .position-absolute i-bs {
display: none; display: none;
} }
@@ -10,6 +10,7 @@ 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, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
@@ -107,6 +108,7 @@ describe('AppFrameComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
DragDropModule, DragDropModule,
NgbModalModule, NgbModalModule,
NgxBootstrapIconsModule.pick(allIcons),
AppFrameComponent, AppFrameComponent,
IfPermissionsDirective, IfPermissionsDirective,
GlobalSearchComponent, GlobalSearchComponent,
@@ -23,10 +23,10 @@ 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 { 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 { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { import {
@@ -89,7 +89,7 @@ const SCROLL_THRESHOLD = 16
NgbPopoverModule, NgbPopoverModule,
NgbCollapseModule, NgbCollapseModule,
NgbNavModule, NgbNavModule,
BootstrapIconComponent, NgxBootstrapIconsModule,
DragDropModule, DragDropModule,
TourNgBootstrap, TourNgBootstrap,
FormsModule, FormsModule,
@@ -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">
<pngx-bs-icon width="1em" height="1em" name="search"></pngx-bs-icon> <i-bs width="1em" height="1em" name="search"></i-bs>
<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>
} }
<pngx-bs-icon width="1em" height="1em" name="arrow-right-short"></pngx-bs-icon> <i-bs width="1em" height="1em" name="arrow-right-short"></i-bs>
</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)">
<pngx-bs-icon width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></pngx-bs-icon> <i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs>
<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) {
<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> <i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
} @else if (type === DataType.SavedView) { } @else if (type === DataType.SavedView) {
<pngx-bs-icon width="1em" height="1em" name="eye" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span> <i-bs width="1em" height="1em" name="eye" class="me-1"></i-bs><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) {
<pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span> <i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
} @else { } @else {
<pngx-bs-icon width="1em" height="1em" name="filter" class="me-1"></pngx-bs-icon><span><ng-container i18n>Filter documents</ng-container></span> <i-bs width="1em" height="1em" name="filter" class="me-1"></i-bs><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) {
<pngx-bs-icon width="1em" height="1em" name="download" class="me-1"></pngx-bs-icon><span><ng-container i18n>Download</ng-container></span> <i-bs width="1em" height="1em" name="download" class="me-1"></i-bs><span><ng-container i18n>Download</ng-container></span>
} @else { } @else {
<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> <i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
} }
</button> </button>
} }
@@ -1,7 +1,7 @@
form { form {
position: relative; position: relative;
> pngx-bs-icon[name="search"] { > i-bs[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 {
pngx-bs-icon[name="search"], i-bs[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
pngx-bs-icon[name="search"] { i-bs[name="search"] {
display: none !important; display: none !important;
} }
@@ -10,6 +10,7 @@ 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 {
@@ -137,6 +138,7 @@ 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,
BootstrapIconComponent, NgxBootstrapIconsModule,
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>
<pngx-bs-icon width="1.3em" height="1.3em" name="bell"></pngx-bs-icon> <i-bs width="1.3em" height="1.3em" name="bell"></i-bs>
</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()">
<pngx-bs-icon width="1em" height="1em" name="check2-all" class="me-1"></pngx-bs-icon><ng-container i18n>Clear All</ng-container> <i-bs width="1em" height="1em" name="check2-all" class="me-1"></i-bs><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">
<pngx-bs-icon width="1.5em" height="1.5em" name="bell" class="d-block mx-auto mb-2 opacity-50"></pngx-bs-icon> <i-bs width="1.5em" height="1.5em" name="bell" class="d-block mx-auto mb-2 opacity-50"></i-bs>
<ng-container i18n>No notifications</ng-container> <ng-container i18n>No notifications</ng-container>
</div> </div>
} }
@@ -1,6 +1,7 @@
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'
@@ -40,7 +41,10 @@ describe('ToastsDropdownComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ToastsDropdownComponent], imports: [
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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
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>
<pngx-bs-icon width="1.3em" height="1.3em" name="chatSquareDots"></pngx-bs-icon> <i-bs width="1.3em" height="1.3em" name="chatSquareDots"></i-bs>
</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">
<pngx-bs-icon width="1em" height="1em" name="stars" class="text-primary me-2"></pngx-bs-icon> <i-bs width="1em" height="1em" name="stars" class="text-primary me-2"></i-bs>
<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>
<pngx-bs-icon width="1em" height="1em" name="arrow-counterclockwise"></pngx-bs-icon> <i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs>
</button> </button>
} }
</div> </div>
@@ -46,7 +46,7 @@
<div class="list-group-item text-muted small py-1" i18n>Sources</div> <div class="list-group-item text-muted small py-1" i18n>Sources</div>
@for (reference of message.references; track reference.id) { @for (reference of message.references; track reference.id) {
<a class="list-group-item list-group-item-action d-flex align-items-center text-primary" [routerLink]="['/documents', reference.id]" [title]="reference.title"> <a class="list-group-item list-group-item-action d-flex align-items-center text-primary" [routerLink]="['/documents', reference.id]" [title]="reference.title">
<pngx-bs-icon width="0.9em" height="0.9em" name="file-text" class="me-2 flex-shrink-0"></pngx-bs-icon><span class="text-truncate">{{ reference.title }}</span> <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>
</a> </a>
} }
</div> </div>
@@ -72,11 +72,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>
<pngx-bs-icon width="1em" height="1em" name="x-lg"></pngx-bs-icon> <i-bs width="1em" height="1em" name="x-lg"></i-bs>
</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>
<pngx-bs-icon width="1em" height="1em" name="arrow-return-left"></pngx-bs-icon> <i-bs width="1em" height="1em" name="arrow-return-left"></i-bs>
</button> </button>
} }
</div> </div>
@@ -4,6 +4,7 @@ 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,
@@ -21,7 +22,11 @@ describe('ChatComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule, ChatComponent], imports: [
NgxBootstrapIconsModule.pick(allIcons),
RouterTestingModule,
ChatComponent,
],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
@@ -9,8 +9,8 @@ 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,
@@ -27,7 +27,7 @@ const SLOW_RESPONSE_DELAY_MS = 8000
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
BootstrapIconComponent, NgxBootstrapIconsModule,
NgbDropdownModule, NgbDropdownModule,
MarkdownPipe, MarkdownPipe,
], ],
@@ -1,61 +0,0 @@
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')
})
})
@@ -1,54 +0,0 @@
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
}
}
@@ -1,319 +0,0 @@
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 playCircle from 'bootstrap-icons/icons/play-circle.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,
playCircle,
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()) {
<pngx-bs-icon class="check" width="1em" height="1em" name="check-lg"></pngx-bs-icon> <i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs>
} }
@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>
} }
<pngx-bs-icon class="x" width=".9em" height="1em" name="x-lg"></pngx-bs-icon> <i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs>
</button> </button>
} }
@@ -1,4 +1,5 @@
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', () => {
@@ -7,7 +8,10 @@ describe('ClearableBadgeComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ClearableBadgeComponent], imports: [
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 { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@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: [BootstrapIconComponent], imports: [NgxBootstrapIconsModule],
}) })
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) {
<pngx-bs-icon [class.me-1]="label" name="{{iconName}}"></pngx-bs-icon> <i-bs [class.me-1]="label" name="{{iconName}}"></i-bs>
} }
<ng-container>{{label}}</ng-container> <ng-container>{{label}}</ng-container>
</button> </button>
@@ -1,6 +1,7 @@
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', () => {
@@ -9,7 +10,11 @@ describe('ConfirmButtonComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NgbPopoverModule, ConfirmButtonComponent], imports: [
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 { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@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, BootstrapIconComponent], imports: [NgbPopoverModule, NgxBootstrapIconsModule],
}) })
export class ConfirmButtonComponent { export class ConfirmButtonComponent {
@Input() @Input()
@@ -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>
<pngx-bs-icon name="grip-vertical" class="me-2"></pngx-bs-icon> <i-bs name="grip-vertical" class="me-2"></i-bs>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div> <div>
@if (document.correspondent) { @if (document.correspondent) {
@@ -2,6 +2,7 @@ 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'
@@ -13,7 +14,10 @@ describe('MergeAsVersionsConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [MergeAsVersionsConfirmDialogComponent], imports: [
NgxBootstrapIconsModule.pick(allIcons),
MergeAsVersionsConfirmDialogComponent,
],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@@ -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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
export class MergeAsVersionsConfirmDialogComponent export class MergeAsVersionsConfirmDialogComponent
@@ -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>
<pngx-bs-icon name="grip-vertical" class="me-2"></pngx-bs-icon> <i-bs name="grip-vertical" class="me-2"></i-bs>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div> <div>
@if (document.correspondent) { @if (document.correspondent) {
@@ -3,6 +3,7 @@ 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'
@@ -14,7 +15,12 @@ describe('MergeConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, FormsModule, MergeConfirmDialogComponent], imports: [
NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule,
FormsModule,
MergeConfirmDialogComponent,
],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@@ -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,
BootstrapIconComponent, NgxBootstrapIconsModule,
], ],
}) })
export class MergeConfirmDialogComponent export class MergeConfirmDialogComponent
@@ -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">
<pngx-bs-icon name="pencil"></pngx-bs-icon> <i-bs name="pencil"></i-bs>
<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">
<pngx-bs-icon name="plus"></pngx-bs-icon> <i-bs name="plus"></i-bs>
<span class="ms-2" i18n>Create new document</span> <span class="ms-2" i18n>Create new document</span>
</label> </label>
</div> </div>
@@ -1,6 +1,7 @@
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', () => {
@@ -10,7 +11,10 @@ describe('PasswordRemovalConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
providers: [NgbActiveModal], providers: [NgbActiveModal],
imports: [PasswordRemovalConfirmDialogComponent], imports: [
NgxBootstrapIconsModule.pick(allIcons),
PasswordRemovalConfirmDialogComponent,
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent) fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
@@ -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 { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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, BootstrapIconComponent], imports: [FormsModule, NgxBootstrapIconsModule],
}) })
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent { export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
updateDocument: boolean = true updateDocument: boolean = true
@@ -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>
<pngx-bs-icon name="arrow-counterclockwise"></pngx-bs-icon> <i-bs name="arrow-counterclockwise"></i-bs>
</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>
<pngx-bs-icon name="arrow-clockwise"></pngx-bs-icon> <i-bs name="arrow-clockwise"></i-bs>
</button> </button>
</div> </div>
</div> </div>
@@ -2,6 +2,7 @@ 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', () => {
@@ -10,7 +11,10 @@ describe('RotateConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RotateConfirmDialogComponent], imports: [
NgxBootstrapIconsModule.pick(allIcons),
RotateConfirmDialogComponent,
],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@@ -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 { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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, BootstrapIconComponent], imports: [NgStyle, NgxBootstrapIconsModule],
}) })
export class RotateConfirmDialogComponent extends ConfirmDialogComponent { export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
documentService = inject(DocumentService) documentService = inject(DocumentService)
@@ -1,32 +0,0 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
@if (message) {
<p class="text-break" [innerHTML]="message"></p>
}
<div class="mb-3">
<label class="form-label" for="runWorkflowSelect" i18n>Workflow</label>
<select class="form-select" id="runWorkflowSelect" [(ngModel)]="workflowId" (ngModelChange)="workflowSelected()">
<option [ngValue]="null" disabled i18n>Select a workflow</option>
@for (workflow of workflows; track workflow.id) {
<option [ngValue]="workflow.id">{{workflow.name}}</option>
}
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="runWorkflowForce" [(ngModel)]="force" />
<label class="form-check-label" for="runWorkflowForce" i18n>Run on all selected documents</label>
<div class="form-text" i18n>Ignore the workflow's trigger filters and run on every selected document.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
</button>
</div>
@@ -1,42 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { RunWorkflowConfirmDialogComponent } from './run-workflow-confirm-dialog.component'
describe('RunWorkflowConfirmDialogComponent', () => {
let component: RunWorkflowConfirmDialogComponent
let fixture: ComponentFixture<RunWorkflowConfirmDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [NgbActiveModal],
imports: [RunWorkflowConfirmDialogComponent],
}).compileComponents()
fixture = TestBed.createComponent(RunWorkflowConfirmDialogComponent)
component = fixture.componentInstance
component.workflows = [{ id: 5, name: 'Manual workflow' } as any]
fixture.detectChanges()
})
it('should disable confirm until a workflow is selected and not force by default', () => {
expect(component.confirmButtonEnabled).toBeFalsy()
expect(component.force).toBeFalsy()
component.workflowId = 5
component.workflowSelected()
expect(component.confirmButtonEnabled).toBeTruthy()
component.workflowId = null
component.workflowSelected()
expect(component.confirmButtonEnabled).toBeFalsy()
})
it('should render an option per workflow', () => {
const options = fixture.nativeElement.querySelectorAll(
'#runWorkflowSelect option'
)
// placeholder option + one per workflow
expect(options).toHaveLength(2)
expect(options[1].textContent).toContain('Manual workflow')
})
})
@@ -1,24 +0,0 @@
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { Workflow } from 'src/app/data/workflow'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-run-workflow-confirm-dialog',
templateUrl: './run-workflow-confirm-dialog.component.html',
imports: [FormsModule],
})
export class RunWorkflowConfirmDialogComponent extends ConfirmDialogComponent {
public workflows: Workflow[] = []
public workflowId: number = null
// i.e. ignore the workflow's trigger filters
public force: boolean = false
override confirmButtonEnabled = false
public workflowSelected(): void {
this.confirmButtonEnabled = !!this.workflowId
}
}
@@ -20,7 +20,7 @@
@for (docId of value; track docId) { @for (docId of value; track docId) {
@if (getDocumentTitle(docId)) { @if (getDocumentTitle(docId)) {
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title> <a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
<pngx-bs-icon width="0.9em" height="0.9em" name="file-text" class="me-1"></pngx-bs-icon><span>{{ getDocumentTitle(docId) }}</span> <i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{ getDocumentTitle(docId) }}</span>
</a> </a>
} }
} }
@@ -1,6 +1,6 @@
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions"> <div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle aria-label="Custom Fields" i18n-aria-label> <button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle aria-label="Custom Fields" i18n-aria-label>
<pngx-bs-icon name="ui-radios"></pngx-bs-icon><div class="d-none d-lg-inline ms-1"><ng-container i18n>Custom Fields</ng-container></div> <i-bs name="ui-radios"></i-bs><div class="d-none d-lg-inline ms-1"><ng-container i18n>Custom Fields</ng-container></div>
</button> </button>
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown"> <div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)"> <div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
@@ -11,12 +11,14 @@
</div> </div>
@for (field of filteredFields; track field.id) { @for (field of filteredFields; track field.id) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button> <button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
<div class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></div> <small class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></small>
</button> </button>
} }
@if (!filterText?.length || filteredFields.length === 0) { @if (!filterText?.length || filteredFields.length === 0) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button> <button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
<pngx-bs-icon width=".9em" height=".9em" name="asterisk" class="me-1"></pngx-bs-icon><ng-container i18n>Create new field</ng-container> <small>
<i-bs width=".9em" height=".9em" name="asterisk" class="me-1"></i-bs><ng-container i18n>Create new field</ng-container>
</small>
</button> </button>
} }
</div> </div>
@@ -10,6 +10,7 @@ import {
NgbModalRef, NgbModalRef,
} 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 { of } from 'rxjs' import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
@@ -48,6 +49,7 @@ describe('CustomFieldsDropdownComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbModalModule, NgbModalModule,
NgbDropdownModule, NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons),
CustomFieldsDropdownComponent, CustomFieldsDropdownComponent,
SelectComponent, SelectComponent,
], ],
@@ -12,8 +12,8 @@ import {
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first, takeUntil } from 'rxjs' import { first, takeUntil } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field' import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance' import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import { import {
@@ -34,7 +34,7 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
styleUrls: ['./custom-fields-dropdown.component.scss'], styleUrls: ['./custom-fields-dropdown.component.scss'],
imports: [ imports: [
NgbDropdownModule, NgbDropdownModule,
BootstrapIconComponent, NgxBootstrapIconsModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
], ],
Loaded 100 of 462 files, more files were not shown because too many files have changed in this diff. Show more