Compare commits

..
Author SHA1 Message Date
Trenton HandClaude Sonnet 5.5 4a305aad7e Performance: read shared_by grants once instead of probing per document
The shared_by__id filter counted a correlated guardian subquery per owned document. No guardian index leads with object_pk, so MariaDB re-scanned the grant table for every row (a 360k document install did not finish in 442 s), and Postgres took 21 s for an owner sharing 196k documents.

Match against each grant table once, as an uncorrelated DISTINCT set of integer keys, with a plain OR of two IN conditions. This also drops the aggregate and HAVING.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 08:38:03 -07:00
449 changed files with 13679 additions and 16298 deletions

No files matched your search

+34 -34
View File
@@ -1,53 +1,53 @@
name: Install apt packages
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:
packages:
description: Space separated list of package names to install
description: Space separated list of packages to install
required: true
attempts:
description: Maximum number of download attempts
description: Maximum number of attempts
default: "3"
attempt-timeout:
description: Seconds before a single download attempt is killed
description: Seconds before a single attempt is killed
default: "180"
runs:
using: composite
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 }}
shell: bash
env:
PACKAGES: ${{ inputs.packages }}
ATTEMPTS: ${{ inputs.attempts }}
ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }}
CACHE_DIR: ${{ runner.temp }}/apt-packages
run: bash "$GITHUB_ACTION_PATH/install.sh"
# Save immediately: a later test failure must not discard downloaded packages.
- name: Save apt packages
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
${{ runner.temp }}/apt-packages/*.deb
${{ runner.temp }}/apt-packages/.keep
key: ${{ steps.cache-key.outputs.key }}
run: |
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
attempt_install() {
$SUDO apt-get update -q
# Intentionally unquoted, this is a list of package names
$SUDO apt-get install -q --yes --no-install-recommends $PACKAGES
}
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**'
workflow_dispatch:
concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: backend-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
group: backend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
DEFAULT_UV_VERSION: "0.13.x"
@@ -77,7 +76,6 @@ jobs:
- 'uv.lock'
- 'docker/compose/docker-compose.ci-test.yml'
- '.github/workflows/ci-backend.yml'
- '.github/actions/apt-install/**'
test:
needs: changes
if: needs.changes.outputs.backend_changed == 'true'
@@ -100,11 +98,11 @@ jobs:
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -170,11 +168,11 @@ jobs:
persist-credentials: false
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
+7 -8
View File
@@ -13,8 +13,7 @@ on:
- main
workflow_dispatch:
concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: docker-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
group: docker-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
@@ -109,7 +108,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -155,7 +154,7 @@ jobs:
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
- name: Upload digest
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:
name: digests-${{ matrix.arch }}
path: /tmp/digests/digest-${{ matrix.arch }}.txt
@@ -173,7 +172,7 @@ jobs:
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: /tmp/digests
pattern: digest-*.txt
@@ -185,20 +184,20 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
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:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io
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:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
+3 -4
View File
@@ -6,8 +6,7 @@ on:
pull_request:
workflow_dispatch:
concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: docs-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
group: docs-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
@@ -75,11 +74,11 @@ jobs:
persist-credentials: false
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
+9 -11
View File
@@ -8,8 +8,7 @@ on:
- 'translations**'
workflow_dispatch:
concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: frontend-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
group: frontend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
@@ -69,7 +68,6 @@ jobs:
frontend:
- 'src-ui/**'
- '.github/workflows/ci-frontend.yml'
- '.github/actions/apt-install/**'
install-dependencies:
needs: changes
if: needs.changes.outputs.frontend_changed == 'true'
@@ -87,7 +85,7 @@ jobs:
with:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: 'pnpm'
@@ -119,7 +117,7 @@ jobs:
with:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: 'pnpm'
@@ -158,7 +156,7 @@ jobs:
with:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: 'pnpm'
@@ -193,7 +191,7 @@ jobs:
runs-on: ubuntu-26.04
permissions:
contents: read
container: mcr.microsoft.com/playwright:v1.64.1-resolute
container: mcr.microsoft.com/playwright:v1.62.1-resolute
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -207,18 +205,18 @@ jobs:
with:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: '0.13.x'
enable-cache: false
@@ -262,7 +260,7 @@ jobs:
with:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: 'pnpm'
+3 -4
View File
@@ -7,8 +7,7 @@ on:
branches-ignore:
- 'translations**'
concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: lint-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
group: lint-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
@@ -22,8 +21,8 @@ jobs:
with:
persist-credentials: false
- name: Install Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- 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:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
package-manager-cache: false
@@ -55,11 +55,11 @@ jobs:
# ---- Backend Setup ----
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
@@ -137,7 +137,7 @@ jobs:
sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release
path: dist/paperless-ngx.tar.xz
@@ -155,7 +155,7 @@ jobs:
version: ${{ steps.get-version.outputs.version }}
steps:
- name: Download release artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release
path: ./
@@ -172,7 +172,7 @@ jobs:
fi
- name: Create release and changelog
id: create-release
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
with:
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.version }}
@@ -183,7 +183,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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:
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
@@ -209,11 +209,11 @@ jobs:
persist-credentials: true # for pushing changelog branch
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
+3 -4
View File
@@ -8,8 +8,7 @@ on:
- 'translations**'
workflow_dispatch:
concurrency:
# Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: static-analysis-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
group: static-analysis-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
@@ -27,7 +26,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
semgrep:
name: Semgrep CE
runs-on: ubuntu-26.04
@@ -45,7 +44,7 @@ jobs:
- name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif
- 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()
with:
sarif_file: results.sarif
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0
uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
@@ -56,7 +56,7 @@ jobs:
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0
uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
# 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.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- 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 }}
persist-credentials: false
- name: crowdin action
uses: crowdin/github-action@959dc74ac104447731cba99a016f85d0ae719788 # v3.5.0
uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0
with:
upload_translations: false
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'
steps:
- 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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -23,14 +23,14 @@ jobs:
persist-credentials: true # for pushing translation branch
- name: Set up Python
id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install system dependencies
timeout-minutes: 12
uses: $/.github/actions/apt-install
with:
packages: gettext
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -48,7 +48,7 @@ jobs:
with:
package_json_file: src-ui/package.json
- name: Use Node.js 24
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: 'pnpm'
@@ -68,7 +68,7 @@ jobs:
cd src-ui
pnpm run ng extract-i18n
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
with:
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
commit_message: "Auto translate strings"
-44
View File
@@ -1,44 +0,0 @@
# Guidance for AI agents
Read [CONTRIBUTING.md](CONTRIBUTING.md) before working on this repository, especially [Use of AI Tools](CONTRIBUTING.md#use-of-ai-tools). These contribution rules apply to all agent-assisted work.
## Contribution boundaries
- Do not open or submit pull requests, including "on behalf of" a human. The project does not accept PRs from AI agents. Prepare local changes for a human to review, verify, and submit personally.
- The project does not accept PRs that are entirely or mostly AI-derived. Human submission and disclosure do not override this rule.
- AI-generated content must be clearly disclosed as described in the contributing guidelines. When handing work back to a human, identify what was generated or changed with AI assistance so they can disclose it accurately.
- Trust matters. Encourage first-time contributors to discuss their proposed change before preparing a PR, especially for large changes or changes involving AI-generated code. New features and enhancements should almost always target an existing feature request with community interest and discussion.
- The project prefers thoughtful, well-described ideas to AI-generated pull requests. Help contributors describe their problem, use case, desired behavior, and who would benefit. An implementation is not required for a valuable contribution, and a flawed implementation can obscure a useful idea before it receives proper discussion.
- AI tools have lowered the effort needed to produce a PR, but reviewing and maintaining it still requires substantial work. Encourage discussion of the idea before investing in implementation and review.
- Issue reports must describe observed behavior, reproduction steps, and relevant logs. Do not add speculative root causes, code analysis, or proposed fixes to agent-generated reports.
## Security reports
- AI-assisted security reports must meet the legitimacy and verification requirements in [SECURITY.md](SECURITY.md#ai-assisted-submissions). A human must verify a real Paperless-ngx code path, concrete reproduction steps, affected versions, and realistic security impact before submission.
- Account for existing permission checks, configuration requirements, and trust boundaries. Speculation, scanner output, or concerns about third-party software or deployment choices without a demonstrated Paperless-ngx vulnerability do not meet this threshold and may be closed without extended discussion.
- Disclose AI assistance and use the private vulnerability reporting channel specified in [SECURITY.md](SECURITY.md#reporting-a-vulnerability).
## Working on changes
- Read the relevant code, tests, and documentation before editing. Paperless-ngx is complex: trace interactions between affected components rather than assuming a change is isolated.
- Keep changes focused on the requested work and follow existing patterns. Avoid unrelated refactoring, formatting, dependency updates, or generated-file changes.
- Consider effects on permissions, document processing, background tasks, storage, and the API when those areas are involved.
- Add or update meaningful tests for behavior changes and regressions. Do not weaken tests or checks to make a change pass.
- Update user or developer documentation when behavior or setup changes.
- Keep credentials and private documents out of commits, fixtures, logs, and examples.
## Repository and validation
- `src/`: Django back end.
- `src-ui/`: Angular front end, managed with `pnpm`.
- `docs/`: Documentation.
- `docker/` and `scripts/`: Deployment and development tooling.
- Base functional changes on `dev`; documentation-only changes may target `main`.
Follow [the development documentation](docs/development.md) for environment setup and testing. Common checks after setup are:
- Back end: run `uv run pytest` from `src/` (use a test path to focus on the affected area).
- Front end: run `pnpm test` and `pnpm lint` from `src-ui/`.
- Formatting and other pre-commit checks: run `uv run prek run --files <changed-files>` from the repository root.
When handing work back, summarize the changes, the checks actually run and their results, and any remaining uncertainty. Clearly state when a check could not be run; do not claim verification that did not happen. The human contributor must understand and independently verify the contribution before submitting it.
+3 -16
View File
@@ -4,18 +4,6 @@ If you feel like contributing to the project, please do! Bug fixes and improveme
⚠️ Please note: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Pull requests that are opened without meeting this requirement may not be merged.
## Trust and collaboration
Paperless-ngx places a high value on trust between contributors and maintainers. If you have not contributed before, consider starting with a discussion before preparing a pull request, especially for a large contribution or one involving AI-generated code.
Paperless-ngx is complex, and understanding how its components interact takes time. A change that appears correct in isolation can have consequences elsewhere in the application. Contributors should understand the code they submit, explain how it fits into the project, and work with maintainers through review. Starting with smaller, focused contributions can help build that understanding and trust.
We prefer thoughtful, well-described ideas to AI-generated pull requests. You do not need to supply an implementation for your contribution to be valuable. Explain the problem you have encountered, your use case, the behavior you would like to see, and who would benefit. Concrete examples, alternatives you have considered, and relevant constraints help the community discuss and refine the idea.
AI tools have lowered the effort needed to produce a pull request, but evaluating one still requires substantial maintainer time. Maintainers still need to establish whether the implementation is correct, fits the project, and can be maintained over time.
A flawed implementation can also obscure a good idea: discussion becomes focused on problems with the generated code before the community has considered whether the underlying proposal is useful. Starting with the idea gives it a fair discussion on its own merits and lets contributors and maintainers agree on an approach before investing in implementation and review.
If you want to implement something big:
- As above, please start with a discussion! Maybe something similar is already in development and we can make it happen together.
@@ -73,10 +61,9 @@ This project does not specifically prohibit the use of AI tools or agents _durin
1. Any content — code, pull request descriptions, issue reports or comments — that was generated in whole or in part by an AI tool or agent **must be clearly disclosed as such**. Failing to clearly disclose this is considered a violation of our [Code of Conduct](CODE_OF_CONDUCT.md) and may result in your contributions being closed and further action being taken.
2. AI-generated code must not violate copyright protections.
3. We will not accept PRs opened or submitted by AI agents, including agents acting "on behalf of" a human. A human contributor must personally review, verify, and submit the contribution and take responsibility for it throughout review.
4. We will not accept PRs that are entirely or mostly AI-derived.
5. Issue reports must describe _observed behavior only_: what happened, how to reproduce it and the relevant logs. Reports generated by an AI agent should not include code analysis, speculation about the root cause or suggest fixes.
6. Contributors are responsible for everything they submit, regardless of how it was produced. Please do not open issues or PRs you have not verified yourself.
3. We will not accept PRs that are entirely or mostly AI-derived.
4. Issue reports must describe _observed behavior only_: what happened, how to reproduce it and the relevant logs. Reports generated by an AI agent should not include code analysis, speculation about the root cause or suggest fixes.
5. Contributors are responsible for everything they submit, regardless of how it was produced. Please do not open issues or PRs you have not verified yourself.
# Translating Paperless-ngx
+1 -1
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.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
+1 -4
View File
@@ -56,9 +56,6 @@ We may close reports that are:
The following are not generally considered vulnerabilities unless accompanied by a concrete, reproducible impact in Paperless-ngx:
- password length inferred from masked mail-account passwords by users who already have permission to view the account, without disclosure of the password or token itself
- disclosure limited to document IDs, existence, page counts, checksums, or modification/deletion timestamps, without access to document contents or an unintended privileged action
- authenticated access to non-secret application settings, such as service endpoint URLs, model names, or configured environment variable names, without disclosure of credentials or secret values
- large uploads or resource usage that do not bypass documented limits or privileges
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
@@ -69,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
- 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
- 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
## Transparency
+3 -3
View File
@@ -4,7 +4,7 @@
# correct networking for the tests
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.38
image: docker.io/gotenberg/gotenberg:8.37
hostname: gotenberg
container_name: gotenberg
network_mode: host
@@ -24,7 +24,7 @@ services:
network_mode: host
restart: unless-stopped
greenmail:
image: docker.io/greenmail/standalone:2.1.15
image: docker.io/greenmail/standalone:2.1.14
hostname: greenmail
container_name: greenmail
environment:
@@ -35,7 +35,7 @@ services:
- "3143:3143" # IMAP
restart: unless-stopped
nginx:
image: docker.io/nginx:1.31.7-alpine
image: docker.io/nginx:1.31.6-alpine
hostname: nginx
container_name: nginx
ports:
@@ -71,7 +71,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.38
image: docker.io/gotenberg/gotenberg:8.37
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
@@ -67,7 +67,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.38
image: docker.io/gotenberg/gotenberg:8.37
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
@@ -56,7 +56,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.38
image: docker.io/gotenberg/gotenberg:8.37
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
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}/drf_spectacular"
"${site_packages}/drf_spectacular_sidecar"
"${site_packages}/treenode"
"${site_packages}/compression_middleware"
)
@@ -10,11 +10,9 @@ if [[ -n "${PAPERLESS_CONSUMER_DISABLE}" ]]; then
else
cd ${PAPERLESS_SRC_DIR}
# Django system checks already ran once in init-system-checks, skip repeating them
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec python3 manage.py document_consumer --skip-checks
exec python3 manage.py document_consumer
else
exec s6-setuidgid paperless python3 manage.py document_consumer --skip-checks
exec s6-setuidgid paperless python3 manage.py document_consumer
fi
fi
@@ -3,9 +3,8 @@
cd ${PAPERLESS_SRC_DIR}
# Django system checks already ran once in init-system-checks, skip repeating them
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
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
@@ -3,9 +3,8 @@
cd ${PAPERLESS_SRC_DIR}
# Django system checks already ran once in init-system-checks, skip repeating them
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
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
-91
View File
@@ -1,96 +1,5 @@
# 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
### Features / Enhancements
+11 -10
View File
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "3.4.2"
version = "3.4.0"
description = """\
A community-supported supercharged document management system: scan, index and archive all your physical documents\
"""
@@ -26,21 +26,22 @@ dependencies = [
"dateparser~=1.4",
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.18",
"django-allauth[mfa,socialaccount]~=65.21.1",
"django~=5.2.13",
"django-allauth[mfa,socialaccount]~=65.20.0",
"django-auditlog~=3.4.1",
"django-cachalot~=2.9.0",
"django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0",
"django-extensions~=4.1",
"django-filter>=26.2,<27",
"django-guardian>=3.5.2,<3.6",
"django-filter>=25.1,<27",
"django-guardian>=3.3.3,<3.6",
"django-multiselectfield~=1.0.1",
"django-rich~=2.2.0",
"django-soft-delete~=1.0.18",
"django-treenode>=0.24",
"djangorestframework~=3.16",
"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",
"filelock>=3.32,<4.3",
"flower>=2.0.1,<2.3",
@@ -75,7 +76,7 @@ dependencies = [
"tantivy~=0.26.0",
"tika-client[httpx]~=1.0",
"torch>=2.13,<2.15",
"turbohtml~=1.15.3",
"turbohtml~=1.10.0",
"watchfiles>=1.2",
"whitenoise~=6.11",
"whoosh-compat[tantivy]==0.3",
@@ -111,14 +112,14 @@ lint = [
testing = [
"daphne",
"factory-boy~=3.3.1",
"faker>=40.36,<40.44",
"faker>=40.36,<40.40",
"imagehash",
"pytest~=9.1.1",
"pytest-cov~=7.1.0",
"pytest-django>=4.12,<4.15",
"pytest-env~=1.8.1",
"pytest-env~=1.7.0",
"pytest-httpx",
"pytest-mock~=3.17.0",
"pytest-mock~=3.15.1",
"pytest-randomly~=5.0.0",
"pytest-rerunfailures~=16.4",
"pytest-sugar",
-3
View File
@@ -69,9 +69,6 @@
"src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json",
"loader": {
".svg": "text"
},
"localize": true,
"assets": [
"src/apple-touch-icon.png",
+1 -9
View File
@@ -43,7 +43,6 @@ def configure_environment(instance_root: Path) -> None:
def seed_database() -> None:
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.core.management import call_command
from django.utils import timezone
@@ -64,18 +63,11 @@ def seed_database() -> None:
username="playwright",
password="playwright", # NOSONAR
)
viewer = User.objects.create_user(
User.objects.create_user(
username="viewer",
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)
quick_filter = Tag.objects.create(name="Another Sample Tag", 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 }) => {
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
.getByRole('button', { name: 'Filter documents with these Tags' })
.click()
await expect(page).toHaveURL((url) => {
const tags = url.searchParams.get('tags__id__all')?.split(',').sort()
return url.pathname === '/documents' && tags?.join(',') === '1,2'
})
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
await expect(page).toHaveURL(
/tags__id__all=2,1&sort=created&reverse=1&page=1/
)
})
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 expect(page).toHaveURL(/sort=num_notes/)
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/)
})
@@ -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 }) => {
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
await page.goto('/settings')
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 }) => {
await page.goto('/dashboard')
await expect(
page.locator('nav').getByRole('link', { name: 'Documents' })
).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 }) => {
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
).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 }) => {
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
).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 }) => {
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
).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 }) => {
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
).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 }) => {
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
await page.goto('/logs')
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 }) => {
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
await page.goto('/tasks')
await expect(page.locator('body')).toHaveText(
+1 -1
View File
@@ -28,7 +28,7 @@ module.exports = {
'abstract-paperless-service',
],
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: {
...esmPreset.moduleNameMapper,
+138 -158
View File
File diff suppressed because it is too large. Load diff
+25 -25
View File
@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "3.4.2",
"version": "3.4.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
@@ -15,60 +15,60 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^22.2.4",
"@angular/common": "~22.2.3",
"@angular/compiler": "~22.2.3",
"@angular/core": "~22.2.3",
"@angular/forms": "~22.2.3",
"@angular/localize": "~22.2.3",
"@angular/platform-browser": "~22.2.3",
"@angular/router": "~22.2.3",
"@angular/cdk": "^22.2.2",
"@angular/common": "~22.2.2",
"@angular/compiler": "~22.2.2",
"@angular/core": "~22.2.2",
"@angular/forms": "~22.2.2",
"@angular/localize": "~22.2.2",
"@angular/platform-browser": "~22.2.2",
"@angular/router": "~22.2.2",
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~24.2.1",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.2",
"file-saver": "^2.0.5",
"lodash-es": "^4.18.1",
"marked": "~18.1.1",
"marked": "~18.0.14",
"mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.1.0",
"ngx-cookie-service": "^22.0.0",
"ngx-device-detector": "^12.0.0",
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
"normalize-diacritics": "^5.0.0",
"pdfjs-dist": "^6.4.299",
"pdfjs-dist": "^6.3.289",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^14.0.3"
"uuid": "^14.0.2"
},
"devDependencies": {
"@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.2.4",
"@angular-devkit/schematics": "^22.2.4",
"@angular-devkit/core": "^22.2.2",
"@angular-devkit/schematics": "^22.2.2",
"@angular-eslint/builder": "22.6.0",
"@angular-eslint/eslint-plugin": "22.6.0",
"@angular-eslint/eslint-plugin-template": "22.6.0",
"@angular-eslint/schematics": "22.6.0",
"@angular-eslint/template-parser": "22.6.0",
"@angular/build": "22.2.4",
"@angular/cli": "22.2.4",
"@angular/compiler-cli": "~22.2.3",
"@playwright/test": "^1.64.1",
"@angular/build": "22.2.2",
"@angular/cli": "22.2.2",
"@angular/compiler-cli": "~22.2.2",
"@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0",
"@types/node": "^26.6.5",
"@typescript-eslint/eslint-plugin": "^8.72.0",
"@typescript-eslint/parser": "^8.72.0",
"@typescript-eslint/utils": "^8.72.0",
"eslint": "^10.13.0",
"@types/node": "^26.6.2",
"@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.70.1",
"@typescript-eslint/utils": "^8.70.1",
"eslint": "^10.11.0",
"jest": "30.5.2",
"jest-environment-jsdom": "^30.5.2",
"jest-junit": "^17.0.0",
"jest-preset-angular": "^17.0.3",
"jest-websocket-mock": "^2.5.0",
"prettier": "^3.9.10",
"prettier": "^3.9.9",
"prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.2",
"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',
component: DashboardComponent,
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 { Router, RouterModule } from '@angular/router'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
provideUiTour,
TourNgBootstrap,
@@ -46,6 +47,7 @@ describe('AppComponent', () => {
ToastsComponent,
FileDropComponent,
TourNgBootstrap,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [
PermissionsGuard,
@@ -28,7 +28,7 @@
{{option.title}}
</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">
<pngx-bs-icon name="info-circle"></pngx-bs-icon>
<i-bs name="info-circle"></i-bs>
</a>
@if (isExternallyConfigured(option.config_key)) {
@if (isSet(option.key)) {
@@ -40,11 +40,11 @@
@if (isSet(option.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)">
<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>
} @else {
<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>
}
}
@@ -6,6 +6,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import {
ConfigCategory,
@@ -38,6 +39,7 @@ describe('ConfigComponent', () => {
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
ConfigComponent,
TextComponent,
SelectComponent,
@@ -9,6 +9,7 @@ import {
} from '@angular/forms'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
BehaviorSubject,
Observable,
@@ -16,7 +17,6 @@ import {
first,
takeUntil,
} from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import {
ConfigCategory,
ConfigOption,
@@ -52,7 +52,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
NgbNavModule,
FormsModule,
ReactiveFormsModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class ConfigComponent
@@ -8,6 +8,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { BrowserModule, By } from '@angular/platform-browser'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
@@ -39,6 +40,7 @@ describe('LogsComponent', () => {
imports: [
BrowserModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
LogsComponent,
PageHeaderComponent,
CommonModule,
@@ -5,7 +5,7 @@
i18n-info
>
<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>
@if (canViewSystemStatus) {
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
@@ -13,14 +13,14 @@
@if (!systemStatus()) {
<div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div>
} @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) {
<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>
} @else {
<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>
}
}
@@ -30,7 +30,7 @@
@if (permissionsService.isAdmin()) {
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
<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>
}
</pngx-page-header>
@@ -151,7 +151,7 @@
</div>
<div class="col-2">
<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>
</div>
</div>
@@ -181,7 +181,7 @@
<div class="col d-flex flex-row align-items-start">
<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">
<pngx-bs-icon name="question-circle"></pngx-bs-icon>
<i-bs name="question-circle"></i-bs>
</button>
<ng-template #updatesPopover>
<p i18n>
@@ -263,9 +263,9 @@
<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>
<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">
<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>
</div>
</div>
@@ -15,6 +15,7 @@ import {
NgbNavLink,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
@@ -132,6 +133,7 @@ describe('SettingsComponent', () => {
ReactiveFormsModule,
NgbAlertModule,
NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
NgbModalModule,
DragDropModule,
SettingsComponent,
@@ -23,6 +23,7 @@ import {
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import {
BehaviorSubject,
@@ -33,7 +34,6 @@ import {
takeUntil,
tap,
} from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Group } from 'src/app/data/group'
import {
SystemStatus,
@@ -135,7 +135,7 @@ const sidebarItemLabels: Record<HideableSidebarItemID, string> = {
ReactiveFormsModule,
NgbNavModule,
NgbPopoverModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class SettingsComponent
@@ -7,13 +7,13 @@
>
<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">
<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 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 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>
<div class="form-check form-switch mb-0 ms-2">
<input id="autoRefreshSwitch" class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
@@ -86,7 +86,7 @@
</div>
@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>
<pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon>
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
<label class="visually-hidden" for="taskSearch" i18n>Search tasks</label>
@@ -98,7 +98,7 @@
@if (isFiltered) {
<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>
}
@@ -125,7 +125,7 @@
@if (hasMore) {
<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)">
<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>
} @else {
<ng-container i18n>{count, plural, =1 {1 task} other {{{count}} tasks}}</ng-container>
@@ -180,7 +180,7 @@
@if (taskShowsSeparateTypeLabel(task)) {
<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>
</td>
<td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td>
@@ -197,7 +197,7 @@
}
@if (duplicateDocumentId(task)) {
<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>
</div>
}
@@ -213,18 +213,18 @@
}
<td class="d-lg-none">
<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>
</td>
<td class="actions-col">
<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 }">
<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>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document_ids?.[0]) {
<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>
}
</ng-container>
@@ -10,6 +10,7 @@ import { By } from '@angular/platform-browser'
import { Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import {
@@ -173,6 +174,7 @@ describe('TasksComponent', () => {
imports: [
NgbModule,
RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
FormsModule,
TasksComponent,
PageHeaderComponent,
@@ -9,6 +9,7 @@ import {
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
debounceTime,
distinctUntilChanged,
@@ -21,7 +22,6 @@ import {
takeUntil,
timer,
} from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import {
PaperlessTask,
PaperlessTaskStatus,
@@ -151,7 +151,7 @@ const TRIGGER_SOURCE_OPTIONS: Array<{
NgbDropdownModule,
NgbPaginationModule,
NgbPopoverModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class TasksComponent
@@ -5,16 +5,16 @@
i18n-info
infoLink="usage/#document-trash">
<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 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 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 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>
</pngx-page-header>
@@ -59,7 +59,7 @@
<td scope="row">
{{ document.title }}
<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>
</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 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>
<pngx-bs-icon name="three-dots-vertical"></pngx-bs-icon>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="restore(document)" ngbDropdownItem i18n>Restore</button>
@@ -77,10 +77,10 @@
</div>
<div class="btn-group d-none d-sm-block">
<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 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>
</div>
</td>
@@ -9,6 +9,7 @@ import {
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
@@ -47,6 +48,7 @@ describe('TrashComponent', () => {
ReactiveFormsModule,
NgbPopoverModule,
NgbPaginationModule,
NgxBootstrapIconsModule.pick(allIcons),
TrashComponent,
PageHeaderComponent,
ConfirmDialogComponent,
@@ -6,8 +6,8 @@ import {
NgbModal,
NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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 { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service'
@@ -29,7 +29,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
ReactiveFormsModule,
NgbDropdownModule,
NgbPaginationModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class TrashComponent
@@ -11,7 +11,7 @@
<h4 class="d-flex">
<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 }">
<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>
</h4>
<ul class="list-group">
@@ -32,10 +32,10 @@
<div class="col">
<div class="btn-group">
<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 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>
</div>
</div>
@@ -49,7 +49,7 @@
<h4 class="mt-4 d-flex">
<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 }">
<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>
</h4>
<ul class="list-group">
@@ -70,10 +70,10 @@
<div class="col">
<div class="btn-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 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>
</div>
</div>
@@ -3,6 +3,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
@@ -40,6 +41,7 @@ describe('UsersAndGroupsComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NgxBootstrapIconsModule.pick(allIcons)],
providers: [
CustomDatePipe,
DatePipe,
@@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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 { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -29,7 +29,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
imports: [
PageHeaderComponent,
IfPermissionsDirective,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class UsersAndGroupsComponent
@@ -2,7 +2,7 @@
<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"
(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>
<div class="navbar-brand-container order-sm-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">
<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>
<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>
@if (uploadsInProgress > 0) {
<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>
<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>
<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">
{{this.settingsService.displayName}}
</span>
@@ -63,19 +63,19 @@
<div class="dropdown-divider"></div>
</div>
<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>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
*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 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>
<div class="dropdown-divider"></div>
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
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>
</div>
</li>
@@ -93,9 +93,9 @@
@if (canSaveSettings) {
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()" [aria-label]="slimSidebarEnabled ? 'Expand sidebar' : 'Collapse sidebar'" i18n-aria-label>
@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 {
<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>
}
@@ -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()"
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
@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>
@@ -117,7 +117,7 @@
(click)="closeMenu()"
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
</li>
</ul>
@@ -136,7 +136,7 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
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) {
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
}
@@ -147,7 +147,7 @@
</a>
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
<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>
}
</li>
@@ -174,10 +174,10 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
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"
(click)="$event.preventDefault(); $event.stopPropagation(); closeDocument(d)">
<pngx-bs-icon name="x"></pngx-bs-icon>
<i-bs name="x"></i-bs>
</span>
</a>
</li>
@@ -188,7 +188,7 @@
[class.text-truncate]="!slimSidebarEnabled" (click)="closeAll()"
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
</li>
}
@@ -207,7 +207,7 @@
[routerLinkActiveOptions]="{ exact: !(slimSidebarEnabled || attributesSectionsCollapsed) }" (click)="closeMenu()"
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
@if (!slimSidebarEnabled && canSaveSettings) {
<button
@@ -217,7 +217,7 @@
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
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>
}
</div>
@@ -228,27 +228,27 @@
<ul class="nav flex-column">
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
<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>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<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>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<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>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<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>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<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>
</li>
</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()"
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
@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>
@@ -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()"
ngbPopover="Share links" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
@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>
@@ -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()"
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
@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>
@@ -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"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
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>
@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>
@@ -304,7 +304,7 @@
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
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>
</li>
</ul>
@@ -320,21 +320,21 @@
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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>
</li>
<li class="nav-item app-link"
@@ -343,7 +343,7 @@
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
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) {
<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"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
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>
</li>
}
@@ -366,7 +366,7 @@
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
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>
@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>
@@ -408,7 +408,7 @@
href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
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) {
<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
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
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>
}
</div>
@@ -142,7 +142,7 @@ main {
padding-left: 0;
padding-right: 0;
pngx-bs-icon {
i-bs {
margin-right: 0 !important;
}
}
@@ -268,7 +268,7 @@ main {
}
}
pngx-bs-icon {
i-bs {
position: relative;
top: -1px;
}
@@ -279,7 +279,7 @@ main {
align-items: center;
min-width: 0;
> pngx-bs-icon {
> i-bs {
flex: 0 0 auto;
}
@@ -373,11 +373,11 @@ main {
width: 1.8rem;
height: 100%;
pngx-bs-icon {
i-bs {
opacity: 0.5;
}
&:hover pngx-bs-icon {
&:hover i-bs {
opacity: 1;
}
}
@@ -392,7 +392,7 @@ main {
text-decoration: underline;
}
pngx-bs-icon {
i-bs {
margin-bottom: 2px;
}
}
@@ -665,7 +665,7 @@ main {
.navbar .dropdown-menu {
font-size: 0.875rem; // body size
a pngx-bs-icon, button pngx-bs-icon {
a i-bs, button i-bs {
opacity: 0.6;
}
}
@@ -693,7 +693,7 @@ main {
cursor: move;
}
::ng-deep .navItemDrag .position-absolute pngx-bs-icon {
::ng-deep .navItemDrag .position-absolute i-bs {
display: none;
}
@@ -10,6 +10,7 @@ import { BrowserModule, By } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
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 { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
@@ -107,6 +108,7 @@ describe('AppFrameComponent', () => {
ReactiveFormsModule,
DragDropModule,
NgbModalModule,
NgxBootstrapIconsModule.pick(allIcons),
AppFrameComponent,
IfPermissionsDirective,
GlobalSearchComponent,
@@ -23,10 +23,10 @@ import {
NgbNavModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
import { Observable } from 'rxjs'
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 { SavedView } from 'src/app/data/saved-view'
import {
@@ -89,7 +89,7 @@ const SCROLL_THRESHOLD = 16
NgbPopoverModule,
NgbCollapseModule,
NgbNavModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
DragDropModule,
TourNgBootstrap,
FormsModule,
@@ -1,7 +1,7 @@
<div ngbDropdown #resultsDropdown="ngbDropdown" (openChange)="onDropdownOpenChange">
<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="form-control form-control-sm">
<input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query"
@@ -25,7 +25,7 @@
} @else {
<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>
}
</div>
@@ -35,7 +35,7 @@
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1"
(click)="primaryAction(type, item, $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">
{{item[nameProp]}}
@if (date) {
@@ -49,13 +49,13 @@
[disabled]="disablePrimaryButton(type, item)"
(mouseenter)="onButtonHover($event)">
@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) {
<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) {
<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 {
<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>
@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)"
(mouseenter)="onButtonHover($event)">
@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 {
<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>
}
@@ -1,7 +1,7 @@
form {
position: relative;
> pngx-bs-icon[name="search"] {
> i-bs[name="search"] {
position: absolute;
left: 0.6rem;
top: .25rem;
@@ -14,7 +14,7 @@ form {
}
&:focus-within {
pngx-bs-icon[name="search"],
i-bs[name="search"],
.badge {
display: none !important;
}
@@ -72,7 +72,7 @@ form {
.dropdown.show {
// Hide the search icon when the dropdown is shown
pngx-bs-icon[name="search"] {
i-bs[name="search"] {
display: none !important;
}
@@ -10,6 +10,7 @@ import {
NgbModalModule,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DataType } from 'src/app/data/datatype'
import {
@@ -137,6 +138,7 @@ describe('GlobalSearchComponent', () => {
NgbDropdownModule,
FormsModule,
ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
GlobalSearchComponent,
],
providers: [
@@ -17,8 +17,8 @@ import {
NgbModal,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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 {
FILTER_FULLTEXT_QUERY,
@@ -65,7 +65,7 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e
CustomDatePipe,
FormsModule,
ReactiveFormsModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
NgbDropdownModule,
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>
}
<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>
<div ngbDropdownMenu class="dropdown-menu-end shadow p-2" aria-labelledby="notificationsDropdown">
<div class="d-flex align-items-center px-1 mb-2">
@@ -12,13 +12,13 @@
@if (toasts().length) {
<button class="btn btn-link btn-sm p-0 ms-auto text-body-secondary text-decoration-none clear-all"
(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>
}
</div>
@if (toasts().length === 0) {
<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>
</div>
}
@@ -1,6 +1,7 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
import { ToastsDropdownComponent } from './toasts-dropdown.component'
@@ -40,7 +41,10 @@ describe('ToastsDropdownComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [ToastsDropdownComponent],
imports: [
ToastsDropdownComponent,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@@ -12,8 +12,8 @@ import {
NgbDropdownModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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 { ToastComponent } from '../../common/toast/toast.component'
@@ -25,7 +25,7 @@ import { ToastComponent } from '../../common/toast/toast.component'
ToastComponent,
NgbDropdownModule,
NgbProgressbarModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class ToastsDropdownComponent {
@@ -1,11 +1,11 @@
<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>
<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>
<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">
<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">
@if (documentId()) {
<ng-container i18n>Ask about this document</ng-container>
@@ -15,7 +15,7 @@
</span>
@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>
<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>
}
</div>
@@ -46,7 +46,7 @@
<div class="list-group-item text-muted small py-1" i18n>Sources</div>
@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">
<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>
}
</div>
@@ -72,11 +72,11 @@
/>
@if (loading()) {
<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>
} @else {
<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>
}
</div>
@@ -4,6 +4,7 @@ import { ElementRef } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NavigationEnd, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import {
CHAT_METADATA_DELIMITER,
@@ -21,7 +22,11 @@ describe('ChatComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, ChatComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
RouterTestingModule,
ChatComponent,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@@ -9,8 +9,8 @@ import {
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NavigationEnd, Router, RouterModule } from '@angular/router'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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 {
ChatMessage,
@@ -27,7 +27,7 @@ const SLOW_RESPONSE_DELAY_MS = 8000
FormsModule,
ReactiveFormsModule,
RouterModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
NgbDropdownModule,
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,317 +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 playFill from 'bootstrap-icons/icons/play-fill.svg'
import plusCircle from 'bootstrap-icons/icons/plus-circle.svg'
import plus from 'bootstrap-icons/icons/plus.svg'
import printer from 'bootstrap-icons/icons/printer.svg'
import questionCircle from 'bootstrap-icons/icons/question-circle.svg'
import receipt from 'bootstrap-icons/icons/receipt.svg'
import safe from 'bootstrap-icons/icons/safe.svg'
import scissors from 'bootstrap-icons/icons/scissors.svg'
import search from 'bootstrap-icons/icons/search.svg'
import send from 'bootstrap-icons/icons/send.svg'
import shop from 'bootstrap-icons/icons/shop.svg'
import slashCircle from 'bootstrap-icons/icons/slash-circle.svg'
import sliders2Vertical from 'bootstrap-icons/icons/sliders2-vertical.svg'
import sortAlphaDown from 'bootstrap-icons/icons/sort-alpha-down.svg'
import sortAlphaUpAlt from 'bootstrap-icons/icons/sort-alpha-up-alt.svg'
import stack from 'bootstrap-icons/icons/stack.svg'
import stars from 'bootstrap-icons/icons/stars.svg'
import tagFill from 'bootstrap-icons/icons/tag-fill.svg'
import tag from 'bootstrap-icons/icons/tag.svg'
import tags from 'bootstrap-icons/icons/tags.svg'
import telephone from 'bootstrap-icons/icons/telephone.svg'
import textIndentLeft from 'bootstrap-icons/icons/text-indent-left.svg'
import textLeft from 'bootstrap-icons/icons/text-left.svg'
import threeDotsVertical from 'bootstrap-icons/icons/three-dots-vertical.svg'
import threeDots from 'bootstrap-icons/icons/three-dots.svg'
import trash from 'bootstrap-icons/icons/trash.svg'
import truck from 'bootstrap-icons/icons/truck.svg'
import uiRadios from 'bootstrap-icons/icons/ui-radios.svg'
import unlock from 'bootstrap-icons/icons/unlock.svg'
import upcScan from 'bootstrap-icons/icons/upc-scan.svg'
import wallet2 from 'bootstrap-icons/icons/wallet2.svg'
import windowStack from 'bootstrap-icons/icons/window-stack.svg'
import xCircle from 'bootstrap-icons/icons/x-circle.svg'
import xLg from 'bootstrap-icons/icons/x-lg.svg'
import x from 'bootstrap-icons/icons/x.svg'
/**
* `<pngx-bs-icon>`. Only icons registered here are bundled but can add
* new ones (any icon from the bootstrap-icons package) as needed.
*/
export const icons: Record<string, string> = {
airplane,
archive,
arrowClockwise,
arrowCounterclockwise,
arrowDown,
arrowDownUp,
arrowLeft,
arrowRepeat,
arrowReturnLeft,
arrowRight,
arrowRightShort,
arrowUpRight,
asterisk,
bank,
basket,
bell,
braces,
bodyText,
bookmark,
boxArrowInRight,
boxArrowUp,
boxArrowUpRight,
boxes,
briefcase,
building,
calculator,
calendar,
calendarEvent,
calendarEventFill,
camera,
cardChecklist,
cardHeading,
caretDown,
caretUp,
chatLeftText,
chatSquareDots,
check,
check2All,
checkAll,
checkCircle,
checkCircleFill,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
chevronDown,
chevronRight,
chevronUp,
circle,
clipboard,
clipboardCheck,
clipboardCheckFill,
clipboardFill,
clockHistory,
cpu,
cash,
creditCard,
dash,
dashCircle,
diagram3,
dice5,
doorOpen,
download,
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
eye,
fileEarmark,
fileEarmarkArrowUp,
fileEarmarkCheck,
fileEarmarkDiff,
fileEarmarkFill,
fileEarmarkLock,
fileEarmarkMedical,
fileEarmarkMinus,
fileEarmarkPerson,
fileEarmarkPlus,
fileEarmarkRichtext,
fileEarmarkSpreadsheet,
files,
fileText,
filter,
floppy,
folder,
folderFill,
funnel,
gear,
globe2,
google,
grid,
gripVertical,
hash,
hddStack,
heart,
house,
inbox,
infoCircle,
journalBookmarkFill,
journals,
lightbulb,
link,
list,
listNested,
listTask,
listUl,
microsoft,
newspaper,
nodePlus,
pencil,
paperclip,
people,
peopleFill,
person,
personCircle,
personFill,
personFillLock,
personLock,
personSquare,
playFill,
plus,
plusCircle,
printer,
questionCircle,
receipt,
safe,
scissors,
search,
send,
slashCircle,
shop,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
stack,
stars,
tagFill,
tag,
tags,
textIndentLeft,
textLeft,
telephone,
threeDots,
threeDotsVertical,
trash,
truck,
uiRadios,
unlock,
upcScan,
wallet2,
windowStack,
x,
xCircle,
xLg,
}
@@ -1,11 +1,11 @@
@if (active()) {
<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()) {
<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()) {
<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>
}
@@ -1,4 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ClearableBadgeComponent } from './clearable-badge.component'
describe('ClearableBadgeComponent', () => {
@@ -7,7 +8,10 @@ describe('ClearableBadgeComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [ClearableBadgeComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
ClearableBadgeComponent,
],
}).compileComponents()
fixture = TestBed.createComponent(ClearableBadgeComponent)
@@ -1,11 +1,11 @@
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({
selector: 'pngx-clearable-badge',
templateUrl: './clearable-badge.component.html',
styleUrls: ['./clearable-badge.component.scss'],
imports: [BootstrapIconComponent],
imports: [NgxBootstrapIconsModule],
})
export class ClearableBadgeComponent {
readonly number = input<number>(undefined)
@@ -10,7 +10,7 @@
popoverClass="popover-slim"
>
@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>
</button>
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './confirm-button.component'
describe('ConfirmButtonComponent', () => {
@@ -9,7 +10,11 @@ describe('ConfirmButtonComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgbPopoverModule, ConfirmButtonComponent],
imports: [
NgbPopoverModule,
NgxBootstrapIconsModule.pick(allIcons),
ConfirmButtonComponent,
],
}).compileComponents()
fixture = TestBed.createComponent(ConfirmButtonComponent)
@@ -6,13 +6,13 @@ import {
ViewChild,
} from '@angular/core'
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({
selector: 'pngx-confirm-button',
templateUrl: './confirm-button.component.html',
styleUrl: './confirm-button.component.scss',
imports: [NgbPopoverModule, BootstrapIconComponent],
imports: [NgbPopoverModule, NgxBootstrapIconsModule],
})
export class ConfirmButtonComponent {
@Input()
@@ -22,7 +22,7 @@
@let document = getDocument(documentID);
@if (document) {
<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>
@if (document.correspondent) {
@@ -2,6 +2,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { MergeAsVersionsConfirmDialogComponent } from './merge-as-versions-confirm-dialog.component'
@@ -13,7 +14,10 @@ describe('MergeAsVersionsConfirmDialogComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MergeAsVersionsConfirmDialogComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
MergeAsVersionsConfirmDialogComponent,
],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
@@ -6,8 +6,8 @@ import {
import { AsyncPipe } from '@angular/common'
import { Component, OnInit, computed, inject, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Document } from 'src/app/data/document'
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -24,7 +24,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
CustomDatePipe,
DragDropModule,
FormsModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class MergeAsVersionsConfirmDialogComponent
@@ -15,7 +15,7 @@
@let document = getDocument(documentID);
@if (document) {
<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>
@if (document.correspondent) {
@@ -3,6 +3,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
@@ -14,7 +15,12 @@ describe('MergeConfirmDialogComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, FormsModule, MergeConfirmDialogComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule,
FormsModule,
MergeConfirmDialogComponent,
],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
@@ -6,8 +6,8 @@ import {
import { AsyncPipe } from '@angular/common'
import { Component, OnInit, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Document } from 'src/app/data/document'
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -26,7 +26,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
DragDropModule,
FormsModule,
ReactiveFormsModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
],
})
export class MergeConfirmDialogComponent
@@ -18,7 +18,7 @@
(ngModelChange)="onUpdateDocumentChange($event)"
/>
<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>
</label>
<input
@@ -31,7 +31,7 @@
(ngModelChange)="onUpdateDocumentChange($event)"
/>
<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>
</label>
</div>
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component'
describe('PasswordRemovalConfirmDialogComponent', () => {
@@ -10,7 +11,10 @@ describe('PasswordRemovalConfirmDialogComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [NgbActiveModal],
imports: [PasswordRemovalConfirmDialogComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
PasswordRemovalConfirmDialogComponent,
],
}).compileComponents()
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
@@ -1,13 +1,13 @@
import { Component } from '@angular/core'
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'
@Component({
selector: 'pngx-password-removal-confirm-dialog',
templateUrl: './password-removal-confirm-dialog.component.html',
styleUrls: ['./password-removal-confirm-dialog.component.scss'],
imports: [FormsModule, BootstrapIconComponent],
imports: [FormsModule, NgxBootstrapIconsModule],
})
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
updateDocument: boolean = true
@@ -7,7 +7,7 @@
<div class="row">
<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>
<pngx-bs-icon name="arrow-counterclockwise"></pngx-bs-icon>
<i-bs name="arrow-counterclockwise"></i-bs>
</button>
</div>
<div class="col-8 d-flex align-items-center">
@@ -17,7 +17,7 @@
</div>
<div class="col-2 d-flex">
<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>
</div>
</div>
@@ -2,6 +2,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
describe('RotateConfirmDialogComponent', () => {
@@ -10,7 +11,10 @@ describe('RotateConfirmDialogComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RotateConfirmDialogComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
RotateConfirmDialogComponent,
],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
@@ -1,6 +1,6 @@
import { NgStyle } from '@angular/common'
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 { ConfirmDialogComponent } from '../confirm-dialog.component'
@@ -8,7 +8,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
selector: 'pngx-rotate-confirm-dialog',
templateUrl: './rotate-confirm-dialog.component.html',
styleUrl: './rotate-confirm-dialog.component.scss',
imports: [NgStyle, BootstrapIconComponent],
imports: [NgStyle, NgxBootstrapIconsModule],
})
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
documentService = inject(DocumentService)
@@ -20,7 +20,7 @@
@for (docId of value; track docId) {
@if (getDocumentTitle(docId)) {
<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>
}
}
@@ -1,6 +1,6 @@
<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>
<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>
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
@@ -11,12 +11,14 @@
</div>
@for (field of filteredFields; track field.id) {
<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>
}
@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>
<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>
}
</div>
@@ -1,3 +1,10 @@
.custom-fields-dropdown {
min-width: 300px;
// correct position on mobile
@media (max-width: 575.98px) {
&.show {
margin-left: -245px !important;
}
}
}
@@ -10,6 +10,7 @@ import {
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
@@ -48,6 +49,7 @@ describe('CustomFieldsDropdownComponent', () => {
ReactiveFormsModule,
NgbModalModule,
NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons),
CustomFieldsDropdownComponent,
SelectComponent,
],
@@ -12,8 +12,8 @@ import {
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
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 { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import {
@@ -34,7 +34,7 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
styleUrls: ['./custom-fields-dropdown.component.scss'],
imports: [
NgbDropdownModule,
BootstrapIconComponent,
NgxBootstrapIconsModule,
FormsModule,
ReactiveFormsModule,
],
@@ -1,7 +1,7 @@
@if (useDropdown) {
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
<button class="btn btn-sm" [ngClass]="!editing && isActive ? 'btn-primary' : 'btn-outline-primary'" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled" [aria-label]="title">
<pngx-bs-icon name="{{icon}}"></pngx-bs-icon><div class="d-none d-sm-inline ms-1">{{title}}</div>
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
@if (isActive) {
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
}
@@ -38,11 +38,11 @@
<fieldset class="btn-group btn-group-sm flex-shrink-0 date-mode" aria-label="Date mode" i18n-aria-label>
<input type="radio" class="btn-check" [id]="atom.id + '_fixed'" [name]="atom.id + '_dateMode'" [checked]="!relativeDate" (change)="toggleRelativeDate(atom)" [disabled]="disabled" aria-label="Fixed date" i18n-aria-label>
<label class="btn btn-outline-secondary text-nowrap" [for]="atom.id + '_fixed'" title="Fixed date" i18n-title>
<pngx-bs-icon name="calendar"></pngx-bs-icon><span class="d-none d-md-inline ms-2" i18n>Fixed</span>
<i-bs name="calendar"></i-bs><span class="d-none d-md-inline ms-2" i18n>Fixed</span>
</label>
<input type="radio" class="btn-check" [id]="atom.id + '_relative'" [name]="atom.id + '_dateMode'" [checked]="!!relativeDate" (change)="toggleRelativeDate(atom)" [disabled]="disabled" aria-label="Relative date" i18n-aria-label>
<label class="btn btn-outline-secondary text-nowrap" [for]="atom.id + '_relative'" title="Relative date" i18n-title>
<pngx-bs-icon name="clock-history"></pngx-bs-icon><span class="d-none d-md-inline ms-2" i18n>Relative</span>
<i-bs name="clock-history"></i-bs><span class="d-none d-md-inline ms-2" i18n>Relative</span>
</label>
</fieldset>
<div class="input-group input-group-sm flex-grow-1">
@@ -70,7 +70,7 @@
#d="ngbDatepicker"
[footerTemplate]="datePickerFooterTemplate" />
<button class="btn btn-sm btn-outline-secondary" (click)="d.toggle()" type="button" aria-label="Open date picker" i18n-aria-label>
<pngx-bs-icon name="calendar-event"></pngx-bs-icon>
<i-bs name="calendar-event"></i-bs>
</button>
<ng-template #datePickerFooterTemplate>
<div class="btn-group-xs border-top p-2 d-flex">
@@ -187,7 +187,7 @@
}
</div>
<button class="btn btn-link btn-sm text-danger pe-0 align-self-center" type="button" (click)="removeElement(atom)" [disabled]="disabled" aria-label="Remove query" i18n-aria-label>
<pngx-bs-icon name="x-circle"></pngx-bs-icon>
<i-bs name="x-circle"></i-bs>
</button>
</ng-template>
@@ -221,14 +221,14 @@
</div>
<div class="btn-group-vertical ms-2 ps-2 border-start" role="group" aria-label="Vertical button group">
<button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add query" i18n-title (click)="addAtom(expression)" [disabled]="disabled || expression.value.length === CUSTOM_FIELD_QUERY_MAX_ATOMS">
<pngx-bs-icon name="node-plus"></pngx-bs-icon>
<i-bs name="node-plus"></i-bs>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add expression" i18n-title (click)="addExpression(expression)" [disabled]="disabled || expression.depth === CUSTOM_FIELD_QUERY_MAX_DEPTH">
<pngx-bs-icon name="braces"></pngx-bs-icon>
<i-bs name="braces"></i-bs>
</button>
@if (expression.depth > 0) {
<button type="button" class="btn btn-sm btn-outline-secondary text-danger" (click)="removeElement(expression)" [disabled]="disabled" aria-label="Remove expression" i18n-aria-label>
<pngx-bs-icon name="x-circle"></pngx-bs-icon>
<i-bs name="x-circle"></i-bs>
</button>
}
</div>
Loaded 100 of 449 files, more files were not shown because too many files have changed in this diff. Show more