mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-09 23:49:29 -06:00
Compare commits
58 Commits
copilot/su
...
feature-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6758bba0c7 | ||
|
|
e3d3feca23 | ||
|
|
1f64f6caff | ||
|
|
8d694388cd | ||
|
|
e29a393743 | ||
|
|
9b0e4756b3 | ||
|
|
8ebbb6eea8 | ||
|
|
f829a89770 | ||
|
|
4d7a4e3e62 | ||
|
|
3672ab3b13 | ||
|
|
87872a377c | ||
|
|
bc26380025 | ||
|
|
2727bbc716 | ||
|
|
be8b027f53 | ||
|
|
64cfae1fcd | ||
|
|
83b02cd40a | ||
|
|
2caf2ec5be | ||
|
|
c634bb4a02 | ||
|
|
75c8d53293 | ||
|
|
79b44e1850 | ||
|
|
4e0ec9ca0b | ||
|
|
6dfb919421 | ||
|
|
145c11394b | ||
|
|
9d7231d2dc | ||
|
|
4208d9255a | ||
|
|
9e9e55758f | ||
|
|
6a87c3f4dd | ||
|
|
d7c64760ed | ||
|
|
750c77736b | ||
|
|
30b1d3c6d7 | ||
|
|
d3ff856202 | ||
|
|
3bc4631a0f | ||
|
|
ab328e0212 | ||
|
|
5c3d02e6d4 | ||
|
|
1d89ec402b | ||
|
|
6192915be7 | ||
|
|
b9b90ec9f7 | ||
|
|
0dc58cf686 | ||
|
|
505ff31748 | ||
|
|
3c51b3f9cd | ||
|
|
dfbac35f9c | ||
|
|
8f917555b1 | ||
|
|
734b5b9a45 | ||
|
|
0f1cae03ec | ||
|
|
71663fdbe2 | ||
|
|
1188a89369 | ||
|
|
b8e3b6590e | ||
|
|
4a5116adf8 | ||
|
|
bbf2e63f10 | ||
|
|
33cbe2ad54 | ||
|
|
261e10ebeb | ||
|
|
585c28b460 | ||
|
|
e77ab3357c | ||
|
|
05ab091ea4 | ||
|
|
fb7abf7a6e | ||
|
|
6ad2fc0356 | ||
|
|
2ec8ec96c8 | ||
|
|
276dc13e3f |
@@ -91,12 +91,12 @@ Additional tasks are available for common maintenance operations:
|
|||||||
|
|
||||||
## Committing from the Host Machine
|
## Committing from the Host Machine
|
||||||
|
|
||||||
The DevContainer automatically installs pre-commit hooks during setup. However, these hooks are configured for use inside the container.
|
The DevContainer automatically installs Git pre-commit hooks during setup. However, these hooks are configured for use inside the container.
|
||||||
|
|
||||||
If you want to commit changes from your host machine (outside the DevContainer), you need to set up pre-commit on your host. This installs it as a standalone tool.
|
If you want to commit changes from your host machine (outside the DevContainer), you need to set up prek on your host. This installs it as a standalone tool.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv tool install pre-commit && pre-commit install
|
uv tool install prek && prek install
|
||||||
```
|
```
|
||||||
|
|
||||||
After this, you can commit either from inside the DevContainer or from your host machine.
|
After this, you can commit either from inside the DevContainer or from your host machine.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache"
|
"UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache"
|
||||||
},
|
},
|
||||||
"postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'",
|
"postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run prek install'",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
@@ -116,9 +116,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Maintenance: Build Documentation",
|
"label": "Maintenance: Build Documentation",
|
||||||
"description": "Build the documentation with MkDocs",
|
"description": "Build the documentation with Zensical",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "uv run mkdocs build --config-file mkdocs.yml && uv run mkdocs serve",
|
"command": "uv run zensical build && uv run zensical serve",
|
||||||
"group": "none",
|
"group": "none",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"echo": true,
|
"echo": true,
|
||||||
|
|||||||
@@ -28,3 +28,4 @@
|
|||||||
./resources
|
./resources
|
||||||
# Other stuff
|
# Other stuff
|
||||||
**/*.drawio.png
|
**/*.drawio.png
|
||||||
|
.mypy_baseline
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -37,6 +37,6 @@ NOTE: PRs that do not address the following will not be merged, please do not sk
|
|||||||
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
||||||
- [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
- [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||||
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
||||||
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
- [ ] I have run all Git `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
||||||
- [ ] I have made corresponding changes to the documentation as needed.
|
- [ ] I have made corresponding changes to the documentation as needed.
|
||||||
- [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR.
|
- [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR.
|
||||||
|
|||||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -46,8 +46,8 @@ updates:
|
|||||||
patterns:
|
patterns:
|
||||||
- "*pytest*"
|
- "*pytest*"
|
||||||
- "ruff"
|
- "ruff"
|
||||||
- "mkdocs-material"
|
- "zensical"
|
||||||
- "pre-commit*"
|
- "prek*"
|
||||||
# Django & DRF Ecosystem
|
# Django & DRF Ecosystem
|
||||||
django-ecosystem:
|
django-ecosystem:
|
||||||
patterns:
|
patterns:
|
||||||
|
|||||||
49
.github/workflows/ci-backend.yml
vendored
49
.github/workflows/ci-backend.yml
vendored
@@ -99,3 +99,52 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker compose --file docker/compose/docker-compose.ci-test.yml logs
|
docker compose --file docker/compose/docker-compose.ci-test.yml logs
|
||||||
docker compose --file docker/compose/docker-compose.ci-test.yml down
|
docker compose --file docker/compose/docker-compose.ci-test.yml down
|
||||||
|
typing:
|
||||||
|
name: Check project typing
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
DEFAULT_PYTHON: "3.12"
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6.0.1
|
||||||
|
- name: Set up Python
|
||||||
|
id: setup-python
|
||||||
|
uses: actions/setup-python@v6.2.0
|
||||||
|
with:
|
||||||
|
python-version: "${{ env.DEFAULT_PYTHON }}"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v7.2.1
|
||||||
|
with:
|
||||||
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
|
enable-cache: true
|
||||||
|
python-version: ${{ steps.setup-python.outputs.python-version }}
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
uv sync \
|
||||||
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
|
--group testing \
|
||||||
|
--group typing \
|
||||||
|
--frozen
|
||||||
|
- name: List installed Python dependencies
|
||||||
|
run: |
|
||||||
|
uv pip list
|
||||||
|
- name: Check typing (pyrefly)
|
||||||
|
run: |
|
||||||
|
uv run pyrefly \
|
||||||
|
check \
|
||||||
|
src/
|
||||||
|
- name: Cache Mypy
|
||||||
|
uses: actions/cache@v5.0.3
|
||||||
|
with:
|
||||||
|
path: .mypy_cache
|
||||||
|
# Keyed by OS, Python version, and dependency hashes
|
||||||
|
key: ${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-${{ hashFiles('pyproject.toml', 'uv.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
||||||
|
${{ runner.os }}-mypy-
|
||||||
|
- name: Check typing (mypy)
|
||||||
|
run: |
|
||||||
|
uv run mypy \
|
||||||
|
--show-error-codes \
|
||||||
|
--warn-unused-configs \
|
||||||
|
src/ | uv run mypy-baseline filter
|
||||||
|
|||||||
56
.github/workflows/ci-docs.yml
vendored
56
.github/workflows/ci-docs.yml
vendored
@@ -6,17 +6,25 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'mkdocs.yml'
|
- 'zensical.toml'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
- '.github/workflows/ci-docs.yml'
|
- '.github/workflows/ci-docs.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'mkdocs.yml'
|
- 'zensical.toml'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
- '.github/workflows/ci-docs.yml'
|
- '.github/workflows/ci-docs.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: docs-${{ github.event.pull_request.number || github.ref }}
|
group: docs-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.9.x"
|
DEFAULT_UV_VERSION: "0.9.x"
|
||||||
DEFAULT_PYTHON_VERSION: "3.11"
|
DEFAULT_PYTHON_VERSION: "3.11"
|
||||||
@@ -25,6 +33,7 @@ jobs:
|
|||||||
name: Build Documentation
|
name: Build Documentation
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/configure-pages@v5
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
@@ -47,42 +56,23 @@ jobs:
|
|||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
--frozen \
|
--frozen \
|
||||||
mkdocs build --config-file ./mkdocs.yml
|
zensical build --clean
|
||||||
- name: Upload artifact
|
- name: Upload GitHub Pages artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-pages-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: documentation
|
path: site
|
||||||
path: site/
|
name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
retention-days: 7
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy Documentation
|
name: Deploy Documentation
|
||||||
needs: build
|
needs: build
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Deploy GitHub Pages
|
||||||
uses: actions/checkout@v6
|
uses: actions/deploy-pages@v4
|
||||||
- name: Set up Python
|
id: deployment
|
||||||
id: setup-python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
artifact_name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v7
|
|
||||||
with:
|
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
|
||||||
enable-cache: true
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: |
|
|
||||||
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
|
|
||||||
- name: Deploy documentation
|
|
||||||
run: |
|
|
||||||
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
|
|
||||||
git config --global user.name "${{ github.actor }}"
|
|
||||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
|
||||||
uv run \
|
|
||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
|
||||||
--dev \
|
|
||||||
--frozen \
|
|
||||||
mkdocs gh-deploy --force --no-history
|
|
||||||
|
|||||||
2
.github/workflows/ci-frontend.yml
vendored
2
.github/workflows/ci-frontend.yml
vendored
@@ -121,7 +121,7 @@ jobs:
|
|||||||
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||||
needs: install-dependencies
|
needs: install-dependencies
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
container: mcr.microsoft.com/playwright:v1.58.1-noble
|
container: mcr.microsoft.com/playwright:v1.58.2-noble
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
|||||||
16
.github/workflows/ci-lint.yml
vendored
16
.github/workflows/ci-lint.yml
vendored
@@ -10,15 +10,15 @@ concurrency:
|
|||||||
group: lint-${{ github.event.pull_request.number || github.ref }}
|
group: lint-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
lint:
|
||||||
name: Pre-commit Checks
|
name: Linting via prek
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-slim
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6.0.2
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.14"
|
||||||
- name: Run pre-commit
|
- name: Run prek
|
||||||
uses: pre-commit/action@v3.0.1
|
uses: j178/prek-action@v1.1.0
|
||||||
|
|||||||
4
.github/workflows/ci-release.yml
vendored
4
.github/workflows/ci-release.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
|||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
--frozen \
|
--frozen \
|
||||||
mkdocs build --config-file ./mkdocs.yml
|
zensical build --clean
|
||||||
# ---- Prepare Release ----
|
# ---- Prepare Release ----
|
||||||
- name: Generate requirements file
|
- name: Generate requirements file
|
||||||
run: |
|
run: |
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
uv run \
|
uv run \
|
||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
pre-commit run --files changelog.md || true
|
prek run --files changelog.md || true
|
||||||
|
|
||||||
git config --global user.name "github-actions"
|
git config --global user.name "github-actions"
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -54,7 +54,7 @@ junit.xml
|
|||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# MkDocs documentation
|
# Zensical documentation
|
||||||
site/
|
site/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
|
|||||||
2470
.mypy-baseline.txt
Normal file
2470
.mypy-baseline.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
# This file configures pre-commit hooks.
|
# This file configures pre-commit hooks.
|
||||||
# See https://pre-commit.com/ for general information
|
# See https://pre-commit.com/ for general information
|
||||||
# See https://pre-commit.com/hooks.html for a listing of possible hooks
|
# See https://pre-commit.com/hooks.html for a listing of possible hooks
|
||||||
|
# We actually run via https://github.com/j178/prek which is compatible
|
||||||
repos:
|
repos:
|
||||||
# General hooks
|
# General hooks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
@@ -49,12 +50,12 @@ repos:
|
|||||||
- 'prettier-plugin-organize-imports@4.1.0'
|
- 'prettier-plugin-organize-imports@4.1.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.14
|
rev: v0.15.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.11.1"
|
rev: "v2.12.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
# Dockerfile hooks
|
# Dockerfile hooks
|
||||||
|
|||||||
17368
.pyrefly-baseline.json
Normal file
17368
.pyrefly-baseline.json
Normal file
File diff suppressed because one or more lines are too long
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.9.26-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.9.29-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# correct networking for the tests
|
# correct networking for the tests
|
||||||
services:
|
services:
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.25
|
image: docker.io/gotenberg/gotenberg:8.26
|
||||||
hostname: gotenberg
|
hostname: gotenberg
|
||||||
container_name: gotenberg
|
container_name: gotenberg
|
||||||
network_mode: host
|
network_mode: host
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- "3143:3143" # IMAP
|
- "3143:3143" # IMAP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
nginx:
|
nginx:
|
||||||
image: docker.io/nginx:1.29-alpine
|
image: docker.io/nginx:1.29.5-alpine
|
||||||
hostname: nginx
|
hostname: nginx
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.25
|
image: docker.io/gotenberg/gotenberg:8.26
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.25
|
image: docker.io/gotenberg/gotenberg:8.26
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.25
|
image: docker.io/gotenberg/gotenberg:8.26
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# The REST API
|
# REST API
|
||||||
|
|
||||||
Paperless-ngx now ships with a fully-documented REST API and a browsable
|
Paperless-ngx now ships with a fully-documented REST API and a browsable
|
||||||
web interface to explore it. The API browsable interface is available at
|
web interface to explore it. The API browsable interface is available at
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
:root > * {
|
:root>* {
|
||||||
--md-primary-fg-color: #17541f;
|
--paperless-green: #17541f;
|
||||||
--md-primary-fg-color--dark: #17541f;
|
--paperless-green-accent: #2b8a38;
|
||||||
--md-primary-fg-color--light: #17541f;
|
--md-primary-fg-color: var(--paperless-green);
|
||||||
--md-accent-fg-color: #2b8a38;
|
--md-primary-fg-color--dark: var(--paperless-green);
|
||||||
|
--md-primary-fg-color--light: var(--paperless-green-accent);
|
||||||
|
--md-accent-fg-color: var(--paperless-green-accent);
|
||||||
--md-typeset-a-color: #21652a;
|
--md-typeset-a-color: #21652a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-header,
|
||||||
|
.md-tabs {
|
||||||
|
background-color: var(--paperless-green);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-tabs__link {
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-tabs__link:hover,
|
||||||
|
.md-tabs__link--active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
[data-md-color-scheme="slate"] {
|
[data-md-color-scheme="slate"] {
|
||||||
--md-hue: 222;
|
--md-hue: 222;
|
||||||
|
--md-default-bg-color: hsla(var(--md-hue), 15%, 10%, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -69,8 +87,8 @@ h4 code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
||||||
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="PAPERLESS_"],
|
.md-nav.md-nav--secondary .md-nav__item:has(> .md-nav__link[href*="PAPERLESS_"]),
|
||||||
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="USERMAP_"] {
|
.md-nav.md-nav--secondary .md-nav__item:has(> .md-nav__link[href*="USERMAP_"]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +101,3 @@ h4 code {
|
|||||||
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show search shortcut key */
|
|
||||||
[data-md-toggle="search"]:not(:checked) ~ .md-header .md-search__form::after {
|
|
||||||
position: absolute;
|
|
||||||
top: .3rem;
|
|
||||||
right: .3rem;
|
|
||||||
display: block;
|
|
||||||
padding: .1rem .4rem;
|
|
||||||
color: var(--md-default-fg-color--lighter);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: .8rem;
|
|
||||||
border: .05rem solid var(--md-default-fg-color--lighter);
|
|
||||||
border-radius: .1rem;
|
|
||||||
content: "/";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ first-time setup.
|
|||||||
5. Install pre-commit hooks:
|
5. Install pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run pre-commit install
|
$ uv run prek install
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
|
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
|
||||||
@@ -217,7 +217,7 @@ commit. See [above](#code-formatting-with-pre-commit-hooks) for installation ins
|
|||||||
command such as
|
command such as
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
$ git ls-files -- '*.ts' | xargs prek run prettier --files
|
||||||
```
|
```
|
||||||
|
|
||||||
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
||||||
@@ -338,13 +338,13 @@ LANGUAGES = [
|
|||||||
|
|
||||||
## Building the documentation
|
## Building the documentation
|
||||||
|
|
||||||
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
The documentation is built using Zensical, see their [documentation](https://zensical.org/docs/).
|
||||||
If you want to build the documentation locally, this is how you do it:
|
If you want to build the documentation locally, this is how you do it:
|
||||||
|
|
||||||
1. Build the documentation
|
1. Build the documentation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run mkdocs build --config-file mkdocs.yml
|
$ uv run zensical build
|
||||||
```
|
```
|
||||||
|
|
||||||
_alternatively..._
|
_alternatively..._
|
||||||
@@ -355,7 +355,7 @@ If you want to build the documentation locally, this is how you do it:
|
|||||||
something.
|
something.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run mkdocs serve
|
$ uv run zensical serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the Docker image
|
## Building the Docker image
|
||||||
@@ -481,3 +481,147 @@ To get started:
|
|||||||
|
|
||||||
5. The project is ready for debugging, start either run the fullstack debug or individual debug
|
5. The project is ready for debugging, start either run the fullstack debug or individual debug
|
||||||
processes. Yo spin up the project without debugging run the task **Project Start: Run all Services**
|
processes. Yo spin up the project without debugging run the task **Project Start: Run all Services**
|
||||||
|
|
||||||
|
## Developing Date Parser Plugins
|
||||||
|
|
||||||
|
Paperless-ngx uses a plugin system for date parsing, allowing you to extend or replace the default date parsing behavior. Plugins are discovered using [Python entry points](https://setuptools.pypa.io/en/latest/userguide/entry_point.html).
|
||||||
|
|
||||||
|
### Creating a Date Parser Plugin
|
||||||
|
|
||||||
|
To create a custom date parser plugin, you need to:
|
||||||
|
|
||||||
|
1. Create a class that inherits from `DateParserPluginBase`
|
||||||
|
2. Implement the required abstract method
|
||||||
|
3. Register your plugin via an entry point
|
||||||
|
|
||||||
|
#### 1. Implementing the Parser Class
|
||||||
|
|
||||||
|
Your parser must extend `documents.plugins.date_parsing.DateParserPluginBase` and implement the `parse` method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections.abc import Iterator
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from documents.plugins.date_parsing import DateParserPluginBase
|
||||||
|
|
||||||
|
|
||||||
|
class MyDateParserPlugin(DateParserPluginBase):
|
||||||
|
"""
|
||||||
|
Custom date parser implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse(self, filename: str, content: str) -> Iterator[datetime.datetime]:
|
||||||
|
"""
|
||||||
|
Parse dates from the document's filename and content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The original filename of the document
|
||||||
|
content: The extracted text content of the document
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
datetime.datetime: Valid datetime objects found in the document
|
||||||
|
"""
|
||||||
|
# Your parsing logic here
|
||||||
|
# Use self.config to access configuration settings
|
||||||
|
|
||||||
|
# Example: parse dates from filename first
|
||||||
|
if self.config.filename_date_order:
|
||||||
|
# Your filename parsing logic
|
||||||
|
yield some_datetime
|
||||||
|
|
||||||
|
# Then parse dates from content
|
||||||
|
# Your content parsing logic
|
||||||
|
yield another_datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configuration and Helper Methods
|
||||||
|
|
||||||
|
Your parser instance is initialized with a `DateParserConfig` object accessible via `self.config`. This provides:
|
||||||
|
|
||||||
|
- `languages: list[str]` - List of language codes for date parsing
|
||||||
|
- `timezone_str: str` - Timezone string for date localization
|
||||||
|
- `ignore_dates: set[datetime.date]` - Dates that should be filtered out
|
||||||
|
- `reference_time: datetime.datetime` - Current time for filtering future dates
|
||||||
|
- `filename_date_order: str | None` - Date order preference for filenames (e.g., "DMY", "MDY")
|
||||||
|
- `content_date_order: str` - Date order preference for content
|
||||||
|
|
||||||
|
The base class provides two helper methods you can use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _parse_string(
|
||||||
|
self,
|
||||||
|
date_string: str,
|
||||||
|
date_order: str,
|
||||||
|
) -> datetime.datetime | None:
|
||||||
|
"""
|
||||||
|
Parse a single date string using dateparser with configured settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _filter_date(
|
||||||
|
self,
|
||||||
|
date: datetime.datetime | None,
|
||||||
|
) -> datetime.datetime | None:
|
||||||
|
"""
|
||||||
|
Validate a parsed datetime against configured rules.
|
||||||
|
Filters out dates before 1900, future dates, and ignored dates.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Resource Management (Optional)
|
||||||
|
|
||||||
|
If your plugin needs to acquire or release resources (database connections, API clients, etc.), override the context manager methods. Paperless-ngx will always use plugins as context managers, ensuring resources can be released even in the event of errors.
|
||||||
|
|
||||||
|
#### 4. Registering Your Plugin
|
||||||
|
|
||||||
|
Register your plugin using a setuptools entry point in your package's `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.entry-points."paperless_ngx.date_parsers"]
|
||||||
|
my_parser = "my_package.parsers:MyDateParserPlugin"
|
||||||
|
```
|
||||||
|
|
||||||
|
The entry point name (e.g., `"my_parser"`) is used for sorting when multiple plugins are found. Paperless-ngx will use the first plugin alphabetically by name if multiple plugins are discovered.
|
||||||
|
|
||||||
|
### Plugin Discovery
|
||||||
|
|
||||||
|
Paperless-ngx automatically discovers and loads date parser plugins at runtime. The discovery process:
|
||||||
|
|
||||||
|
1. Queries the `paperless_ngx.date_parsers` entry point group
|
||||||
|
2. Validates that each plugin is a subclass of `DateParserPluginBase`
|
||||||
|
3. Sorts valid plugins alphabetically by entry point name
|
||||||
|
4. Uses the first valid plugin, or falls back to the default `RegexDateParserPlugin` if none are found
|
||||||
|
|
||||||
|
If multiple plugins are installed, a warning is logged indicating which plugin was selected.
|
||||||
|
|
||||||
|
### Example: Simple Date Parser
|
||||||
|
|
||||||
|
Here's a minimal example that only looks for ISO 8601 dates:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from documents.plugins.date_parsing.base import DateParserPluginBase
|
||||||
|
|
||||||
|
|
||||||
|
class ISODateParserPlugin(DateParserPluginBase):
|
||||||
|
"""
|
||||||
|
Parser that only matches ISO 8601 formatted dates (YYYY-MM-DD).
|
||||||
|
"""
|
||||||
|
|
||||||
|
ISO_REGEX = re.compile(r"\b(\d{4}-\d{2}-\d{2})\b")
|
||||||
|
|
||||||
|
def parse(self, filename: str, content: str) -> Iterator[datetime.datetime]:
|
||||||
|
# Combine filename and content for searching
|
||||||
|
text = f"{filename} {content}"
|
||||||
|
|
||||||
|
for match in self.ISO_REGEX.finditer(text):
|
||||||
|
date_string = match.group(1)
|
||||||
|
# Use helper method to parse with configured timezone
|
||||||
|
date = self._parse_string(date_string, "YMD")
|
||||||
|
# Use helper method to validate the date
|
||||||
|
filtered_date = self._filter_date(date)
|
||||||
|
if filtered_date is not None:
|
||||||
|
yield filtered_date
|
||||||
|
```
|
||||||
|
|||||||
10
docs/faq.md
10
docs/faq.md
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: FAQs
|
||||||
|
---
|
||||||
|
|
||||||
# Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
## _What's the general plan for Paperless-ngx?_
|
## _What's the general plan for Paperless-ngx?_
|
||||||
@@ -63,8 +67,10 @@ elsewhere. Here are a couple notes about that.
|
|||||||
Paperless also supports various Office documents (.docx, .doc, odt,
|
Paperless also supports various Office documents (.docx, .doc, odt,
|
||||||
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
||||||
|
|
||||||
Paperless-ngx determines the type of a file by inspecting its content.
|
Paperless-ngx determines the type of a file by inspecting its content
|
||||||
The file extensions do not matter.
|
rather than its file extensions. However, files processed via the
|
||||||
|
consumption directory will be rejected if they have a file extension that
|
||||||
|
not supported by any of the available parsers.
|
||||||
|
|
||||||
## _Will paperless-ngx run on Raspberry Pi?_
|
## _Will paperless-ngx run on Raspberry Pi?_
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Home
|
||||||
|
---
|
||||||
|
|
||||||
<div class="grid-left" markdown>
|
<div class="grid-left" markdown>
|
||||||
{.index-logo}
|
{.index-logo}
|
||||||
{.index-logo}
|
{.index-logo}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
## Installation
|
---
|
||||||
|
title: Setup
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
You can go multiple routes to setup and run Paperless:
|
You can go multiple routes to setup and run Paperless:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
# Usage Overview
|
---
|
||||||
|
title: Basic Usage
|
||||||
|
---
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
Paperless-ngx is an application that manages your personal documents. With
|
Paperless-ngx is an application that manages your personal documents. With
|
||||||
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
||||||
|
|||||||
87
mkdocs.yml
87
mkdocs.yml
@@ -1,87 +0,0 @@
|
|||||||
site_name: Paperless-ngx
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
logo: assets/logo.svg
|
|
||||||
font:
|
|
||||||
text: Roboto
|
|
||||||
code: Roboto Mono
|
|
||||||
palette:
|
|
||||||
# Palette toggle for automatic mode
|
|
||||||
- media: "(prefers-color-scheme)"
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-auto
|
|
||||||
name: Switch to light mode
|
|
||||||
# Palette toggle for light mode
|
|
||||||
- media: "(prefers-color-scheme: light)"
|
|
||||||
scheme: default
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-7
|
|
||||||
name: Switch to dark mode
|
|
||||||
# Palette toggle for dark mode
|
|
||||||
- media: "(prefers-color-scheme: dark)"
|
|
||||||
scheme: slate
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-4
|
|
||||||
name: Switch to system preference
|
|
||||||
features:
|
|
||||||
- navigation.tabs
|
|
||||||
- navigation.top
|
|
||||||
- toc.integrate
|
|
||||||
- content.code.annotate
|
|
||||||
icon:
|
|
||||||
repo: fontawesome/brands/github
|
|
||||||
favicon: assets/favicon.png
|
|
||||||
repo_url: https://github.com/paperless-ngx/paperless-ngx
|
|
||||||
repo_name: paperless-ngx/paperless-ngx
|
|
||||||
edit_uri: blob/main/docs/
|
|
||||||
extra_css:
|
|
||||||
- assets/extra.css
|
|
||||||
markdown_extensions:
|
|
||||||
- attr_list
|
|
||||||
- md_in_html
|
|
||||||
- def_list
|
|
||||||
- admonition
|
|
||||||
- tables
|
|
||||||
- pymdownx.highlight:
|
|
||||||
anchor_linenums: true
|
|
||||||
- pymdownx.superfences
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.snippets
|
|
||||||
- pymdownx.tilde
|
|
||||||
- footnotes
|
|
||||||
- pymdownx.superfences:
|
|
||||||
custom_fences:
|
|
||||||
- name: mermaid
|
|
||||||
class: mermaid
|
|
||||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
||||||
- pymdownx.emoji:
|
|
||||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
||||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
||||||
strict: true
|
|
||||||
nav:
|
|
||||||
- index.md
|
|
||||||
- setup.md
|
|
||||||
- 'Basic Usage': usage.md
|
|
||||||
- configuration.md
|
|
||||||
- administration.md
|
|
||||||
- advanced_usage.md
|
|
||||||
- 'REST API': api.md
|
|
||||||
- development.md
|
|
||||||
- 'FAQs': faq.md
|
|
||||||
- troubleshooting.md
|
|
||||||
- 'Migration to v3': migration.md
|
|
||||||
- changelog.md
|
|
||||||
copyright: Copyright © 2016 - 2026 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
|
|
||||||
extra:
|
|
||||||
social:
|
|
||||||
- icon: fontawesome/brands/github
|
|
||||||
link: https://github.com/paperless-ngx/paperless-ngx
|
|
||||||
- icon: fontawesome/brands/docker
|
|
||||||
link: https://hub.docker.com/r/paperlessngx/paperless-ngx
|
|
||||||
- icon: material/chat
|
|
||||||
link: https://matrix.to/#/#paperless:matrix.org
|
|
||||||
plugins:
|
|
||||||
- search
|
|
||||||
- glightbox:
|
|
||||||
skip_classes:
|
|
||||||
- no-lightbox
|
|
||||||
@@ -27,9 +27,9 @@ dependencies = [
|
|||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.10",
|
"django~=5.2.10",
|
||||||
"django-allauth[mfa,socialaccount]~=65.13.1",
|
"django-allauth[mfa,socialaccount]~=65.14.0",
|
||||||
"django-auditlog~=3.4.1",
|
"django-auditlog~=3.4.1",
|
||||||
"django-cachalot~=2.8.0",
|
"django-cachalot~=2.9.0",
|
||||||
"django-celery-results~=2.6.0",
|
"django-celery-results~=2.6.0",
|
||||||
"django-compression-middleware~=0.5.0",
|
"django-compression-middleware~=0.5.0",
|
||||||
"django-cors-headers~=4.9.0",
|
"django-cors-headers~=4.9.0",
|
||||||
@@ -42,7 +42,7 @@ dependencies = [
|
|||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"djangorestframework-guardian~=0.4.0",
|
"djangorestframework-guardian~=0.4.0",
|
||||||
"drf-spectacular~=0.28",
|
"drf-spectacular~=0.28",
|
||||||
"drf-spectacular-sidecar~=2025.10.1",
|
"drf-spectacular-sidecar~=2026.1.1",
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"faiss-cpu>=1.10",
|
"faiss-cpu>=1.10",
|
||||||
"filelock~=3.20.0",
|
"filelock~=3.20.0",
|
||||||
@@ -76,7 +76,7 @@ dependencies = [
|
|||||||
"sentence-transformers>=4.1",
|
"sentence-transformers>=4.1",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"tika-client~=0.10.0",
|
"tika-client~=0.10.0",
|
||||||
"torch~=2.9.1",
|
"torch~=2.10.0",
|
||||||
"tqdm~=4.67.1",
|
"tqdm~=4.67.1",
|
||||||
"watchfiles>=1.1.1",
|
"watchfiles>=1.1.1",
|
||||||
"whitenoise~=6.11",
|
"whitenoise~=6.11",
|
||||||
@@ -94,7 +94,7 @@ optional-dependencies.postgres = [
|
|||||||
"psycopg-pool==3.3",
|
"psycopg-pool==3.3",
|
||||||
]
|
]
|
||||||
optional-dependencies.webserver = [
|
optional-dependencies.webserver = [
|
||||||
"granian[uvloop]~=2.6.0",
|
"granian[uvloop]~=2.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@@ -106,8 +106,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
docs = [
|
docs = [
|
||||||
"mkdocs-glightbox~=0.5.1",
|
"zensical>=0.0.21",
|
||||||
"mkdocs-material~=9.7.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
testing = [
|
testing = [
|
||||||
@@ -127,9 +126,8 @@ testing = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
lint = [
|
lint = [
|
||||||
"pre-commit~=4.5.1",
|
"prek~=0.3.0",
|
||||||
"pre-commit-uv~=4.2.0",
|
"ruff~=0.15.0",
|
||||||
"ruff~=0.14.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
typing = [
|
typing = [
|
||||||
@@ -138,8 +136,12 @@ typing = [
|
|||||||
"django-stubs[compatible-mypy]",
|
"django-stubs[compatible-mypy]",
|
||||||
"djangorestframework-stubs[compatible-mypy]",
|
"djangorestframework-stubs[compatible-mypy]",
|
||||||
"lxml-stubs",
|
"lxml-stubs",
|
||||||
|
"microsoft-python-type-stubs @ git+https://github.com/microsoft/python-type-stubs.git",
|
||||||
"mypy",
|
"mypy",
|
||||||
|
"mypy-baseline",
|
||||||
|
"pyrefly",
|
||||||
"types-bleach",
|
"types-bleach",
|
||||||
|
"types-channels",
|
||||||
"types-colorama",
|
"types-colorama",
|
||||||
"types-dateparser",
|
"types-dateparser",
|
||||||
"types-markdown",
|
"types-markdown",
|
||||||
@@ -159,6 +161,11 @@ environments = [
|
|||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "pytorch-cpu"
|
||||||
|
url = "https://download.pytorch.org/whl/cpu"
|
||||||
|
explicit = true
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
# Markers are chosen to select these almost exclusively when building the Docker image
|
# Markers are chosen to select these almost exclusively when building the Docker image
|
||||||
psycopg-c = [
|
psycopg-c = [
|
||||||
@@ -174,11 +181,6 @@ torch = [
|
|||||||
{ index = "pytorch-cpu" },
|
{ index = "pytorch-cpu" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[tool.uv.index]]
|
|
||||||
name = "pytorch-cpu"
|
|
||||||
url = "https://download.pytorch.org/whl/cpu"
|
|
||||||
explicit = true
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
line-length = 88
|
line-length = 88
|
||||||
@@ -306,12 +308,20 @@ markers = [
|
|||||||
"gotenberg: Tests requiring Gotenberg service",
|
"gotenberg: Tests requiring Gotenberg service",
|
||||||
"tika: Tests requiring Tika service",
|
"tika: Tests requiring Tika service",
|
||||||
"greenmail: Tests requiring Greenmail service",
|
"greenmail: Tests requiring Greenmail service",
|
||||||
|
"date_parsing: Tests which cover date parsing from content or filename",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest_env]
|
[tool.pytest_env]
|
||||||
PAPERLESS_DISABLE_DBHANDLER = "true"
|
PAPERLESS_DISABLE_DBHANDLER = "true"
|
||||||
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_also = [
|
||||||
|
"if settings.AUDIT_LOG_ENABLED:",
|
||||||
|
"if AUDIT_LOG_ENABLED:",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = [
|
source = [
|
||||||
"src/",
|
"src/",
|
||||||
@@ -323,13 +333,6 @@ omit = [
|
|||||||
"paperless/auth.py",
|
"paperless/auth.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.coverage.report]
|
|
||||||
exclude_also = [
|
|
||||||
"if settings.AUDIT_LOG_ENABLED:",
|
|
||||||
"if AUDIT_LOG_ENABLED:",
|
|
||||||
"if TYPE_CHECKING:",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
mypy_path = "src"
|
mypy_path = "src"
|
||||||
plugins = [
|
plugins = [
|
||||||
@@ -343,5 +346,15 @@ disallow_untyped_defs = true
|
|||||||
warn_redundant_casts = true
|
warn_redundant_casts = true
|
||||||
warn_unused_ignores = true
|
warn_unused_ignores = true
|
||||||
|
|
||||||
|
[tool.pyrefly]
|
||||||
|
search-path = [ "src" ]
|
||||||
|
baseline = ".pyrefly-baseline.json"
|
||||||
|
python-platform = "linux"
|
||||||
|
|
||||||
[tool.django-stubs]
|
[tool.django-stubs]
|
||||||
django_settings_module = "paperless.settings"
|
django_settings_module = "paperless.settings"
|
||||||
|
|
||||||
|
[tool.mypy-baseline]
|
||||||
|
baseline_path = ".mypy-baseline.txt"
|
||||||
|
sort_baseline = true
|
||||||
|
ignore_categories = [ "note" ]
|
||||||
|
|||||||
@@ -86,7 +86,6 @@
|
|||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"ng2-pdf-viewer",
|
|
||||||
"file-saver",
|
"file-saver",
|
||||||
"utif"
|
"utif"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ test('dashboard saved view document links', async ({ page }) => {
|
|||||||
test('test slim sidebar', async ({ page }) => {
|
test('test slim sidebar', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.locator('#sidebarMenu').getByRole('button').click()
|
await page.locator('.sidebar-slim-toggler').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
await page.locator('#sidebarMenu').getByRole('button').click()
|
await page.locator('.sidebar-slim-toggler').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ test('should show a mobile preview', async ({ page }) => {
|
|||||||
await page.setViewportSize({ width: 400, height: 1000 })
|
await page.setViewportSize({ width: 400, height: 1000 })
|
||||||
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
||||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||||
await page.waitForSelector('pdf-viewer')
|
await page.waitForSelector('pngx-pdf-viewer')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should show a list of notes', async ({ page }) => {
|
test('should show a list of notes', async ({ page }) => {
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Correspondents' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/correspondents')
|
await page.goto('/attributes/correspondents')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -44,8 +44,10 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
test('should not allow user to view tags', async ({ page }) => {
|
test('should not allow user to view tags', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
|
await expect(
|
||||||
await page.goto('/tags')
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
|
).not.toBeAttached()
|
||||||
|
await page.goto('/attributes/tags')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -55,9 +57,9 @@ test('should not allow user to view document types', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Document Types' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/documenttypes')
|
await page.goto('/attributes/documenttypes')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -67,9 +69,9 @@ test('should not allow user to view storage paths', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Storage Paths' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/storagepaths')
|
await page.goto('/attributes/storagepaths')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ module.exports = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
...esmPreset.moduleNameMapper,
|
...esmPreset.moduleNameMapper,
|
||||||
'^src/(.*)': '<rootDir>/src/$1',
|
'^src/(.*)': '<rootDir>/src/$1',
|
||||||
|
'^pdfjs-dist/legacy/build/pdf\\.mjs$':
|
||||||
|
'<rootDir>/src/test/mocks/pdfjs-legacy-build-pdf.ts',
|
||||||
|
'^pdfjs-dist/web/pdf_viewer\\.mjs$':
|
||||||
|
'<rootDir>/src/test/mocks/pdfjs-web-pdf_viewer.ts',
|
||||||
},
|
},
|
||||||
workerIdleMemoryLimit: '512MB',
|
workerIdleMemoryLimit: '512MB',
|
||||||
reporters: [
|
reporters: [
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<trans-unit id="ngb.alert.close" datatype="html">
|
<trans-unit id="ngb.alert.close" datatype="html">
|
||||||
<source>Close</source>
|
<source>Close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/alert/alert.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/alert/alert.ts</context>
|
||||||
<context context-type="linenumber">50</context>
|
<context context-type="linenumber">50</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.carousel.slide-number" datatype="html">
|
<trans-unit id="ngb.carousel.slide-number" datatype="html">
|
||||||
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
|
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">131,135</context>
|
<context context-type="linenumber">131,135</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Currently selected slide number read by screen reader</note>
|
<note priority="1" from="description">Currently selected slide number read by screen reader</note>
|
||||||
@@ -20,114 +20,114 @@
|
|||||||
<trans-unit id="ngb.carousel.previous" datatype="html">
|
<trans-unit id="ngb.carousel.previous" datatype="html">
|
||||||
<source>Previous</source>
|
<source>Previous</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">159,162</context>
|
<context context-type="linenumber">159,162</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.carousel.next" datatype="html">
|
<trans-unit id="ngb.carousel.next" datatype="html">
|
||||||
<source>Next</source>
|
<source>Next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">202,203</context>
|
<context context-type="linenumber">202,203</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.select-month" datatype="html">
|
<trans-unit id="ngb.datepicker.select-month" datatype="html">
|
||||||
<source>Select month</source>
|
<source>Select month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">91</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">91</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.select-year" datatype="html">
|
<trans-unit id="ngb.datepicker.select-year" datatype="html">
|
||||||
<source>Select year</source>
|
<source>Select year</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">91</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">91</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.previous-month" datatype="html">
|
<trans-unit id="ngb.datepicker.previous-month" datatype="html">
|
||||||
<source>Previous month</source>
|
<source>Previous month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">83,85</context>
|
<context context-type="linenumber">83,85</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.next-month" datatype="html">
|
<trans-unit id="ngb.datepicker.next-month" datatype="html">
|
||||||
<source>Next month</source>
|
<source>Next month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.first" datatype="html">
|
<trans-unit id="ngb.pagination.first" datatype="html">
|
||||||
<source>««</source>
|
<source>««</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.previous" datatype="html">
|
<trans-unit id="ngb.pagination.previous" datatype="html">
|
||||||
<source>«</source>
|
<source>«</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.next" datatype="html">
|
<trans-unit id="ngb.pagination.next" datatype="html">
|
||||||
<source>»</source>
|
<source>»</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.last" datatype="html">
|
<trans-unit id="ngb.pagination.last" datatype="html">
|
||||||
<source>»»</source>
|
<source>»»</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.first-aria" datatype="html">
|
<trans-unit id="ngb.pagination.first-aria" datatype="html">
|
||||||
<source>First</source>
|
<source>First</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
|
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
|
||||||
<source>Previous</source>
|
<source>Previous</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.next-aria" datatype="html">
|
<trans-unit id="ngb.pagination.next-aria" datatype="html">
|
||||||
<source>Next</source>
|
<source>Next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.last-aria" datatype="html">
|
<trans-unit id="ngb.pagination.last-aria" datatype="html">
|
||||||
<source>Last</source>
|
<source>Last</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/pagination/pagination-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/pagination/pagination-config.ts</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -135,105 +135,105 @@
|
|||||||
<source><x id="INTERPOLATION" equiv-text="barConfig);
|
<source><x id="INTERPOLATION" equiv-text="barConfig);
|
||||||
pu"/></source>
|
pu"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/progressbar/progressbar.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/progressbar/progressbar.ts</context>
|
||||||
<context context-type="linenumber">41,42</context>
|
<context context-type="linenumber">41,42</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.HH" datatype="html">
|
<trans-unit id="ngb.timepicker.HH" datatype="html">
|
||||||
<source>HH</source>
|
<source>HH</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.hours" datatype="html">
|
<trans-unit id="ngb.timepicker.hours" datatype="html">
|
||||||
<source>Hours</source>
|
<source>Hours</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.MM" datatype="html">
|
<trans-unit id="ngb.timepicker.MM" datatype="html">
|
||||||
<source>MM</source>
|
<source>MM</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.minutes" datatype="html">
|
<trans-unit id="ngb.timepicker.minutes" datatype="html">
|
||||||
<source>Minutes</source>
|
<source>Minutes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
|
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
|
||||||
<source>Increment hours</source>
|
<source>Increment hours</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
|
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
|
||||||
<source>Decrement hours</source>
|
<source>Decrement hours</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
|
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
|
||||||
<source>Increment minutes</source>
|
<source>Increment minutes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
|
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
|
||||||
<source>Decrement minutes</source>
|
<source>Decrement minutes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.SS" datatype="html">
|
<trans-unit id="ngb.timepicker.SS" datatype="html">
|
||||||
<source>SS</source>
|
<source>SS</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.seconds" datatype="html">
|
<trans-unit id="ngb.timepicker.seconds" datatype="html">
|
||||||
<source>Seconds</source>
|
<source>Seconds</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
|
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
|
||||||
<source>Increment seconds</source>
|
<source>Increment seconds</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
|
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
|
||||||
<source>Decrement seconds</source>
|
<source>Decrement seconds</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.PM" datatype="html">
|
<trans-unit id="ngb.timepicker.PM" datatype="html">
|
||||||
<source><x id="INTERPOLATION"/></source>
|
<source><x id="INTERPOLATION"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/timepicker/timepicker-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/timepicker/timepicker-config.ts</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.toast.close-aria" datatype="html">
|
<trans-unit id="ngb.toast.close-aria" datatype="html">
|
||||||
<source>Close</source>
|
<source>Close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.2_@angular+core@21.1.2_@angular+_0ae6084f45398d2eea75800b78d6d6df/node_modules/src/toast/toast-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.1.3_@angular+core@21.1.3_@angular+_1ede04b1f6b65fa8e34a28e44afe1de9/node_modules/src/toast/toast-config.ts</context>
|
||||||
<context context-type="linenumber">54</context>
|
<context context-type="linenumber">54</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -241,18 +241,18 @@
|
|||||||
<source>Document <x id="PH" equiv-text="status.filename"/> was added to Paperless-ngx.</source>
|
<source>Document <x id="PH" equiv-text="status.filename"/> was added to Paperless-ngx.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">95</context>
|
<context context-type="linenumber">90</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">104</context>
|
<context context-type="linenumber">99</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1931214133925051574" datatype="html">
|
<trans-unit id="1931214133925051574" datatype="html">
|
||||||
<source>Open document</source>
|
<source>Open document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">92</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||||
@@ -279,21 +279,21 @@
|
|||||||
<source>Could not add <x id="PH" equiv-text="status.filename"/>: <x id="PH_1" equiv-text="status.message"/></source>
|
<source>Could not add <x id="PH" equiv-text="status.filename"/>: <x id="PH_1" equiv-text="status.message"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">114</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1218124467712564468" datatype="html">
|
<trans-unit id="1218124467712564468" datatype="html">
|
||||||
<source>Document <x id="PH" equiv-text="status.filename"/> is being processed by Paperless-ngx.</source>
|
<source>Document <x id="PH" equiv-text="status.filename"/> is being processed by Paperless-ngx.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">134</context>
|
<context context-type="linenumber">129</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6570363013146073520" datatype="html">
|
<trans-unit id="6570363013146073520" datatype="html">
|
||||||
<source>Dashboard</source>
|
<source>Dashboard</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">141</context>
|
<context context-type="linenumber">136</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
<source>Documents</source>
|
<source>Documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">152</context>
|
<context context-type="linenumber">147</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
@@ -367,7 +367,7 @@
|
|||||||
<source>Settings</source>
|
<source>Settings</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">164</context>
|
<context context-type="linenumber">159</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
@@ -386,78 +386,53 @@
|
|||||||
<context context-type="linenumber">260</context>
|
<context context-type="linenumber">260</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2501522447884928778" datatype="html">
|
|
||||||
<source>Prev</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
|
||||||
<context context-type="linenumber">170</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3885497195825665706" datatype="html">
|
|
||||||
<source>Next</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
|
||||||
<context context-type="linenumber">171</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">109</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1241348629231510663" datatype="html">
|
|
||||||
<source>End</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
|
||||||
<context context-type="linenumber">172</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5890330709052835856" datatype="html">
|
<trans-unit id="5890330709052835856" datatype="html">
|
||||||
<source>The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.</source>
|
<source>The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">178</context>
|
<context context-type="linenumber">168</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9075755296812854717" datatype="html">
|
<trans-unit id="9075755296812854717" datatype="html">
|
||||||
<source>Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.</source>
|
<source>Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">185</context>
|
<context context-type="linenumber">175</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7495498057594070122" datatype="html">
|
<trans-unit id="7495498057594070122" datatype="html">
|
||||||
<source>The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.</source>
|
<source>The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">190</context>
|
<context context-type="linenumber">180</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1334220418719920556" datatype="html">
|
<trans-unit id="1334220418719920556" datatype="html">
|
||||||
<source>The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.</source>
|
<source>The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">197</context>
|
<context context-type="linenumber">187</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5427326625898532358" datatype="html">
|
<trans-unit id="5427326625898532358" datatype="html">
|
||||||
<source>Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.</source>
|
<source>Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">203</context>
|
<context context-type="linenumber">193</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2804886236408698479" datatype="html">
|
<trans-unit id="2804886236408698479" datatype="html">
|
||||||
<source>Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.</source>
|
<source>Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">208</context>
|
<context context-type="linenumber">198</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7851939076947092983" datatype="html">
|
<trans-unit id="7851939076947092983" datatype="html">
|
||||||
<source>Manage e-mail accounts and rules for automatically importing documents.</source>
|
<source>Manage e-mail accounts and rules for automatically importing documents.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">216</context>
|
<context context-type="linenumber">206</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
|
||||||
@@ -468,14 +443,14 @@
|
|||||||
<source>Workflows give you more control over the document pipeline.</source>
|
<source>Workflows give you more control over the document pipeline.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">224</context>
|
<context context-type="linenumber">214</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4680387114119209483" datatype="html">
|
<trans-unit id="4680387114119209483" datatype="html">
|
||||||
<source>File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.</source>
|
<source>File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">232</context>
|
<context context-type="linenumber">222</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
@@ -486,28 +461,28 @@
|
|||||||
<source>Check out the settings for various tweaks to the web app.</source>
|
<source>Check out the settings for various tweaks to the web app.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">240</context>
|
<context context-type="linenumber">230</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7172877665285340082" datatype="html">
|
<trans-unit id="7172877665285340082" datatype="html">
|
||||||
<source>Thank you! 🙏</source>
|
<source>Thank you! 🙏</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">248</context>
|
<context context-type="linenumber">238</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7354947513482088740" datatype="html">
|
<trans-unit id="7354947513482088740" datatype="html">
|
||||||
<source>There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.</source>
|
<source>There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">250</context>
|
<context context-type="linenumber">240</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4270528545616947218" datatype="html">
|
<trans-unit id="4270528545616947218" datatype="html">
|
||||||
<source>Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!</source>
|
<source>Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||||
<context context-type="linenumber">252</context>
|
<context context-type="linenumber">242</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9063918187161876141" datatype="html">
|
<trans-unit id="9063918187161876141" datatype="html">
|
||||||
@@ -1234,7 +1209,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1472</context>
|
<context context-type="linenumber">1481</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1577733187050997705" datatype="html">
|
<trans-unit id="1577733187050997705" datatype="html">
|
||||||
@@ -2823,11 +2798,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1108</context>
|
<context context-type="linenumber">1116</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1473</context>
|
<context context-type="linenumber">1482</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -3418,7 +3393,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1061</context>
|
<context context-type="linenumber">1069</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -3523,7 +3498,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1524</context>
|
<context context-type="linenumber">1533</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6661109599266152398" datatype="html">
|
<trans-unit id="6661109599266152398" datatype="html">
|
||||||
@@ -3534,7 +3509,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1525</context>
|
<context context-type="linenumber">1534</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5162686434580248853" datatype="html">
|
<trans-unit id="5162686434580248853" datatype="html">
|
||||||
@@ -3545,7 +3520,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1526</context>
|
<context context-type="linenumber">1535</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8157388568390631653" datatype="html">
|
<trans-unit id="8157388568390631653" datatype="html">
|
||||||
@@ -6215,7 +6190,7 @@
|
|||||||
<source>Open preview</source>
|
<source>Open preview</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.ts</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">54</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2984628903434675339" datatype="html">
|
<trans-unit id="2984628903434675339" datatype="html">
|
||||||
@@ -7460,6 +7435,17 @@
|
|||||||
<context context-type="linenumber">106</context>
|
<context context-type="linenumber">106</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="3885497195825665706" datatype="html">
|
||||||
|
<source>Next</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
|
<context context-type="linenumber">109</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/main.ts</context>
|
||||||
|
<context context-type="linenumber">403</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="5028777105388019087" datatype="html">
|
<trans-unit id="5028777105388019087" datatype="html">
|
||||||
<source>Details</source>
|
<source>Details</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -7663,63 +7649,63 @@
|
|||||||
<source>Enter Password</source>
|
<source>Enter Password</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">499</context>
|
<context context-type="linenumber">497</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2218903673684131427" datatype="html">
|
<trans-unit id="2218903673684131427" datatype="html">
|
||||||
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">428,430</context>
|
<context context-type="linenumber">434,436</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3200733026060976258" datatype="html">
|
<trans-unit id="3200733026060976258" datatype="html">
|
||||||
<source>Document changes detected</source>
|
<source>Document changes detected</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">467</context>
|
<context context-type="linenumber">473</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2887155916749964" datatype="html">
|
<trans-unit id="2887155916749964" datatype="html">
|
||||||
<source>The version of this document in your browser session appears older than the existing version.</source>
|
<source>The version of this document in your browser session appears older than the existing version.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">468</context>
|
<context context-type="linenumber">474</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="237142428785956348" datatype="html">
|
<trans-unit id="237142428785956348" datatype="html">
|
||||||
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">469</context>
|
<context context-type="linenumber">475</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8720977247725652816" datatype="html">
|
<trans-unit id="8720977247725652816" datatype="html">
|
||||||
<source>Ok</source>
|
<source>Ok</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">471</context>
|
<context context-type="linenumber">477</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6142395741265832184" datatype="html">
|
<trans-unit id="6142395741265832184" datatype="html">
|
||||||
<source>Next document</source>
|
<source>Next document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">597</context>
|
<context context-type="linenumber">605</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="651985345816518480" datatype="html">
|
<trans-unit id="651985345816518480" datatype="html">
|
||||||
<source>Previous document</source>
|
<source>Previous document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">607</context>
|
<context context-type="linenumber">615</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2885986061416655600" datatype="html">
|
<trans-unit id="2885986061416655600" datatype="html">
|
||||||
<source>Close document</source>
|
<source>Close document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">615</context>
|
<context context-type="linenumber">623</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
||||||
@@ -7730,67 +7716,67 @@
|
|||||||
<source>Save document</source>
|
<source>Save document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">622</context>
|
<context context-type="linenumber">630</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1784543155727940353" datatype="html">
|
<trans-unit id="1784543155727940353" datatype="html">
|
||||||
<source>Save and close / next</source>
|
<source>Save and close / next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">631</context>
|
<context context-type="linenumber">639</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5758784066858623886" datatype="html">
|
<trans-unit id="5758784066858623886" datatype="html">
|
||||||
<source>Error retrieving metadata</source>
|
<source>Error retrieving metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">686</context>
|
<context context-type="linenumber">694</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3456881259945295697" datatype="html">
|
<trans-unit id="3456881259945295697" datatype="html">
|
||||||
<source>Error retrieving suggestions.</source>
|
<source>Error retrieving suggestions.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">741</context>
|
<context context-type="linenumber">749</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2194092841814123758" datatype="html">
|
<trans-unit id="2194092841814123758" datatype="html">
|
||||||
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">950</context>
|
<context context-type="linenumber">958</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">974</context>
|
<context context-type="linenumber">982</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6626387786259219838" datatype="html">
|
<trans-unit id="6626387786259219838" datatype="html">
|
||||||
<source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source>
|
<source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">980</context>
|
<context context-type="linenumber">988</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="448882439049417053" datatype="html">
|
<trans-unit id="448882439049417053" datatype="html">
|
||||||
<source>Error saving document</source>
|
<source>Error saving document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1030</context>
|
<context context-type="linenumber">1038</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8410796510716511826" datatype="html">
|
<trans-unit id="8410796510716511826" datatype="html">
|
||||||
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
|
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1062</context>
|
<context context-type="linenumber">1070</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="282586936710748252" datatype="html">
|
<trans-unit id="282586936710748252" datatype="html">
|
||||||
<source>Documents can be restored prior to permanent deletion.</source>
|
<source>Documents can be restored prior to permanent deletion.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1063</context>
|
<context context-type="linenumber">1071</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -7801,7 +7787,7 @@
|
|||||||
<source>Move to trash</source>
|
<source>Move to trash</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1065</context>
|
<context context-type="linenumber">1073</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -7812,14 +7798,14 @@
|
|||||||
<source>Error deleting document</source>
|
<source>Error deleting document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1084</context>
|
<context context-type="linenumber">1092</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="619486176823357521" datatype="html">
|
<trans-unit id="619486176823357521" datatype="html">
|
||||||
<source>Reprocess confirm</source>
|
<source>Reprocess confirm</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1104</context>
|
<context context-type="linenumber">1112</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -7830,102 +7816,102 @@
|
|||||||
<source>This operation will permanently recreate the archive file for this document.</source>
|
<source>This operation will permanently recreate the archive file for this document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1105</context>
|
<context context-type="linenumber">1113</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="302054111564709516" datatype="html">
|
<trans-unit id="302054111564709516" datatype="html">
|
||||||
<source>The archive file will be re-generated with the current settings.</source>
|
<source>The archive file will be re-generated with the current settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1106</context>
|
<context context-type="linenumber">1114</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8251197608401006898" datatype="html">
|
<trans-unit id="8251197608401006898" datatype="html">
|
||||||
<source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
<source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1116</context>
|
<context context-type="linenumber">1124</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4409560272830824468" datatype="html">
|
<trans-unit id="4409560272830824468" datatype="html">
|
||||||
<source>Error executing operation</source>
|
<source>Error executing operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1127</context>
|
<context context-type="linenumber">1135</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6030453331794586802" datatype="html">
|
<trans-unit id="6030453331794586802" datatype="html">
|
||||||
<source>Error downloading document</source>
|
<source>Error downloading document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1176</context>
|
<context context-type="linenumber">1184</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4458954481601077369" datatype="html">
|
<trans-unit id="4458954481601077369" datatype="html">
|
||||||
<source>Page Fit</source>
|
<source>Page Fit</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1253</context>
|
<context context-type="linenumber">1263</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4663705961777238777" datatype="html">
|
<trans-unit id="4663705961777238777" datatype="html">
|
||||||
<source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
<source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1491</context>
|
<context context-type="linenumber">1500</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9043972994040261999" datatype="html">
|
<trans-unit id="9043972994040261999" datatype="html">
|
||||||
<source>Error executing PDF edit operation</source>
|
<source>Error executing PDF edit operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1503</context>
|
<context context-type="linenumber">1512</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6172690334763056188" datatype="html">
|
<trans-unit id="6172690334763056188" datatype="html">
|
||||||
<source>Please enter the current password before attempting to remove it.</source>
|
<source>Please enter the current password before attempting to remove it.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1514</context>
|
<context context-type="linenumber">1523</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="968660764814228922" datatype="html">
|
<trans-unit id="968660764814228922" datatype="html">
|
||||||
<source>Password removal operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
<source>Password removal operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1546</context>
|
<context context-type="linenumber">1555</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2282118435712883014" datatype="html">
|
<trans-unit id="2282118435712883014" datatype="html">
|
||||||
<source>Error executing password removal operation</source>
|
<source>Error executing password removal operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1560</context>
|
<context context-type="linenumber">1569</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740891324955700797" datatype="html">
|
<trans-unit id="3740891324955700797" datatype="html">
|
||||||
<source>Print failed.</source>
|
<source>Print failed.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1597</context>
|
<context context-type="linenumber">1606</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6457245677384603573" datatype="html">
|
<trans-unit id="6457245677384603573" datatype="html">
|
||||||
<source>Error loading document for printing.</source>
|
<source>Error loading document for printing.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1609</context>
|
<context context-type="linenumber">1618</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6085793215710522488" datatype="html">
|
<trans-unit id="6085793215710522488" datatype="html">
|
||||||
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1674</context>
|
<context context-type="linenumber">1683</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1678</context>
|
<context context-type="linenumber">1687</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4958946940233632319" datatype="html">
|
<trans-unit id="4958946940233632319" datatype="html">
|
||||||
@@ -11244,6 +11230,20 @@
|
|||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="2501522447884928778" datatype="html">
|
||||||
|
<source>Prev</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/main.ts</context>
|
||||||
|
<context context-type="linenumber">402</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1241348629231510663" datatype="html">
|
||||||
|
<source>End</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/main.ts</context>
|
||||||
|
<context context-type="linenumber">404</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -11,15 +11,15 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^21.1.2",
|
"@angular/cdk": "^21.1.3",
|
||||||
"@angular/common": "~21.1.2",
|
"@angular/common": "~21.1.3",
|
||||||
"@angular/compiler": "~21.1.2",
|
"@angular/compiler": "~21.1.3",
|
||||||
"@angular/core": "~21.1.2",
|
"@angular/core": "~21.1.3",
|
||||||
"@angular/forms": "~21.1.2",
|
"@angular/forms": "~21.1.3",
|
||||||
"@angular/localize": "~21.1.2",
|
"@angular/localize": "~21.1.3",
|
||||||
"@angular/platform-browser": "~21.1.2",
|
"@angular/platform-browser": "~21.1.3",
|
||||||
"@angular/platform-browser-dynamic": "~21.1.2",
|
"@angular/platform-browser-dynamic": "~21.1.3",
|
||||||
"@angular/router": "~21.1.2",
|
"@angular/router": "~21.1.3",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
|
||||||
"@ng-select/ng-select": "^21.2.0",
|
"@ng-select/ng-select": "^21.2.0",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
@@ -27,12 +27,12 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
"ng2-pdf-viewer": "^10.4.0",
|
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^10.1.0",
|
"ngx-color": "^10.1.0",
|
||||||
"ngx-cookie-service": "^21.1.0",
|
"ngx-cookie-service": "^21.1.0",
|
||||||
"ngx-device-detector": "^11.0.0",
|
"ngx-device-detector": "^11.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
|
"ngx-ui-tour-ng-bootstrap": "^18.0.0",
|
||||||
|
"pdfjs-dist": "^5.4.624",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
@@ -42,20 +42,20 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^21.0.3",
|
"@angular-builders/custom-webpack": "^21.0.3",
|
||||||
"@angular-builders/jest": "^21.0.3",
|
"@angular-builders/jest": "^21.0.3",
|
||||||
"@angular-devkit/core": "^21.1.2",
|
"@angular-devkit/core": "^21.1.3",
|
||||||
"@angular-devkit/schematics": "^21.1.2",
|
"@angular-devkit/schematics": "^21.1.3",
|
||||||
"@angular-eslint/builder": "21.2.0",
|
"@angular-eslint/builder": "21.2.0",
|
||||||
"@angular-eslint/eslint-plugin": "21.2.0",
|
"@angular-eslint/eslint-plugin": "21.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "21.2.0",
|
"@angular-eslint/eslint-plugin-template": "21.2.0",
|
||||||
"@angular-eslint/schematics": "21.2.0",
|
"@angular-eslint/schematics": "21.2.0",
|
||||||
"@angular-eslint/template-parser": "21.2.0",
|
"@angular-eslint/template-parser": "21.2.0",
|
||||||
"@angular/build": "^21.1.2",
|
"@angular/build": "^21.1.3",
|
||||||
"@angular/cli": "~21.1.2",
|
"@angular/cli": "~21.1.3",
|
||||||
"@angular/compiler-cli": "~21.1.2",
|
"@angular/compiler-cli": "~21.1.3",
|
||||||
"@codecov/webpack-plugin": "^1.9.1",
|
"@codecov/webpack-plugin": "^1.9.1",
|
||||||
"@playwright/test": "^1.58.1",
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^25.2.0",
|
"@types/node": "^25.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"@typescript-eslint/utils": "^8.54.0",
|
"@typescript-eslint/utils": "^8.54.0",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"webpack": "^5.103.0"
|
"webpack": "^5.105.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.17.1",
|
"packageManager": "pnpm@10.17.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
1332
src-ui/pnpm-lock.yaml
generated
1332
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -100,10 +100,10 @@ const mock = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, 'open', { value: jest.fn() })
|
Object.defineProperty(globalThis, 'open', { value: jest.fn() })
|
||||||
Object.defineProperty(window, 'localStorage', { value: mock() })
|
Object.defineProperty(globalThis, 'localStorage', { value: mock() })
|
||||||
Object.defineProperty(window, 'sessionStorage', { value: mock() })
|
Object.defineProperty(globalThis, 'sessionStorage', { value: mock() })
|
||||||
Object.defineProperty(window, 'getComputedStyle', {
|
Object.defineProperty(globalThis, 'getComputedStyle', {
|
||||||
value: () => ['-webkit-appearance'],
|
value: () => ['-webkit-appearance'],
|
||||||
})
|
})
|
||||||
Object.defineProperty(navigator, 'clipboard', {
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
@@ -115,13 +115,33 @@ Object.defineProperty(navigator, 'canShare', { value: () => true })
|
|||||||
if (!navigator.share) {
|
if (!navigator.share) {
|
||||||
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
if (!URL.createObjectURL) {
|
if (!globalThis.URL.createObjectURL) {
|
||||||
Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() })
|
Object.defineProperty(globalThis.URL, 'createObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
if (!URL.revokeObjectURL) {
|
if (!globalThis.URL.revokeObjectURL) {
|
||||||
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
Object.defineProperty(globalThis.URL, 'revokeObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
class MockResizeObserver {
|
||||||
|
private readonly callback: ResizeObserverCallback
|
||||||
|
|
||||||
|
constructor(callback: ResizeObserverCallback) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
observe = jest.fn()
|
||||||
|
unobserve = jest.fn()
|
||||||
|
disconnect = jest.fn()
|
||||||
|
|
||||||
|
trigger = (entries: ResizeObserverEntry[] = []) => {
|
||||||
|
this.callback(entries, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'ResizeObserver', {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: MockResizeObserver,
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof IntersectionObserver === 'undefined') {
|
if (typeof IntersectionObserver === 'undefined') {
|
||||||
class MockIntersectionObserver {
|
class MockIntersectionObserver {
|
||||||
@@ -136,7 +156,7 @@ if (typeof IntersectionObserver === 'undefined') {
|
|||||||
takeRecords = jest.fn()
|
takeRecords = jest.fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, 'IntersectionObserver', {
|
Object.defineProperty(globalThis, 'IntersectionObserver', {
|
||||||
writable: true,
|
writable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: MockIntersectionObserver,
|
value: MockIntersectionObserver,
|
||||||
|
|||||||
@@ -11,13 +11,9 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
|
|||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
import { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component'
|
||||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
|
||||||
import { MailComponent } from './components/manage/mail/mail.component'
|
import { MailComponent } from './components/manage/mail/mail.component'
|
||||||
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
|
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
|
||||||
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
@@ -106,52 +102,76 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tags',
|
path: 'attributes',
|
||||||
component: TagListComponent,
|
component: DocumentAttributesComponent,
|
||||||
canActivate: [PermissionsGuard],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
requiredPermission: {
|
requiredPermissionAny: [
|
||||||
action: PermissionAction.View,
|
{ action: PermissionAction.View, type: PermissionType.Tag },
|
||||||
type: PermissionType.Tag,
|
|
||||||
},
|
|
||||||
componentName: 'TagListComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'documenttypes',
|
|
||||||
component: DocumentTypeListComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
componentName: 'DocumentTypeListComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'correspondents',
|
|
||||||
component: CorrespondentListComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
action: PermissionAction.View,
|
||||||
type: PermissionType.Correspondent,
|
type: PermissionType.Correspondent,
|
||||||
},
|
},
|
||||||
componentName: 'CorrespondentListComponent',
|
{
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
||||||
|
],
|
||||||
|
componentName: 'DocumentAttributesComponent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'storagepaths',
|
path: 'attributes/:section',
|
||||||
component: StoragePathListComponent,
|
component: DocumentAttributesComponent,
|
||||||
canActivate: [PermissionsGuard],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
requiredPermission: {
|
requiredPermissionAny: [
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.Tag },
|
||||||
|
{
|
||||||
action: PermissionAction.View,
|
action: PermissionAction.View,
|
||||||
type: PermissionType.StoragePath,
|
type: PermissionType.Correspondent,
|
||||||
},
|
},
|
||||||
componentName: 'StoragePathListComponent',
|
{
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
},
|
},
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
||||||
|
],
|
||||||
|
componentName: 'DocumentAttributesComponent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documentproperties',
|
||||||
|
redirectTo: '/attributes',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documentproperties/:section',
|
||||||
|
redirectTo: '/attributes/:section',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tags',
|
||||||
|
redirectTo: '/attributes/tags',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'correspondents',
|
||||||
|
redirectTo: '/attributes/correspondents',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documenttypes',
|
||||||
|
redirectTo: '/attributes/documenttypes',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'storagepaths',
|
||||||
|
redirectTo: '/attributes/storagepaths',
|
||||||
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
@@ -239,15 +259,8 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'customfields',
|
path: 'customfields',
|
||||||
component: CustomFieldsComponent,
|
redirectTo: '/attributes/customfields',
|
||||||
canActivate: [PermissionsGuard],
|
pathMatch: 'full',
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.CustomField,
|
|
||||||
},
|
|
||||||
componentName: 'CustomFieldsComponent',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflows',
|
path: 'workflows',
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
import { Router, RouterModule } from '@angular/router'
|
import { Router, RouterModule } from '@angular/router'
|
||||||
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import {
|
||||||
|
provideUiTour,
|
||||||
|
TourNgBootstrap,
|
||||||
|
TourService,
|
||||||
|
} from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { routes } from './app-routing.module'
|
import { routes } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
@@ -40,12 +44,12 @@ describe('AppComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
TourNgBootstrapModule,
|
|
||||||
RouterModule.forRoot(routes),
|
RouterModule.forRoot(routes),
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
AppComponent,
|
AppComponent,
|
||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
FileDropComponent,
|
FileDropComponent,
|
||||||
|
TourNgBootstrap,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -53,6 +57,7 @@ describe('AppComponent', () => {
|
|||||||
DirtySavedViewGuard,
|
DirtySavedViewGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||||
import { Router, RouterOutlet } from '@angular/router'
|
import { Router, RouterOutlet } from '@angular/router'
|
||||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { first, Subscription } from 'rxjs'
|
import { first, Subscription } from 'rxjs'
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||||
@@ -21,12 +21,7 @@ import { WebsocketStatusService } from './services/websocket-status.service'
|
|||||||
selector: 'pngx-root',
|
selector: 'pngx-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
imports: [
|
imports: [FileDropComponent, ToastsComponent, TourNgBootstrap, RouterOutlet],
|
||||||
FileDropComponent,
|
|
||||||
ToastsComponent,
|
|
||||||
TourNgBootstrapModule,
|
|
||||||
RouterOutlet,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private settings = inject(SettingsService)
|
private settings = inject(SettingsService)
|
||||||
@@ -167,12 +162,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevBtnTitle = $localize`Prev`
|
this.tourService.initialize([
|
||||||
const nextBtnTitle = $localize`Next`
|
|
||||||
const endBtnTitle = $localize`End`
|
|
||||||
|
|
||||||
this.tourService.initialize(
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
anchorId: 'tour.dashboard',
|
anchorId: 'tour.dashboard',
|
||||||
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
|
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
|
||||||
@@ -205,8 +195,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchorId: 'tour.tags',
|
anchorId: 'tour.tags',
|
||||||
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
content: $localize`Attributes like tags, correspondents, document types, storage paths and custom fields can all be managed here. They can also be created from the document edit view.`,
|
||||||
route: '/tags',
|
route: '/attributes/tags',
|
||||||
backdropConfig: {
|
backdropConfig: {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
},
|
},
|
||||||
@@ -256,19 +246,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
])
|
||||||
{
|
|
||||||
enableBackdrop: true,
|
|
||||||
backdropConfig: {
|
|
||||||
offset: 10,
|
|
||||||
},
|
|
||||||
prevBtnTitle,
|
|
||||||
nextBtnTitle,
|
|
||||||
endBtnTitle,
|
|
||||||
isOptional: true,
|
|
||||||
useLegacyTitle: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
this.tourService.start$.subscribe(() => {
|
this.tourService.start$.subscribe(() => {
|
||||||
this.renderer.addClass(document.body, 'tour-active')
|
this.renderer.addClass(document.body, 'tour-active')
|
||||||
|
|||||||
@@ -222,8 +222,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
||||||
<option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option>
|
<option [ngValue]="PdfZoomScale.PageWidth" i18n>Fit width</option>
|
||||||
<option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option>
|
<option [ngValue]="PdfZoomScale.PageFit" i18n>Fit page</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import {
|
import {
|
||||||
@@ -147,6 +148,7 @@ describe('SettingsComponent', () => {
|
|||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
|
|||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
|
import { PdfZoomScale } from '../../common/pdf-viewer/pdf-viewer.types'
|
||||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
import { ZoomSetting } from '../../document-detail/zoom-setting'
|
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
@@ -196,7 +196,7 @@ export class SettingsComponent
|
|||||||
|
|
||||||
public readonly GlobalSearchType = GlobalSearchType
|
public readonly GlobalSearchType = GlobalSearchType
|
||||||
|
|
||||||
public readonly ZoomSetting = ZoomSetting
|
public readonly PdfZoomScale = PdfZoomScale
|
||||||
|
|
||||||
public readonly PdfEditorEditMode = PdfEditorEditMode
|
public readonly PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
|
|||||||
@@ -175,44 +175,60 @@
|
|||||||
<span i18n>Manage</span>
|
<span i18n>Manage</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item app-link"
|
@if (canManageAttributes) {
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
<li class="nav-item app-link" tourAnchor="tour.tags">
|
||||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
<div class="d-flex align-items-center attributes-row">
|
||||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
<a class="nav-link flex-fill" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()"
|
||||||
|
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
<i-bs class="me-1" name="stack"></i-bs><span> <ng-container i18n>Attributes</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
@if (!slimSidebarEnabled) {
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
<button
|
||||||
tourAnchor="tour.tags">
|
type="button"
|
||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
(click)="toggleAttributesSections($event)"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
|
||||||
|
i18n-aria-label
|
||||||
|
>
|
||||||
|
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="attributes-submenu ms-2"
|
||||||
|
[ngbCollapse]="slimSidebarEnabled || attributesSectionsCollapsed"
|
||||||
|
>
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
|
||||||
|
<a class="nav-link py-1" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link"
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
</a>
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
</li>
|
||||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
|
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||||
|
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document types</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link py-1" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
||||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link py-1" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
|
||||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom fields</ng-container></span>
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
|
|||||||
@@ -177,6 +177,15 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attributes-row .attributes-expand-btn {
|
||||||
|
opacity: 0.2;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-row:hover .attributes-expand-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ActivatedRoute, Router } from '@angular/router'
|
|||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
@@ -27,7 +28,10 @@ import {
|
|||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import {
|
||||||
|
PermissionType,
|
||||||
|
PermissionsService,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
@@ -157,6 +161,7 @@ describe('AppFrameComponent', () => {
|
|||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@@ -256,7 +261,7 @@ describe('AppFrameComponent', () => {
|
|||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
component.toggleSlimSidebar()
|
component.toggleSlimSidebar()
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
.match(`${environment.apiBaseUrl}ui_settings/`)[0]
|
||||||
.flush('error', {
|
.flush('error', {
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: 'error',
|
statusText: 'error',
|
||||||
@@ -371,4 +376,103 @@ describe('AppFrameComponent', () => {
|
|||||||
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
||||||
expect(maybeRefreshSpy).toHaveBeenCalled()
|
expect(maybeRefreshSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should indicate attributes management availability when any permission is granted', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserCan')
|
||||||
|
.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.Tag
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(component.canManageAttributes).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should indicate attributes management availability for other permission types', () => {
|
||||||
|
const canSpy = jest
|
||||||
|
.spyOn(permissionsService, 'currentUserCan')
|
||||||
|
.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.Correspondent
|
||||||
|
})
|
||||||
|
expect(component.canManageAttributes).toBe(true)
|
||||||
|
|
||||||
|
canSpy.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.DocumentType
|
||||||
|
})
|
||||||
|
expect(component.canManageAttributes).toBe(true)
|
||||||
|
|
||||||
|
canSpy.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.StoragePath
|
||||||
|
})
|
||||||
|
expect(component.canManageAttributes).toBe(true)
|
||||||
|
|
||||||
|
canSpy.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.CustomField
|
||||||
|
})
|
||||||
|
expect(component.canManageAttributes).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle attributes sections and stop event bubbling', () => {
|
||||||
|
const preventDefault = jest.fn()
|
||||||
|
const stopPropagation = jest.fn()
|
||||||
|
const setSpy = jest.spyOn(settingsService, 'set')
|
||||||
|
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
||||||
|
|
||||||
|
component.toggleAttributesSections({
|
||||||
|
preventDefault,
|
||||||
|
stopPropagation,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(preventDefault).toHaveBeenCalled()
|
||||||
|
expect(stopPropagation).toHaveBeenCalled()
|
||||||
|
expect(setSpy).toHaveBeenCalledWith(
|
||||||
|
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
['attributes']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error when saving slim sidebar setting fails', () => {
|
||||||
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
|
jest
|
||||||
|
.spyOn(settingsService, 'storeSettings')
|
||||||
|
.mockReturnValue(throwError(() => new Error('boom')))
|
||||||
|
|
||||||
|
component.slimSidebarEnabled = true
|
||||||
|
|
||||||
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error when saving attributes collapsed setting fails', () => {
|
||||||
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
|
jest
|
||||||
|
.spyOn(settingsService, 'storeSettings')
|
||||||
|
.mockReturnValue(throwError(() => new Error('boom')))
|
||||||
|
|
||||||
|
component.attributesSectionsCollapsed = true
|
||||||
|
|
||||||
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should persist attributes section collapse state', () => {
|
||||||
|
const setSpy = jest.spyOn(settingsService, 'set')
|
||||||
|
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
||||||
|
|
||||||
|
component.attributesSectionsCollapsed = true
|
||||||
|
|
||||||
|
expect(setSpy).toHaveBeenCalledWith(
|
||||||
|
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
['attributes']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should collapse attributes sections when enabling slim sidebar', () => {
|
||||||
|
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
||||||
|
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
|
||||||
|
settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false)
|
||||||
|
|
||||||
|
component.toggleSlimSidebar()
|
||||||
|
|
||||||
|
expect(component.attributesSectionsCollapsed).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import {
|
|||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { CollapsibleSection, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||||
@@ -69,7 +69,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrap,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppFrameComponent
|
export class AppFrameComponent
|
||||||
@@ -141,11 +141,20 @@ export class AppFrameComponent
|
|||||||
toggleSlimSidebar(): void {
|
toggleSlimSidebar(): void {
|
||||||
this.slimSidebarAnimating = true
|
this.slimSidebarAnimating = true
|
||||||
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
||||||
|
if (this.slimSidebarEnabled) {
|
||||||
|
this.attributesSectionsCollapsed = true
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.slimSidebarAnimating = false
|
this.slimSidebarAnimating = false
|
||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleAttributesSections(event?: Event): void {
|
||||||
|
event?.preventDefault()
|
||||||
|
event?.stopPropagation()
|
||||||
|
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
|
||||||
|
}
|
||||||
|
|
||||||
get versionString(): string {
|
get versionString(): string {
|
||||||
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
||||||
}
|
}
|
||||||
@@ -167,6 +176,31 @@ export class AppFrameComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canManageAttributes(): boolean {
|
||||||
|
return (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Tag
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Correspondent
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.DocumentType
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.StoragePath
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.CustomField
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
@@ -186,6 +220,31 @@ export class AppFrameComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get attributesSectionsCollapsed(): boolean {
|
||||||
|
return this.settingsService
|
||||||
|
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
|
||||||
|
?.includes(CollapsibleSection.ATTRIBUTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
set attributesSectionsCollapsed(collapsed: boolean) {
|
||||||
|
// TODO: refactor to be able to toggle individual sections, if implemented
|
||||||
|
this.settingsService.set(
|
||||||
|
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
collapsed ? [CollapsibleSection.ATTRIBUTES] : []
|
||||||
|
)
|
||||||
|
this.settingsService
|
||||||
|
.storeSettings()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred while saving settings.`
|
||||||
|
)
|
||||||
|
console.warn(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { Component, Input, inject } from '@angular/core'
|
|||||||
import { Title } from '@angular/platform-browser'
|
import { Title } from '@angular/platform-browser'
|
||||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-page-header',
|
selector: 'pngx-page-header',
|
||||||
templateUrl: './page-header.component.html',
|
templateUrl: './page-header.component.html',
|
||||||
styleUrls: ['./page-header.component.scss'],
|
styleUrls: ['./page-header.component.scss'],
|
||||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrapModule],
|
imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrap],
|
||||||
})
|
})
|
||||||
export class PageHeaderComponent {
|
export class PageHeaderComponent {
|
||||||
private titleService = inject(Title)
|
private titleService = inject(Title)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
|
<pngx-pdf-viewer class="visually-hidden" [src]="pdfSrc" [renderMode]="PdfRenderMode.Single" [page]="1" [selectable]="false" (afterLoadComplete)="pdfLoaded($event)"></pngx-pdf-viewer>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">{{ title }}</h4>
|
<h4 class="modal-title">{{ title }}</h4>
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<span class="placeholder w-100 h-100"></span>
|
<span class="placeholder w-100 h-100"></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false" (page-rendered)="p.loaded = true"></pdf-viewer>
|
<pngx-pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [renderMode]="PdfRenderMode.Single" (rendered)="p.loaded = true"></pngx-pdf-viewer>
|
||||||
} @placeholder {
|
} @placeholder {
|
||||||
<div class="placeholder-glow w-100 h-100 z-10">
|
<div class="placeholder-glow w-100 h-100 z-10">
|
||||||
<span class="placeholder w-100 h-100"></span>
|
<span class="placeholder w-100 h-100"></span>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
background-color: gray;
|
background-color: gray;
|
||||||
height: 240px;
|
height: 240px;
|
||||||
|
|
||||||
pdf-viewer {
|
pngx-pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ng2-pdf-viewer-container {
|
::ng-deep .pngx-pdf-viewer-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import {
|
|||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
||||||
|
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
|
||||||
|
import {
|
||||||
|
PdfRenderMode,
|
||||||
|
PngxPdfDocumentProxy,
|
||||||
|
} from '../pdf-viewer/pdf-viewer.types'
|
||||||
import { PdfEditorEditMode } from './pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from './pdf-editor-edit-mode'
|
||||||
|
|
||||||
interface PageOperation {
|
interface PageOperation {
|
||||||
@@ -29,11 +33,12 @@ interface PageOperation {
|
|||||||
imports: [
|
imports: [
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
PdfViewerModule,
|
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
PngxPdfViewerComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PDFEditorComponent extends ConfirmDialogComponent {
|
export class PDFEditorComponent extends ConfirmDialogComponent {
|
||||||
|
PdfRenderMode = PdfRenderMode
|
||||||
public PdfEditorEditMode = PdfEditorEditMode
|
public PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
private documentService = inject(DocumentService)
|
private documentService = inject(DocumentService)
|
||||||
@@ -53,7 +58,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
|
|||||||
return this.documentService.getPreviewUrl(this.documentID)
|
return this.documentService.getPreviewUrl(this.documentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfLoaded(pdf: PDFDocumentProxy) {
|
pdfLoaded(pdf: PngxPdfDocumentProxy) {
|
||||||
this.totalPages = pdf.numPages
|
this.totalPages = pdf.numPages
|
||||||
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
|
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
|
||||||
page: i + 1,
|
page: i + 1,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div #container class="pngx-pdf-viewer-container">
|
||||||
|
<div #viewer class="pdfViewer"></div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pngx-pdf-viewer-container {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer {
|
||||||
|
--scale-factor: 1;
|
||||||
|
--page-bg-color: unset;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer .page {
|
||||||
|
--user-unit: 1;
|
||||||
|
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
|
||||||
|
--scale-round-x: 1px;
|
||||||
|
--scale-round-y: 1px;
|
||||||
|
direction: ltr;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
border: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
background-clip: content-box;
|
||||||
|
background-color: var(--page-bg-color, rgb(255 255 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer > .page:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer.singlePageView {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer.singlePageView .page {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer .canvasWrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer .canvasWrapper canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
contain: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer {
|
||||||
|
position: absolute;
|
||||||
|
text-align: initial;
|
||||||
|
inset: 0;
|
||||||
|
overflow: clip;
|
||||||
|
opacity: 1;
|
||||||
|
line-height: 1;
|
||||||
|
text-size-adjust: none;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
caret-color: CanvasText;
|
||||||
|
z-index: 0;
|
||||||
|
user-select: text;
|
||||||
|
--min-font-size: 1;
|
||||||
|
--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));
|
||||||
|
--min-font-size-inv: calc(1 / var(--min-font-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer.highlighting {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer :is(span, br) {
|
||||||
|
position: absolute;
|
||||||
|
white-space: pre;
|
||||||
|
color: transparent;
|
||||||
|
cursor: text;
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer > :not(.markedContent),
|
||||||
|
:host ::ng-deep .textLayer .markedContent span:not(.markedContent) {
|
||||||
|
z-index: 1;
|
||||||
|
--font-height: 0;
|
||||||
|
font-size: calc(var(--text-scale-factor) * var(--font-height));
|
||||||
|
--scale-x: 1;
|
||||||
|
--rotate: 0deg;
|
||||||
|
transform: rotate(var(--rotate)) scaleX(var(--scale-x))
|
||||||
|
scale(var(--min-font-size-inv));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer .markedContent {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer span[role='img'] {
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer .highlight {
|
||||||
|
--highlight-bg-color: rgb(180 0 170 / 0.25);
|
||||||
|
--highlight-selected-bg-color: rgb(0 100 0 / 0.25);
|
||||||
|
--highlight-backdrop-filter: none;
|
||||||
|
--highlight-selected-backdrop-filter: none;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 1px;
|
||||||
|
background-color: var(--highlight-bg-color);
|
||||||
|
backdrop-filter: var(--highlight-backdrop-filter);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .appended:is(.textLayer .highlight) {
|
||||||
|
position: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .begin:is(.textLayer .highlight) {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .end:is(.textLayer .highlight) {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .middle:is(.textLayer .highlight) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .selected:is(.textLayer .highlight) {
|
||||||
|
background-color: var(--highlight-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer ::selection {
|
||||||
|
background: rgba(30, 100, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .annotationLayer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { SimpleChange } from '@angular/core'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||||
|
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
|
||||||
|
import { PngxPdfViewerComponent } from './pdf-viewer.component'
|
||||||
|
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
|
||||||
|
|
||||||
|
describe('PngxPdfViewerComponent', () => {
|
||||||
|
let fixture: ComponentFixture<PngxPdfViewerComponent>
|
||||||
|
let component: PngxPdfViewerComponent
|
||||||
|
|
||||||
|
const initComponent = async (src = 'test.pdf') => {
|
||||||
|
component.src = src
|
||||||
|
fixture.detectChanges()
|
||||||
|
await fixture.whenStable()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PngxPdfViewerComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PngxPdfViewerComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads a document and emits events', async () => {
|
||||||
|
const loadSpy = jest.fn()
|
||||||
|
const renderedSpy = jest.fn()
|
||||||
|
component.afterLoadComplete.subscribe(loadSpy)
|
||||||
|
component.rendered.subscribe(renderedSpy)
|
||||||
|
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
|
||||||
|
'/assets/js/pdf.worker.min.mjs'
|
||||||
|
)
|
||||||
|
const isVisible = (component as any).findController.onIsPageVisible as
|
||||||
|
| (() => boolean)
|
||||||
|
| undefined
|
||||||
|
expect(isVisible?.()).toBe(true)
|
||||||
|
expect(loadSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ numPages: 1 })
|
||||||
|
)
|
||||||
|
expect(renderedSpy).toHaveBeenCalled()
|
||||||
|
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes single-page viewer and disables text layer', async () => {
|
||||||
|
component.renderMode = PdfRenderMode.Single
|
||||||
|
component.selectable = false
|
||||||
|
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFSinglePageViewer & {
|
||||||
|
options: Record<string, unknown>
|
||||||
|
}
|
||||||
|
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
|
||||||
|
expect(viewer.options.textLayerMode).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies zoom, rotation, and page changes', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const pageSpy = jest.fn()
|
||||||
|
component.pageChange.subscribe(pageSpy)
|
||||||
|
|
||||||
|
component.zoomScale = PdfZoomScale.PageFit
|
||||||
|
component.zoom = PdfZoomLevel.Two
|
||||||
|
component.rotation = 90
|
||||||
|
component.page = 2
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
zoomScale: new SimpleChange(
|
||||||
|
PdfZoomScale.PageWidth,
|
||||||
|
PdfZoomScale.PageFit,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
zoom: new SimpleChange(PdfZoomLevel.One, PdfZoomLevel.Two, false),
|
||||||
|
rotation: new SimpleChange(undefined, 90, false),
|
||||||
|
page: new SimpleChange(undefined, 2, false),
|
||||||
|
})
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
expect(viewer.pagesRotation).toBe(90)
|
||||||
|
expect(viewer.currentPageNumber).toBe(2)
|
||||||
|
expect(pageSpy).toHaveBeenCalledWith(2)
|
||||||
|
|
||||||
|
viewer.currentScale = 1
|
||||||
|
;(component as any).applyScale()
|
||||||
|
expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
|
||||||
|
expect(viewer.currentScale).toBe(2)
|
||||||
|
|
||||||
|
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
|
||||||
|
component.page = 2
|
||||||
|
;(component as any).lastViewerPage = 2
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
expect((component as any).lastViewerPage).toBeUndefined()
|
||||||
|
expect(applyScaleSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dispatches find when search query changes after render', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
|
||||||
|
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
|
||||||
|
|
||||||
|
;(component as any).hasRenderedPage = true
|
||||||
|
component.searchQuery = 'needle'
|
||||||
|
component.ngOnChanges({
|
||||||
|
searchQuery: new SimpleChange('', 'needle', false),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(dispatchSpy).toHaveBeenCalledWith('find', {
|
||||||
|
query: 'needle',
|
||||||
|
caseSensitive: false,
|
||||||
|
highlightAll: true,
|
||||||
|
phraseSearch: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
searchQuery: new SimpleChange('needle', 'needle', false),
|
||||||
|
})
|
||||||
|
expect(dispatchSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits error when document load fails', async () => {
|
||||||
|
const errorSpy = jest.fn()
|
||||||
|
component.loadError.subscribe(errorSpy)
|
||||||
|
|
||||||
|
jest.spyOn(pdfjs, 'getDocument').mockImplementationOnce(() => {
|
||||||
|
return {
|
||||||
|
promise: Promise.reject(new Error('boom')),
|
||||||
|
destroy: jest.fn(),
|
||||||
|
} as any
|
||||||
|
})
|
||||||
|
|
||||||
|
await initComponent('bad.pdf')
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cleans up resources on destroy', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as { cleanup: jest.Mock }
|
||||||
|
const loadingTask = (component as any).loadingTask as unknown as {
|
||||||
|
destroy: jest.Mock
|
||||||
|
}
|
||||||
|
const resizeObserver = (component as any).resizeObserver as unknown as {
|
||||||
|
disconnect: jest.Mock
|
||||||
|
}
|
||||||
|
const eventBus = (component as any).eventBus as { off: jest.Mock }
|
||||||
|
|
||||||
|
jest.spyOn(viewer, 'cleanup')
|
||||||
|
jest.spyOn(loadingTask, 'destroy')
|
||||||
|
jest.spyOn(resizeObserver, 'disconnect')
|
||||||
|
jest.spyOn(eventBus, 'off')
|
||||||
|
|
||||||
|
component.ngOnDestroy()
|
||||||
|
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith(
|
||||||
|
'pagerendered',
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith('pagesinit', expect.any(Function))
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith(
|
||||||
|
'pagechanging',
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
expect(resizeObserver.disconnect).toHaveBeenCalled()
|
||||||
|
expect(loadingTask.destroy).toHaveBeenCalled()
|
||||||
|
expect(viewer.cleanup).toHaveBeenCalled()
|
||||||
|
expect((component as any).pdfViewer).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips work when viewer is missing or has no pages', () => {
|
||||||
|
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
|
||||||
|
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
|
||||||
|
;(component as any).dispatchFindIfReady()
|
||||||
|
expect(dispatchSpy).not.toHaveBeenCalled()
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
;(component as any).applyScale()
|
||||||
|
|
||||||
|
const viewer = new PDFViewer({ eventBus: undefined })
|
||||||
|
viewer.pagesCount = 0
|
||||||
|
;(component as any).pdfViewer = viewer
|
||||||
|
viewer.currentScale = 5
|
||||||
|
;(component as any).applyScale()
|
||||||
|
expect(viewer.currentScale).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns early on src change in ngOnChanges', () => {
|
||||||
|
const loadSpy = jest.spyOn(component as any, 'loadDocument')
|
||||||
|
const initSpy = jest.spyOn(component as any, 'initViewer')
|
||||||
|
const scaleSpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
|
const resizeSpy = jest.spyOn(component as any, 'setupResizeObserver')
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
src: new SimpleChange(undefined, 'test.pdf', true),
|
||||||
|
zoomScale: new SimpleChange(
|
||||||
|
PdfZoomScale.PageWidth,
|
||||||
|
PdfZoomScale.PageFit,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(loadSpy).toHaveBeenCalled()
|
||||||
|
expect(resizeSpy).not.toHaveBeenCalled()
|
||||||
|
expect(initSpy).not.toHaveBeenCalled()
|
||||||
|
expect(scaleSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies viewer state after view init when already loaded', () => {
|
||||||
|
const applySpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
|
;(component as any).hasLoaded = true
|
||||||
|
;(component as any).pdf = { numPages: 1 }
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(applySpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips viewer state after view init when no pdf is available', () => {
|
||||||
|
const applySpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
|
;(component as any).hasLoaded = true
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(applySpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not reload when already loaded', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
|
||||||
|
const callCount = getDocumentSpy.mock.calls.length
|
||||||
|
await (component as any).loadDocument()
|
||||||
|
|
||||||
|
expect(getDocumentSpy).toHaveBeenCalledTimes(callCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('runs applyScale on resize observer notifications', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const applySpy = jest.spyOn(component as any, 'applyScale')
|
||||||
|
const resizeObserver = (component as any).resizeObserver as {
|
||||||
|
trigger: () => void
|
||||||
|
}
|
||||||
|
resizeObserver.trigger()
|
||||||
|
|
||||||
|
expect(applySpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips page work when no pages are available', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
viewer.pagesCount = 0
|
||||||
|
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
|
||||||
|
|
||||||
|
component.page = undefined
|
||||||
|
;(component as any).lastViewerPage = 1
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
|
||||||
|
expect(applyScaleSpy).not.toHaveBeenCalled()
|
||||||
|
expect((component as any).lastViewerPage).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to a default zoom when input is invalid', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
viewer.currentScale = 3
|
||||||
|
component.zoom = 'not-a-number' as PdfZoomLevel
|
||||||
|
;(component as any).applyScale()
|
||||||
|
|
||||||
|
expect(viewer.currentScale).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-initializes viewer on selectable or render mode changes', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const initSpy = jest.spyOn(component as any, 'initViewer')
|
||||||
|
component.selectable = false
|
||||||
|
component.renderMode = PdfRenderMode.Single
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
selectable: new SimpleChange(true, false, false),
|
||||||
|
renderMode: new SimpleChange(
|
||||||
|
PdfRenderMode.All,
|
||||||
|
PdfRenderMode.Single,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(initSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import {
|
||||||
|
getDocument,
|
||||||
|
GlobalWorkerOptions,
|
||||||
|
PDFDocumentLoadingTask,
|
||||||
|
PDFDocumentProxy,
|
||||||
|
} from 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||||
|
import {
|
||||||
|
EventBus,
|
||||||
|
PDFFindController,
|
||||||
|
PDFLinkService,
|
||||||
|
PDFSinglePageViewer,
|
||||||
|
PDFViewer,
|
||||||
|
} from 'pdfjs-dist/web/pdf_viewer.mjs'
|
||||||
|
import {
|
||||||
|
PdfRenderMode,
|
||||||
|
PdfSource,
|
||||||
|
PdfZoomLevel,
|
||||||
|
PdfZoomScale,
|
||||||
|
PngxPdfDocumentProxy,
|
||||||
|
} from './pdf-viewer.types'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-pdf-viewer',
|
||||||
|
templateUrl: './pdf-viewer.component.html',
|
||||||
|
styleUrl: './pdf-viewer.component.scss',
|
||||||
|
})
|
||||||
|
export class PngxPdfViewerComponent
|
||||||
|
implements AfterViewInit, OnChanges, OnDestroy
|
||||||
|
{
|
||||||
|
@Input() src!: PdfSource
|
||||||
|
@Input() page?: number
|
||||||
|
@Output() pageChange = new EventEmitter<number>()
|
||||||
|
@Input() rotation?: number
|
||||||
|
@Input() renderMode: PdfRenderMode = PdfRenderMode.All
|
||||||
|
@Input() selectable = true
|
||||||
|
@Input() searchQuery = ''
|
||||||
|
@Input() zoom: PdfZoomLevel = PdfZoomLevel.One
|
||||||
|
@Input() zoomScale: PdfZoomScale = PdfZoomScale.PageWidth
|
||||||
|
|
||||||
|
@Output() afterLoadComplete = new EventEmitter<PngxPdfDocumentProxy>()
|
||||||
|
@Output() rendered = new EventEmitter<void>()
|
||||||
|
@Output() loadError = new EventEmitter<unknown>()
|
||||||
|
|
||||||
|
@ViewChild('container', { static: true })
|
||||||
|
private readonly container!: ElementRef<HTMLDivElement>
|
||||||
|
|
||||||
|
@ViewChild('viewer', { static: true })
|
||||||
|
private readonly viewer!: ElementRef<HTMLDivElement>
|
||||||
|
|
||||||
|
private hasLoaded = false
|
||||||
|
private loadingTask?: PDFDocumentLoadingTask
|
||||||
|
private resizeObserver?: ResizeObserver
|
||||||
|
private pdf?: PDFDocumentProxy
|
||||||
|
private pdfViewer?: PDFViewer | PDFSinglePageViewer
|
||||||
|
private hasRenderedPage = false
|
||||||
|
private lastFindQuery = ''
|
||||||
|
private lastViewerPage?: number
|
||||||
|
|
||||||
|
private readonly eventBus = new EventBus()
|
||||||
|
private readonly linkService = new PDFLinkService({ eventBus: this.eventBus })
|
||||||
|
private readonly findController = new PDFFindController({
|
||||||
|
eventBus: this.eventBus,
|
||||||
|
linkService: this.linkService,
|
||||||
|
updateMatchesCountOnProgress: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
private readonly onPageRendered = () => {
|
||||||
|
this.hasRenderedPage = true
|
||||||
|
this.dispatchFindIfReady()
|
||||||
|
this.rendered.emit()
|
||||||
|
}
|
||||||
|
private readonly onPagesInit = () => this.applyScale()
|
||||||
|
private readonly onPageChanging = (evt: { pageNumber: number }) => {
|
||||||
|
// Avoid [(page)] two-way binding re-triggers navigation
|
||||||
|
this.lastViewerPage = evt.pageNumber
|
||||||
|
this.pageChange.emit(evt.pageNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['src']) {
|
||||||
|
this.hasLoaded = false
|
||||||
|
this.loadDocument()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes['zoomScale']) {
|
||||||
|
this.setupResizeObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes['selectable'] || changes['renderMode']) {
|
||||||
|
this.initViewer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changes['page'] ||
|
||||||
|
changes['zoom'] ||
|
||||||
|
changes['zoomScale'] ||
|
||||||
|
changes['rotation']
|
||||||
|
) {
|
||||||
|
this.applyViewerState()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes['searchQuery']) {
|
||||||
|
this.dispatchFindIfReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.setupResizeObserver()
|
||||||
|
this.initViewer()
|
||||||
|
if (!this.hasLoaded) {
|
||||||
|
this.loadDocument()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.pdf) {
|
||||||
|
this.applyViewerState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.eventBus.off('pagerendered', this.onPageRendered)
|
||||||
|
this.eventBus.off('pagesinit', this.onPagesInit)
|
||||||
|
this.eventBus.off('pagechanging', this.onPageChanging)
|
||||||
|
this.resizeObserver?.disconnect()
|
||||||
|
this.loadingTask?.destroy()
|
||||||
|
this.pdfViewer?.cleanup()
|
||||||
|
this.pdfViewer = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadDocument(): Promise<void> {
|
||||||
|
if (this.hasLoaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasLoaded = true
|
||||||
|
this.hasRenderedPage = false
|
||||||
|
this.lastFindQuery = ''
|
||||||
|
this.loadingTask?.destroy()
|
||||||
|
|
||||||
|
GlobalWorkerOptions.workerSrc = '/assets/js/pdf.worker.min.mjs'
|
||||||
|
this.loadingTask = getDocument(this.src)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdf = await this.loadingTask.promise
|
||||||
|
this.pdf = pdf
|
||||||
|
this.linkService.setDocument(pdf)
|
||||||
|
this.findController.onIsPageVisible = () => true
|
||||||
|
this.pdfViewer?.setDocument(pdf)
|
||||||
|
this.applyViewerState()
|
||||||
|
this.afterLoadComplete.emit(pdf)
|
||||||
|
} catch (err) {
|
||||||
|
this.loadError.emit(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupResizeObserver(): void {
|
||||||
|
this.resizeObserver?.disconnect()
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.applyScale()
|
||||||
|
})
|
||||||
|
this.resizeObserver.observe(this.container.nativeElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private initViewer(): void {
|
||||||
|
this.viewer.nativeElement.innerHTML = ''
|
||||||
|
this.pdfViewer?.cleanup()
|
||||||
|
this.hasRenderedPage = false
|
||||||
|
this.lastFindQuery = ''
|
||||||
|
|
||||||
|
const textLayerMode = this.selectable === false ? 0 : 1
|
||||||
|
const options = {
|
||||||
|
container: this.container.nativeElement,
|
||||||
|
viewer: this.viewer.nativeElement,
|
||||||
|
eventBus: this.eventBus,
|
||||||
|
linkService: this.linkService,
|
||||||
|
findController: this.findController,
|
||||||
|
textLayerMode,
|
||||||
|
removePageBorders: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pdfViewer =
|
||||||
|
this.renderMode === PdfRenderMode.Single
|
||||||
|
? new PDFSinglePageViewer(options)
|
||||||
|
: new PDFViewer(options)
|
||||||
|
this.linkService.setViewer(this.pdfViewer)
|
||||||
|
|
||||||
|
this.eventBus.off('pagerendered', this.onPageRendered)
|
||||||
|
this.eventBus.off('pagesinit', this.onPagesInit)
|
||||||
|
this.eventBus.off('pagechanging', this.onPageChanging)
|
||||||
|
this.eventBus.on('pagerendered', this.onPageRendered)
|
||||||
|
this.eventBus.on('pagesinit', this.onPagesInit)
|
||||||
|
this.eventBus.on('pagechanging', this.onPageChanging)
|
||||||
|
|
||||||
|
if (this.pdf) {
|
||||||
|
this.pdfViewer.setDocument(this.pdf)
|
||||||
|
this.applyViewerState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyViewerState(): void {
|
||||||
|
if (!this.pdfViewer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hasPages = this.pdfViewer.pagesCount > 0
|
||||||
|
if (typeof this.rotation === 'number' && hasPages) {
|
||||||
|
this.pdfViewer.pagesRotation = this.rotation
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof this.page === 'number' &&
|
||||||
|
hasPages &&
|
||||||
|
this.page !== this.lastViewerPage
|
||||||
|
) {
|
||||||
|
this.pdfViewer.currentPageNumber = this.page
|
||||||
|
}
|
||||||
|
if (this.page === this.lastViewerPage) {
|
||||||
|
this.lastViewerPage = undefined
|
||||||
|
}
|
||||||
|
if (hasPages) {
|
||||||
|
this.applyScale()
|
||||||
|
}
|
||||||
|
this.dispatchFindIfReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyScale(): void {
|
||||||
|
if (!this.pdfViewer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.pdfViewer.pagesCount === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const zoomFactor = Number(this.zoom) || 1
|
||||||
|
this.pdfViewer.currentScaleValue = this.zoomScale
|
||||||
|
if (zoomFactor !== 1) {
|
||||||
|
this.pdfViewer.currentScale = this.pdfViewer.currentScale * zoomFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatchFindIfReady(): void {
|
||||||
|
if (!this.hasRenderedPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const query = this.searchQuery.trim()
|
||||||
|
if (query === this.lastFindQuery) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastFindQuery = query
|
||||||
|
this.eventBus.dispatch('find', {
|
||||||
|
query,
|
||||||
|
caseSensitive: false,
|
||||||
|
highlightAll: query.length > 0,
|
||||||
|
phraseSearch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export type PngxPdfDocumentProxy = {
|
||||||
|
numPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PdfSource = string | { url: string; password?: string }
|
||||||
|
|
||||||
|
export enum PdfRenderMode {
|
||||||
|
Single = 'single',
|
||||||
|
All = 'all',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfZoomScale {
|
||||||
|
PageFit = 'page-fit',
|
||||||
|
PageWidth = 'page-width',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfZoomLevel {
|
||||||
|
Quarter = '.25',
|
||||||
|
Half = '.5',
|
||||||
|
ThreeQuarters = '.75',
|
||||||
|
One = '1',
|
||||||
|
OneAndHalf = '1.5',
|
||||||
|
Two = '2',
|
||||||
|
Three = '3',
|
||||||
|
}
|
||||||
@@ -23,14 +23,12 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!requiresPassword) {
|
@if (!requiresPassword) {
|
||||||
<pdf-viewer
|
<pngx-pdf-viewer
|
||||||
[src]="previewUrl"
|
[src]="previewUrl"
|
||||||
[original-size]="false"
|
[renderMode]="PdfRenderMode.All"
|
||||||
[show-borders]="false"
|
[searchQuery]="documentService.searchQuery"
|
||||||
[show-all]="true"
|
(loadError)="onError($event)">
|
||||||
(text-layer-rendered)="onPageRendered()"
|
</pngx-pdf-viewer>
|
||||||
(error)="onError($event)" #pdfViewer>
|
|
||||||
</pdf-viewer>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { of, throwError } from 'rxjs'
|
|||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
|
||||||
import { PreviewPopupComponent } from './preview-popup.component'
|
import { PreviewPopupComponent } from './preview-popup.component'
|
||||||
|
|
||||||
const doc = {
|
const doc = {
|
||||||
@@ -78,7 +79,7 @@ describe('PreviewPopupComponent', () => {
|
|||||||
component.popover.open()
|
component.popover.open()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.query(By.css('object'))).toBeNull()
|
expect(fixture.debugElement.query(By.css('object'))).toBeNull()
|
||||||
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
|
expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show lock icon on password error', () => {
|
it('should show lock icon on password error', () => {
|
||||||
@@ -159,23 +160,15 @@ describe('PreviewPopupComponent', () => {
|
|||||||
expect(component.popover.isOpen()).toBeFalsy()
|
expect(component.popover.isOpen()).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should dispatch find event on viewer loaded if searchQuery set', () => {
|
it('should pass searchQuery to viewer', () => {
|
||||||
documentService.searchQuery = 'test'
|
documentService.searchQuery = 'test'
|
||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
||||||
component.popover.open()
|
component.popover.open()
|
||||||
jest.advanceTimersByTime(1000)
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
// normally setup by pdf-viewer
|
const viewer = fixture.debugElement.query(
|
||||||
jest.replaceProperty(component.pdfViewer, 'eventBus', {
|
By.directive(PngxPdfViewerComponent)
|
||||||
dispatch: jest.fn(),
|
)
|
||||||
} as any)
|
expect(viewer).not.toBeNull()
|
||||||
const dispatchSpy = jest.spyOn(component.pdfViewer.eventBus, 'dispatch')
|
expect(viewer.componentInstance.searchQuery).toBe('test')
|
||||||
component.onPageRendered()
|
|
||||||
expect(dispatchSpy).toHaveBeenCalledWith('find', {
|
|
||||||
query: 'test',
|
|
||||||
caseSensitive: false,
|
|
||||||
highlightAll: true,
|
|
||||||
phraseSearch: true,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
|
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
|
||||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PdfViewerComponent, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first, Subject, takeUntil } from 'rxjs'
|
import { first, Subject, takeUntil } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
@@ -10,6 +9,8 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
|||||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
|
||||||
|
import { PdfRenderMode } from '../pdf-viewer/pdf-viewer.types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-preview-popup',
|
selector: 'pngx-preview-popup',
|
||||||
@@ -18,14 +19,15 @@ import { SettingsService } from 'src/app/services/settings.service'
|
|||||||
imports: [
|
imports: [
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
PdfViewerModule,
|
PngxPdfViewerComponent,
|
||||||
SafeUrlPipe,
|
SafeUrlPipe,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PreviewPopupComponent implements OnDestroy {
|
export class PreviewPopupComponent implements OnDestroy {
|
||||||
|
PdfRenderMode = PdfRenderMode
|
||||||
private settingsService = inject(SettingsService)
|
private settingsService = inject(SettingsService)
|
||||||
private documentService = inject(DocumentService)
|
public readonly documentService = inject(DocumentService)
|
||||||
private http = inject(HttpClient)
|
private http = inject(HttpClient)
|
||||||
|
|
||||||
private _document: Document
|
private _document: Document
|
||||||
@@ -61,8 +63,6 @@ export class PreviewPopupComponent implements OnDestroy {
|
|||||||
|
|
||||||
@ViewChild('popover') popover: NgbPopover
|
@ViewChild('popover') popover: NgbPopover
|
||||||
|
|
||||||
@ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
|
|
||||||
|
|
||||||
mouseOnPreview: boolean = false
|
mouseOnPreview: boolean = false
|
||||||
|
|
||||||
popoverClass: string = 'shadow popover-preview'
|
popoverClass: string = 'shadow popover-preview'
|
||||||
@@ -114,18 +114,6 @@ export class PreviewPopupComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageRendered() {
|
|
||||||
// Only triggered by the pngx pdf viewer
|
|
||||||
if (this.documentService.searchQuery) {
|
|
||||||
this.pdfViewer.eventBus.dispatch('find', {
|
|
||||||
query: this.documentService.searchQuery,
|
|
||||||
caseSensitive: false,
|
|
||||||
highlightAll: true,
|
|
||||||
phraseSearch: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseEnterPreview() {
|
mouseEnterPreview() {
|
||||||
this.mouseOnPreview = true
|
this.mouseOnPreview = true
|
||||||
if (!this.popover.isOpen()) {
|
if (!this.popover.isOpen()) {
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import {
|
||||||
|
provideUiTour,
|
||||||
|
TourNgBootstrap,
|
||||||
|
TourService,
|
||||||
|
} from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
@@ -75,7 +79,7 @@ describe('DashboardComponent', () => {
|
|||||||
imports: [
|
imports: [
|
||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
RouterTestingModule,
|
RouterTestingModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrap,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
@@ -111,6 +115,7 @@ describe('DashboardComponent', () => {
|
|||||||
},
|
},
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
@@ -36,7 +36,7 @@ import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-widget.
|
|||||||
WelcomeWidgetComponent,
|
WelcomeWidgetComponent,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrap,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
@@ -61,6 +62,7 @@ describe('UploadFileWidgetComponent', () => {
|
|||||||
},
|
},
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@@ -33,7 +33,7 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
|||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrap,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { NgbAlert, NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbAlert, NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||||
import { WelcomeWidgetComponent } from './welcome-widget.component'
|
import { WelcomeWidgetComponent } from './welcome-widget.component'
|
||||||
@@ -11,7 +12,7 @@ describe('WelcomeWidgetComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [PermissionsGuard],
|
providers: [PermissionsGuard, provideUiTour()],
|
||||||
imports: [NgbAlertModule, WelcomeWidgetComponent, WidgetFrameComponent],
|
imports: [NgbAlertModule, WelcomeWidgetComponent, WidgetFrameComponent],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -456,17 +456,15 @@
|
|||||||
@case (ContentRenderType.PDF) {
|
@case (ContentRenderType.PDF) {
|
||||||
@if (!useNativePdfViewer) {
|
@if (!useNativePdfViewer) {
|
||||||
<div class="preview-sticky pdf-viewer-container">
|
<div class="preview-sticky pdf-viewer-container">
|
||||||
<pdf-viewer
|
<pngx-pdf-viewer
|
||||||
[src]="{ url: previewUrl, password: password }"
|
[src]="{ url: previewUrl, password: password }"
|
||||||
[original-size]="false"
|
[renderMode]="PdfRenderMode.All"
|
||||||
[show-borders]="true"
|
|
||||||
[show-all]="true"
|
|
||||||
[(page)]="previewCurrentPage"
|
[(page)]="previewCurrentPage"
|
||||||
[zoom-scale]="previewZoomScale"
|
[zoomScale]="previewZoomScale"
|
||||||
[zoom]="previewZoomSetting"
|
[zoom]="previewZoomSetting"
|
||||||
(error)="onError($event)"
|
(loadError)="onError($event)"
|
||||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
(afterLoadComplete)="pdfPreviewLoaded($event)">
|
||||||
</pdf-viewer>
|
</pngx-pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
|
|||||||
@@ -5,20 +5,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer-container {
|
.pdf-viewer-container {
|
||||||
padding-top: 10px;
|
padding: 8px;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
|
|
||||||
pdf-viewer {
|
pngx-pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ng2-pdf-viewer-container .page {
|
|
||||||
--page-margin: 0 auto 10px;
|
|
||||||
--page-border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group .dropdown-toggle-split {
|
.btn-group .dropdown-toggle-split {
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
border-bottom-right-radius: inherit;
|
border-bottom-right-radius: inherit;
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ import { environment } from 'src/environments/environment'
|
|||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||||
|
import {
|
||||||
|
PdfZoomLevel,
|
||||||
|
PdfZoomScale,
|
||||||
|
} from '../common/pdf-viewer/pdf-viewer.types'
|
||||||
import { DocumentDetailComponent } from './document-detail.component'
|
import { DocumentDetailComponent } from './document-detail.component'
|
||||||
import { ZoomSetting } from './zoom-setting'
|
|
||||||
|
|
||||||
const doc: Document = {
|
const doc: Document = {
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -860,7 +863,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
it('should support zoom controls', () => {
|
it('should support zoom controls', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.setZoom(ZoomSetting.One) // from select
|
component.setZoom(PdfZoomLevel.One) // from select
|
||||||
expect(component.previewZoomSetting).toEqual('1')
|
expect(component.previewZoomSetting).toEqual('1')
|
||||||
component.increaseZoom()
|
component.increaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('1.5')
|
expect(component.previewZoomSetting).toEqual('1.5')
|
||||||
@@ -868,18 +871,18 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(component.previewZoomSetting).toEqual('2')
|
expect(component.previewZoomSetting).toEqual('2')
|
||||||
component.decreaseZoom()
|
component.decreaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('1.5')
|
expect(component.previewZoomSetting).toEqual('1.5')
|
||||||
component.setZoom(ZoomSetting.One) // from select
|
component.setZoom(PdfZoomLevel.One) // from select
|
||||||
component.decreaseZoom()
|
component.decreaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('.75')
|
expect(component.previewZoomSetting).toEqual('.75')
|
||||||
|
|
||||||
component.setZoom(ZoomSetting.PageFit) // from select
|
component.setZoom(PdfZoomScale.PageFit) // from select
|
||||||
expect(component.previewZoomScale).toEqual('page-fit')
|
expect(component.previewZoomScale).toEqual('page-fit')
|
||||||
expect(component.previewZoomSetting).toEqual('1')
|
expect(component.previewZoomSetting).toEqual('1')
|
||||||
component.increaseZoom()
|
component.increaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('1.5')
|
expect(component.previewZoomSetting).toEqual('1.5')
|
||||||
expect(component.previewZoomScale).toEqual('page-width')
|
expect(component.previewZoomScale).toEqual('page-width')
|
||||||
|
|
||||||
component.setZoom(ZoomSetting.PageFit) // from select
|
component.setZoom(PdfZoomScale.PageFit) // from select
|
||||||
expect(component.previewZoomScale).toEqual('page-fit')
|
expect(component.previewZoomScale).toEqual('page-fit')
|
||||||
expect(component.previewZoomSetting).toEqual('1')
|
expect(component.previewZoomSetting).toEqual('1')
|
||||||
component.decreaseZoom()
|
component.decreaseZoom()
|
||||||
@@ -889,10 +892,10 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
it('should select correct zoom setting in dropdown', () => {
|
it('should select correct zoom setting in dropdown', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.setZoom(ZoomSetting.PageFit)
|
component.setZoom(PdfZoomScale.PageFit)
|
||||||
expect(component.currentZoom).toEqual(ZoomSetting.PageFit)
|
expect(component.currentZoom).toEqual(PdfZoomScale.PageFit)
|
||||||
component.setZoom(ZoomSetting.Quarter)
|
component.setZoom(PdfZoomLevel.Quarter)
|
||||||
expect(component.currentZoom).toEqual(ZoomSetting.Quarter)
|
expect(component.currentZoom).toEqual(PdfZoomLevel.Quarter)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support updating notes dynamically', () => {
|
it('should support updating notes dynamically', () => {
|
||||||
@@ -1017,7 +1020,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
||||||
expect(component.useNativePdfViewer).toBeFalsy()
|
expect(component.useNativePdfViewer).toBeFalsy()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
|
expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display native pdf viewer if enabled', () => {
|
it('should display native pdf viewer if enabled', () => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||||
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
||||||
@@ -108,13 +107,19 @@ import { UrlComponent } from '../common/input/url/url.component'
|
|||||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||||
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
||||||
|
import { PngxPdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
|
||||||
|
import {
|
||||||
|
PdfRenderMode,
|
||||||
|
PdfZoomLevel,
|
||||||
|
PdfZoomScale,
|
||||||
|
PngxPdfDocumentProxy,
|
||||||
|
} from '../common/pdf-viewer/pdf-viewer.types'
|
||||||
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
||||||
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
||||||
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
||||||
import { ZoomSetting } from './zoom-setting'
|
|
||||||
|
|
||||||
enum DocumentDetailNavIDs {
|
enum DocumentDetailNavIDs {
|
||||||
Details = 1,
|
Details = 1,
|
||||||
@@ -168,16 +173,17 @@ enum ContentRenderType {
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
PdfViewerModule,
|
|
||||||
TextAreaComponent,
|
TextAreaComponent,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
PngxPdfViewerComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentDetailComponent
|
export class DocumentDetailComponent
|
||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, OnDestroy, DirtyComponent
|
implements OnInit, OnDestroy, DirtyComponent
|
||||||
{
|
{
|
||||||
private documentsService = inject(DocumentService)
|
PdfRenderMode = PdfRenderMode
|
||||||
|
documentsService = inject(DocumentService)
|
||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
private tagService = inject(TagService)
|
private tagService = inject(TagService)
|
||||||
private correspondentService = inject(CorrespondentService)
|
private correspondentService = inject(CorrespondentService)
|
||||||
@@ -246,8 +252,8 @@ export class DocumentDetailComponent
|
|||||||
|
|
||||||
previewCurrentPage: number = 1
|
previewCurrentPage: number = 1
|
||||||
previewNumPages: number
|
previewNumPages: number
|
||||||
previewZoomSetting: ZoomSetting = ZoomSetting.One
|
previewZoomSetting: PdfZoomLevel = PdfZoomLevel.One
|
||||||
previewZoomScale: ZoomSetting = ZoomSetting.PageWidth
|
previewZoomScale: PdfZoomScale = PdfZoomScale.PageWidth
|
||||||
|
|
||||||
store: BehaviorSubject<any>
|
store: BehaviorSubject<any>
|
||||||
isDirty$: Observable<boolean>
|
isDirty$: Observable<boolean>
|
||||||
@@ -503,7 +509,9 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING))
|
this.setZoom(
|
||||||
|
this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale
|
||||||
|
)
|
||||||
this.documentForm.valueChanges
|
this.documentForm.valueChanges
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((values) => {
|
.subscribe((values) => {
|
||||||
@@ -1204,7 +1212,7 @@ export class DocumentDetailComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
pdfPreviewLoaded(pdf: PngxPdfDocumentProxy) {
|
||||||
this.previewNumPages = pdf.numPages
|
this.previewNumPages = pdf.numPages
|
||||||
if (this.password) this.requiresPassword = false
|
if (this.password) this.requiresPassword = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1225,31 +1233,33 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setZoom(setting: ZoomSetting) {
|
setZoom(setting: PdfZoomScale | PdfZoomLevel) {
|
||||||
if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) {
|
if (
|
||||||
|
setting === PdfZoomScale.PageFit ||
|
||||||
|
setting === PdfZoomScale.PageWidth
|
||||||
|
) {
|
||||||
this.previewZoomScale = setting
|
this.previewZoomScale = setting
|
||||||
this.previewZoomSetting = ZoomSetting.One
|
this.previewZoomSetting = PdfZoomLevel.One
|
||||||
} else {
|
return
|
||||||
this.previewZoomSetting = setting
|
|
||||||
this.previewZoomScale = ZoomSetting.PageWidth
|
|
||||||
}
|
}
|
||||||
|
this.previewZoomSetting = setting
|
||||||
|
this.previewZoomScale = PdfZoomScale.PageWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
get zoomSettings() {
|
get zoomSettings() {
|
||||||
return Object.values(ZoomSetting).filter(
|
return [PdfZoomScale.PageFit, ...Object.values(PdfZoomLevel)]
|
||||||
(setting) => setting !== ZoomSetting.PageWidth
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentZoom() {
|
get currentZoom() {
|
||||||
if (this.previewZoomScale === ZoomSetting.PageFit) {
|
if (this.previewZoomScale === PdfZoomScale.PageFit) {
|
||||||
return ZoomSetting.PageFit
|
return PdfZoomScale.PageFit
|
||||||
} else return this.previewZoomSetting
|
}
|
||||||
|
return this.previewZoomSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
getZoomSettingTitle(setting: ZoomSetting): string {
|
getZoomSettingTitle(setting: PdfZoomScale | PdfZoomLevel): string {
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case ZoomSetting.PageFit:
|
case PdfZoomScale.PageFit:
|
||||||
return $localize`Page Fit`
|
return $localize`Page Fit`
|
||||||
default:
|
default:
|
||||||
return `${parseFloat(setting) * 100}%`
|
return `${parseFloat(setting) * 100}%`
|
||||||
@@ -1257,25 +1267,24 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
increaseZoom(): void {
|
increaseZoom(): void {
|
||||||
let currentIndex = Object.values(ZoomSetting).indexOf(
|
const zoomLevels = Object.values(PdfZoomLevel)
|
||||||
this.previewZoomSetting
|
let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
|
||||||
)
|
if (this.previewZoomScale === PdfZoomScale.PageFit) {
|
||||||
if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 5
|
currentIndex = zoomLevels.indexOf(PdfZoomLevel.One)
|
||||||
this.previewZoomScale = ZoomSetting.PageWidth
|
}
|
||||||
|
this.previewZoomScale = PdfZoomScale.PageWidth
|
||||||
this.previewZoomSetting =
|
this.previewZoomSetting =
|
||||||
Object.values(ZoomSetting)[
|
zoomLevels[Math.min(zoomLevels.length - 1, currentIndex + 1)]
|
||||||
Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decreaseZoom(): void {
|
decreaseZoom(): void {
|
||||||
let currentIndex = Object.values(ZoomSetting).indexOf(
|
const zoomLevels = Object.values(PdfZoomLevel)
|
||||||
this.previewZoomSetting
|
let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
|
||||||
)
|
if (this.previewZoomScale === PdfZoomScale.PageFit) {
|
||||||
if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 4
|
currentIndex = zoomLevels.indexOf(PdfZoomLevel.ThreeQuarters)
|
||||||
this.previewZoomScale = ZoomSetting.PageWidth
|
}
|
||||||
this.previewZoomSetting =
|
this.previewZoomScale = PdfZoomScale.PageWidth
|
||||||
Object.values(ZoomSetting)[Math.max(2, currentIndex - 1)]
|
this.previewZoomSetting = zoomLevels[Math.max(0, currentIndex - 1)]
|
||||||
}
|
}
|
||||||
|
|
||||||
get showPermissions(): boolean {
|
get showPermissions(): boolean {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export enum ZoomSetting {
|
|
||||||
PageFit = 'page-fit',
|
|
||||||
PageWidth = 'page-width',
|
|
||||||
Quarter = '.25',
|
|
||||||
Half = '.5',
|
|
||||||
ThreeQuarters = '.75',
|
|
||||||
One = '1',
|
|
||||||
OneAndHalf = '1.5',
|
|
||||||
Two = '2',
|
|
||||||
Three = '3',
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Subject, of, throwError } from 'rxjs'
|
import { Subject, of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import {
|
import {
|
||||||
@@ -105,6 +106,7 @@ describe('DocumentListComponent', () => {
|
|||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
DEFAULT_DISPLAY_FIELDS,
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
@@ -99,7 +99,7 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgClass,
|
NgClass,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrap,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentListComponent
|
export class DocumentListComponent
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
@@ -251,6 +252,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
SettingsService,
|
SettingsService,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
NgbTypeaheadModule,
|
NgbTypeaheadModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Observable, Subject, from } from 'rxjs'
|
import { Observable, Subject, from } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
@@ -251,7 +251,7 @@ const DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS = [
|
|||||||
NgbTypeaheadModule,
|
NgbTypeaheadModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrap,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class FilterEditorComponent
|
export class FilterEditorComponent
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
<pngx-page-header
|
|
||||||
title="Custom Fields"
|
|
||||||
i18n-title
|
|
||||||
info="Customize the data fields that can be attached to documents."
|
|
||||||
i18n-info
|
|
||||||
infoLink="usage/#custom-fields"
|
|
||||||
>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
|
||||||
</button>
|
|
||||||
</pngx-page-header>
|
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
@@ -26,9 +26,9 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
|||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
import { CustomFieldEditDialogComponent } from '../../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../common/page-header/page-header.component'
|
||||||
import { CustomFieldsComponent } from './custom-fields.component'
|
import { CustomFieldsComponent } from './custom-fields.component'
|
||||||
|
|
||||||
const fields: CustomField[] = [
|
const fields: CustomField[] = [
|
||||||
@@ -110,10 +110,7 @@ describe('CustomFieldsComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reload')
|
const reloadSpy = jest.spyOn(component, 'reload')
|
||||||
|
|
||||||
const createButton = fixture.debugElement
|
component.editField()
|
||||||
.queryAll(By.css('button'))
|
|
||||||
.find((btn) => btn.nativeElement.textContent.trim().includes('Add Field'))
|
|
||||||
createButton.triggerEventHandler('click')
|
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
|
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
|
||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { delay, takeUntil, tap } from 'rxjs'
|
import { delay, takeUntil, tap } from 'rxjs'
|
||||||
|
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
||||||
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
||||||
import {
|
import {
|
||||||
CustomFieldQueryLogicalOperator,
|
CustomFieldQueryLogicalOperator,
|
||||||
@@ -21,18 +25,12 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
|||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-custom-fields',
|
selector: 'pngx-custom-fields',
|
||||||
templateUrl: './custom-fields.component.html',
|
templateUrl: './custom-fields.component.html',
|
||||||
styleUrls: ['./custom-fields.component.scss'],
|
styleUrls: ['./custom-fields.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
PageHeaderComponent,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
@@ -44,14 +42,14 @@ export class CustomFieldsComponent
|
|||||||
extends LoadingComponentWithPermissions
|
extends LoadingComponentWithPermissions
|
||||||
implements OnInit
|
implements OnInit
|
||||||
{
|
{
|
||||||
private customFieldsService = inject(CustomFieldsService)
|
private readonly customFieldsService = inject(CustomFieldsService)
|
||||||
permissionsService = inject(PermissionsService)
|
public readonly permissionsService = inject(PermissionsService)
|
||||||
private modalService = inject(NgbModal)
|
private readonly modalService = inject(NgbModal)
|
||||||
private toastService = inject(ToastService)
|
private readonly toastService = inject(ToastService)
|
||||||
private documentListViewService = inject(DocumentListViewService)
|
private readonly documentListViewService = inject(DocumentListViewService)
|
||||||
private settingsService = inject(SettingsService)
|
private readonly settingsService = inject(SettingsService)
|
||||||
private documentService = inject(DocumentService)
|
private readonly documentService = inject(DocumentService)
|
||||||
private savedViewService = inject(SavedViewService)
|
private readonly savedViewService = inject(SavedViewService)
|
||||||
|
|
||||||
public fields: CustomField[] = []
|
public fields: CustomField[] = []
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<pngx-page-header
|
||||||
|
[title]="activeTabLabel"
|
||||||
|
info="Manage tags, correspondents, document types, storage paths, and custom fields."
|
||||||
|
i18n-info
|
||||||
|
[infoLink]="activeInfoLink"
|
||||||
|
[loading]="activeHeaderLoading"
|
||||||
|
>
|
||||||
|
@if (activeAttributeList) {
|
||||||
|
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
||||||
|
<i-bs name="text-indent-left"></i-bs>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||||
|
@if (activeAttributeList.selectedObjects.size > 0) {
|
||||||
|
<pngx-clearable-badge [selected]="activeAttributeList.selectedObjects.size > 0" [number]="activeAttributeList.selectedObjects.size" (cleared)="activeAttributeList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
||||||
|
<button ngbDropdownItem (click)="activeAttributeList.selectNone()" i18n>Select none</button>
|
||||||
|
<button ngbDropdownItem (click)="activeAttributeList.selectPage(true)" i18n>Select page</button>
|
||||||
|
<button ngbDropdownItem (click)="activeAttributeList.selectAll()" i18n>Select all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none d-sm-flex flex-fill me-3">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text border-0" i18n>Select:</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm flex-nowrap">
|
||||||
|
@if (activeAttributeList.selectedObjects.size > 0) {
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="activeAttributeList.selectNone()">
|
||||||
|
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.selectPage(true)">
|
||||||
|
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.selectAll()">
|
||||||
|
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.setPermissions()"
|
||||||
|
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Change) || activeAttributeList.selectedObjects.size === 0">
|
||||||
|
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="activeAttributeList.delete()"
|
||||||
|
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Delete) || activeAttributeList.selectedObjects.size === 0">
|
||||||
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="activeAttributeList.openCreateDialog()"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.Add, type: activeAttributeList.permissionType }">
|
||||||
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
||||||
|
</button>
|
||||||
|
} @else if (activeCustomFields) {
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeCustomFields.editField()"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
||||||
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</pngx-page-header>
|
||||||
|
|
||||||
|
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-underline">
|
||||||
|
@for (section of visibleSections; track section.id) {
|
||||||
|
<li [ngbNavItem]="section.id">
|
||||||
|
<a ngbNavLink >
|
||||||
|
<i-bs class="me-2" [name]="section.icon"></i-bs>{{ section.label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="my-3 shadow-sm">
|
||||||
|
<ng-container
|
||||||
|
[ngComponentOutlet]="activeSection?.component"
|
||||||
|
#activeOutlet="ngComponentOutlet"
|
||||||
|
></ng-container>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
convertToParamMap,
|
||||||
|
ParamMap,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router'
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
import { DocumentAttributesComponent } from './document-attributes.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-dummy-section',
|
||||||
|
template: '',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
class DummySectionComponent {}
|
||||||
|
|
||||||
|
describe('DocumentAttributesComponent', () => {
|
||||||
|
let component: DocumentAttributesComponent
|
||||||
|
let fixture: ComponentFixture<DocumentAttributesComponent>
|
||||||
|
let router: Router
|
||||||
|
let paramMapSubject: Subject<ParamMap>
|
||||||
|
let permissionsService: PermissionsService
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
paramMapSubject = new Subject<ParamMap>()
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
DocumentAttributesComponent,
|
||||||
|
DummySectionComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
paramMap: paramMapSubject.asObservable(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PermissionsService,
|
||||||
|
useValue: {
|
||||||
|
currentUserCan: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DocumentAttributesComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
router = TestBed.inject(Router)
|
||||||
|
permissionsService = TestBed.inject(PermissionsService)
|
||||||
|
|
||||||
|
jest.spyOn(router, 'navigate').mockResolvedValue(true)
|
||||||
|
;(component as any).sections = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
path: 'tags',
|
||||||
|
label: 'Tags',
|
||||||
|
icon: 'tags',
|
||||||
|
permissionType: PermissionType.Tag,
|
||||||
|
kind: 'attributeList',
|
||||||
|
component: DummySectionComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
path: 'customfields',
|
||||||
|
label: 'Custom fields',
|
||||||
|
icon: 'ui-radios',
|
||||||
|
permissionType: PermissionType.CustomField,
|
||||||
|
kind: 'customFields',
|
||||||
|
component: DummySectionComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate to default section when no section is provided', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
||||||
|
(action, type) => {
|
||||||
|
return action === PermissionAction.View && type === PermissionType.Tag
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({}))
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'tags'], {
|
||||||
|
replaceUrl: true,
|
||||||
|
})
|
||||||
|
expect(component.activeNavID).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set active section from route param when valid', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
||||||
|
(action, type) => {
|
||||||
|
return (
|
||||||
|
action === PermissionAction.View &&
|
||||||
|
type === PermissionType.CustomField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
||||||
|
|
||||||
|
expect(component.activeNavID).toBe(2)
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update active nav id when route section changes', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(true)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.activeNavID = 1
|
||||||
|
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
||||||
|
|
||||||
|
expect(component.activeNavID).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect to dashboard when no sections are visible', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(false)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({}))
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/dashboard'], {
|
||||||
|
replaceUrl: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate when a nav change occurs', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({ section: 'tags' }))
|
||||||
|
|
||||||
|
component.onNavChange({ nextId: 2 } as any)
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'customfields'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore nav changes for unknown sections', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(true)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({ section: 'tags' }))
|
||||||
|
|
||||||
|
component.onNavChange({ nextId: 999 } as any)
|
||||||
|
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import { NgComponentOutlet } from '@angular/common'
|
||||||
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Type,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import {
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbNavChangeEvent,
|
||||||
|
NgbNavModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { CustomFieldsComponent } from './custom-fields/custom-fields.component'
|
||||||
|
import { CorrespondentListComponent } from './management-list/correspondent-list/correspondent-list.component'
|
||||||
|
import { DocumentTypeListComponent } from './management-list/document-type-list/document-type-list.component'
|
||||||
|
import { ManagementListComponent } from './management-list/management-list.component'
|
||||||
|
import { StoragePathListComponent } from './management-list/storage-path-list/storage-path-list.component'
|
||||||
|
import { TagListComponent } from './management-list/tag-list/tag-list.component'
|
||||||
|
|
||||||
|
enum DocumentAttributesNavIDs {
|
||||||
|
Tags = 1,
|
||||||
|
Correspondents = 2,
|
||||||
|
DocumentTypes = 3,
|
||||||
|
StoragePaths = 4,
|
||||||
|
CustomFields = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DocumentAttributesSectionKind {
|
||||||
|
ManagementList = 'managementList',
|
||||||
|
CustomFields = 'customFields',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentAttributesSection {
|
||||||
|
id: DocumentAttributesNavIDs
|
||||||
|
path: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
infoLink?: string
|
||||||
|
permissionType: PermissionType
|
||||||
|
kind: DocumentAttributesSectionKind
|
||||||
|
component: Type<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-document-attributes',
|
||||||
|
templateUrl: './document-attributes.component.html',
|
||||||
|
styleUrls: ['./document-attributes.component.scss'],
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgComponentOutlet,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
ClearableBadgeComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DocumentAttributesComponent
|
||||||
|
implements OnInit, OnDestroy, AfterViewChecked
|
||||||
|
{
|
||||||
|
private readonly permissionsService = inject(PermissionsService)
|
||||||
|
private readonly activatedRoute = inject(ActivatedRoute)
|
||||||
|
private readonly router = inject(Router)
|
||||||
|
private readonly cdr = inject(ChangeDetectorRef)
|
||||||
|
private readonly unsubscribeNotifier = new Subject<void>()
|
||||||
|
|
||||||
|
protected readonly PermissionAction = PermissionAction
|
||||||
|
protected readonly PermissionType = PermissionType
|
||||||
|
|
||||||
|
readonly sections: DocumentAttributesSection[] = [
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.Tags,
|
||||||
|
path: 'tags',
|
||||||
|
label: $localize`Tags`,
|
||||||
|
icon: 'tags',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.Tag,
|
||||||
|
kind: DocumentAttributesSectionKind.ManagementList,
|
||||||
|
component: TagListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.Correspondents,
|
||||||
|
path: 'correspondents',
|
||||||
|
label: $localize`Correspondents`,
|
||||||
|
icon: 'person',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.Correspondent,
|
||||||
|
kind: DocumentAttributesSectionKind.ManagementList,
|
||||||
|
component: CorrespondentListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.DocumentTypes,
|
||||||
|
path: 'documenttypes',
|
||||||
|
label: $localize`Document types`,
|
||||||
|
icon: 'hash',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.DocumentType,
|
||||||
|
kind: DocumentAttributesSectionKind.ManagementList,
|
||||||
|
component: DocumentTypeListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.StoragePaths,
|
||||||
|
path: 'storagepaths',
|
||||||
|
label: $localize`Storage paths`,
|
||||||
|
icon: 'folder',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.StoragePath,
|
||||||
|
kind: DocumentAttributesSectionKind.ManagementList,
|
||||||
|
component: StoragePathListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.CustomFields,
|
||||||
|
path: 'customfields',
|
||||||
|
label: $localize`Custom fields`,
|
||||||
|
icon: 'ui-radios',
|
||||||
|
infoLink: 'usage/#custom-fields',
|
||||||
|
permissionType: PermissionType.CustomField,
|
||||||
|
kind: DocumentAttributesSectionKind.CustomFields,
|
||||||
|
component: CustomFieldsComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@ViewChild('activeOutlet', { read: NgComponentOutlet })
|
||||||
|
private readonly activeOutlet?: NgComponentOutlet
|
||||||
|
|
||||||
|
private lastHeaderLoading: boolean
|
||||||
|
|
||||||
|
activeNavID: number = null
|
||||||
|
|
||||||
|
get visibleSections(): DocumentAttributesSection[] {
|
||||||
|
return this.sections.filter((section) =>
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
section.permissionType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeSection(): DocumentAttributesSection | null {
|
||||||
|
return (
|
||||||
|
this.visibleSections.find((section) => section.id === this.activeNavID) ??
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeAttributeList(): ManagementListComponent<any> | null {
|
||||||
|
if (
|
||||||
|
this.activeSection?.kind !== DocumentAttributesSectionKind.ManagementList
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
const instance = this.activeOutlet?.componentInstance
|
||||||
|
return instance instanceof ManagementListComponent ? instance : null
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeCustomFields(): CustomFieldsComponent | null {
|
||||||
|
if (this.activeSection?.kind !== DocumentAttributesSectionKind.CustomFields)
|
||||||
|
return null
|
||||||
|
const instance = this.activeOutlet?.componentInstance
|
||||||
|
return instance instanceof CustomFieldsComponent ? instance : null
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeTabLabel(): string {
|
||||||
|
return this.activeSection?.label ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeInfoLink(): string {
|
||||||
|
return this.activeSection?.infoLink ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeHeaderLoading(): boolean {
|
||||||
|
return (
|
||||||
|
this.activeAttributeList?.loading ??
|
||||||
|
this.activeCustomFields?.loading ??
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.activatedRoute.paramMap
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((paramMap) => {
|
||||||
|
const section = paramMap.get('section')
|
||||||
|
const navIDFromSection =
|
||||||
|
this.getNavIDForSection(section) ?? this.getDefaultNavID()
|
||||||
|
|
||||||
|
if (navIDFromSection == null) {
|
||||||
|
this.router.navigate(['/dashboard'], { replaceUrl: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeNavID !== navIDFromSection) {
|
||||||
|
this.activeNavID = navIDFromSection
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!section || this.getNavIDForSection(section) == null) {
|
||||||
|
this.router.navigate(
|
||||||
|
['attributes', this.getSectionForNavID(this.activeNavID)],
|
||||||
|
{ replaceUrl: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribeNotifier.next()
|
||||||
|
this.unsubscribeNotifier.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked(): void {
|
||||||
|
const current = this.activeHeaderLoading
|
||||||
|
if (this.lastHeaderLoading !== current) {
|
||||||
|
this.lastHeaderLoading = current
|
||||||
|
this.cdr.detectChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNavChange(navChangeEvent: NgbNavChangeEvent): void {
|
||||||
|
const nextSection = this.getSectionForNavID(navChangeEvent.nextId)
|
||||||
|
if (!nextSection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.router.navigate(['attributes', nextSection])
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultNavID(): DocumentAttributesNavIDs | null {
|
||||||
|
return this.visibleSections[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNavIDForSection(section: string): DocumentAttributesNavIDs | null {
|
||||||
|
const path = section?.toLowerCase()
|
||||||
|
if (!path) return null
|
||||||
|
|
||||||
|
const found = this.visibleSections.find((s) => s.path === path)
|
||||||
|
return found?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSectionForNavID(navID: number): string | null {
|
||||||
|
const section = this.visibleSections.find((s) => s.id === navID)
|
||||||
|
return section?.path ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { CorrespondentListComponent } from './correspondent-list.component'
|
import { CorrespondentListComponent } from './correspondent-list.component'
|
||||||
|
|
||||||
describe('CorrespondentListComponent', () => {
|
describe('CorrespondentListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { CorrespondentEditDialogComponent } from 'src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@@ -14,21 +15,16 @@ import { SortableDirective } from 'src/app/directives/sortable.directive'
|
|||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-correspondent-list',
|
selector: 'pngx-correspondent-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
providers: [{ provide: CustomDatePipe }],
|
providers: [{ provide: CustomDatePipe }],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
@@ -37,11 +33,10 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
||||||
private datePipe = inject(CustomDatePipe)
|
private readonly datePipe = inject(CustomDatePipe)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { DocumentTypeListComponent } from './document-type-list.component'
|
import { DocumentTypeListComponent } from './document-type-list.component'
|
||||||
|
|
||||||
describe('DocumentTypeListComponent', () => {
|
describe('DocumentTypeListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,25 +7,21 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { DocumentTypeEditDialogComponent } from 'src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-document-type-list',
|
selector: 'pngx-document-type-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -35,7 +31,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
||||||
@@ -1,50 +1,3 @@
|
|||||||
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions" [loading]="loading">
|
|
||||||
|
|
||||||
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
|
||||||
<i-bs name="text-indent-left"></i-bs>
|
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
|
||||||
@if (selectedObjects.size > 0) {
|
|
||||||
<pngx-clearable-badge [selected]="selectedObjects.size > 0" [number]="selectedObjects.size" (cleared)="selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
|
||||||
<button ngbDropdownItem (click)="selectNone()" i18n>Select none</button>
|
|
||||||
<button ngbDropdownItem (click)="selectPage(true)" i18n>Select page</button>
|
|
||||||
<button ngbDropdownItem (click)="selectAll()" i18n>Select all</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-none d-sm-flex flex-fill me-3">
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<span class="input-group-text border-0" i18n>Select:</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group btn-group-sm flex-nowrap">
|
|
||||||
@if (selectedObjects.size > 0) {
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="selectNone()">
|
|
||||||
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="selectPage(true)">
|
|
||||||
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="selectAll()">
|
|
||||||
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
|
||||||
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
|
|
||||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
|
||||||
</button>
|
|
||||||
</pngx-page-header>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col mb-2 mb-xl-0">
|
<div class="col mb-2 mb-xl-0">
|
||||||
<div class="form-inline d-flex align-items-center">
|
<div class="form-inline d-flex align-items-center">
|
||||||
@@ -76,19 +29,19 @@
|
|||||||
<table class="table table-striped align-middle shadow-sm mb-0">
|
<table class="table table-striped align-middle shadow-sm mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th>
|
||||||
<div class="form-check m-0 ms-2 me-n2">
|
<div class="form-check m-0 ms-2 me-n2">
|
||||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (change)="selectPage($event.target.checked); $event.stopPropagation();">
|
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (change)="$event.target.checked ? selectPage() : clearSelection(); $event.stopPropagation();">
|
||||||
<label class="form-check-label" for="all-objects"></label>
|
<label class="form-check-label" for="all-objects"></label>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
<th class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
<th class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
<th class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
@for (column of extraColumns; track column) {
|
@for (column of extraColumns; track column) {
|
||||||
<th scope="col" class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
<th class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||||
}
|
}
|
||||||
<th scope="col" class="fw-normal" i18n>Actions</th>
|
<th class="fw-normal" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -131,16 +84,16 @@
|
|||||||
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td scope="row" class="name-cell" style="--depth: {{depth}}">
|
<td class="name-cell" style="--depth: {{depth}}">
|
||||||
@if (depth > 0) {
|
@if (depth > 0) {
|
||||||
<div class="indicator"></div>
|
<div class="indicator"></div>
|
||||||
}
|
}
|
||||||
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
||||||
</td>
|
</td>
|
||||||
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
<td class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||||
<td scope="row">{{ getDocumentCount(object) }}</td>
|
<td>{{ getDocumentCount(object) }}</td>
|
||||||
@for (column of extraColumns; track column) {
|
@for (column of extraColumns; track column) {
|
||||||
<td scope="row" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
<td [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
||||||
@if (column.badgeFn) {
|
@if (column.badgeFn) {
|
||||||
<span
|
<span
|
||||||
class="badge"
|
class="badge"
|
||||||
@@ -156,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
<td scope="row">
|
<td>
|
||||||
<div class="btn-toolbar gap-2">
|
<div class="btn-toolbar gap-2">
|
||||||
<div class="btn-group d-block d-sm-none">
|
<div class="btn-group d-block d-sm-none">
|
||||||
<div ngbDropdown container="body" class="d-inline-block">
|
<div ngbDropdown container="body" class="d-inline-block">
|
||||||
@@ -44,12 +44,12 @@ import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-fil
|
|||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogComponent } from '../../../common/edit-dialog/edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../common/page-header/page-header.component'
|
||||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
import { PermissionsDialogComponent } from '../../../common/permissions-dialog/permissions-dialog.component'
|
||||||
import { TagListComponent } from '../tag-list/tag-list.component'
|
|
||||||
import { ManagementListComponent } from './management-list.component'
|
import { ManagementListComponent } from './management-list.component'
|
||||||
|
import { TagListComponent } from './tag-list/tag-list.component'
|
||||||
|
|
||||||
const tags: Tag[] = [
|
const tags: Tag[] = [
|
||||||
{
|
{
|
||||||
@@ -304,12 +304,12 @@ describe('ManagementListComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('selectPage should select current page items or clear selection', () => {
|
it('selectPage should select current page items or clear selection', () => {
|
||||||
component.selectPage(true)
|
component.selectPage()
|
||||||
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
||||||
expect(component.togggleAll).toBe(true)
|
expect(component.togggleAll).toBe(true)
|
||||||
|
|
||||||
component.togggleAll = true
|
component.togggleAll = true
|
||||||
component.selectPage(false)
|
component.clearSelection()
|
||||||
expect(component.selectedObjects.size).toBe(0)
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
expect(component.togggleAll).toBe(false)
|
expect(component.togggleAll).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs/operators'
|
} from 'rxjs/operators'
|
||||||
|
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
||||||
import {
|
import {
|
||||||
MATCH_AUTO,
|
MATCH_AUTO,
|
||||||
MATCH_NONE,
|
MATCH_NONE,
|
||||||
@@ -40,10 +44,6 @@ import {
|
|||||||
} from 'src/app/services/rest/abstract-name-filter-service'
|
} from 'src/app/services/rest/abstract-name-filter-service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
|
||||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
|
||||||
|
|
||||||
export interface ManagementListColumn {
|
export interface ManagementListColumn {
|
||||||
key: string
|
key: string
|
||||||
@@ -69,13 +69,14 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
protected service: AbstractNameFilterService<T>
|
protected service: AbstractNameFilterService<T>
|
||||||
private modalService: NgbModal = inject(NgbModal)
|
private readonly modalService: NgbModal = inject(NgbModal)
|
||||||
protected editDialogComponent: any
|
protected editDialogComponent: any
|
||||||
private toastService: ToastService = inject(ToastService)
|
private readonly toastService: ToastService = inject(ToastService)
|
||||||
private documentListViewService: DocumentListViewService = inject(
|
private readonly documentListViewService: DocumentListViewService = inject(
|
||||||
DocumentListViewService
|
DocumentListViewService
|
||||||
)
|
)
|
||||||
private permissionsService: PermissionsService = inject(PermissionsService)
|
private readonly permissionsService: PermissionsService =
|
||||||
|
inject(PermissionsService)
|
||||||
protected filterRuleType: number
|
protected filterRuleType: number
|
||||||
public typeName: string
|
public typeName: string
|
||||||
public typeNamePlural: string
|
public typeNamePlural: string
|
||||||
@@ -196,7 +197,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
openCreateDialog() {
|
openCreateDialog() {
|
||||||
var activeModal = this.modalService.open(this.editDialogComponent, {
|
const activeModal = this.modalService.open(this.editDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
|
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||||
@@ -215,7 +216,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
openEditDialog(object: T) {
|
openEditDialog(object: T) {
|
||||||
var activeModal = this.modalService.open(this.editDialogComponent, {
|
const activeModal = this.modalService.open(this.editDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.object = object
|
activeModal.componentInstance.object = object
|
||||||
@@ -243,7 +244,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
openDeleteDialog(object: T) {
|
openDeleteDialog(object: T) {
|
||||||
var activeModal = this.modalService.open(ConfirmDialogComponent, {
|
const activeModal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.title = $localize`Confirm delete`
|
activeModal.componentInstance.title = $localize`Confirm delete`
|
||||||
@@ -343,13 +344,9 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
selectPage(select: boolean) {
|
selectPage() {
|
||||||
if (select) {
|
|
||||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||||
this.togggleAll = this.areAllPageItemsSelected()
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
} else {
|
|
||||||
this.clearSelection()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAll() {
|
selectAll() {
|
||||||
@@ -10,7 +10,7 @@ import { StoragePath } from 'src/app/data/storage-path'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { StoragePathListComponent } from './storage-path-list.component'
|
import { StoragePathListComponent } from './storage-path-list.component'
|
||||||
|
|
||||||
describe('StoragePathListComponent', () => {
|
describe('StoragePathListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,25 +7,21 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { StoragePathEditDialogComponent } from 'src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-storage-path-list',
|
selector: 'pngx-storage-path-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -35,7 +31,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { TagListComponent } from './tag-list.component'
|
import { TagListComponent } from './tag-list.component'
|
||||||
|
|
||||||
describe('TagListComponent', () => {
|
describe('TagListComponent', () => {
|
||||||
@@ -138,12 +138,12 @@ describe('TagListComponent', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component.data = [parent as any]
|
component.data = [parent as any]
|
||||||
component.selectPage(true)
|
component.selectPage()
|
||||||
|
|
||||||
expect(component.selectedObjects.has(10)).toBe(true)
|
expect(component.selectedObjects.has(10)).toBe(true)
|
||||||
expect(component.selectedObjects.has(11)).toBe(true)
|
expect(component.selectedObjects.has(11)).toBe(true)
|
||||||
|
|
||||||
component.selectPage(false)
|
component.clearSelection()
|
||||||
expect(component.selectedObjects.size).toBe(0)
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,25 +7,21 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { TagEditDialogComponent } from 'src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-tag-list',
|
selector: 'pngx-tag-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -35,7 +31,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TagListComponent extends ManagementListComponent<Tag> {
|
export class TagListComponent extends ManagementListComponent<Tag> {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
|
||||||
import { ZoomSetting } from '../components/document-detail/zoom-setting'
|
import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
@@ -19,6 +19,10 @@ export enum GlobalSearchType {
|
|||||||
TITLE_CONTENT = 'title-content',
|
TITLE_CONTENT = 'title-content',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CollapsibleSection {
|
||||||
|
ATTRIBUTES = 'attributes',
|
||||||
|
}
|
||||||
|
|
||||||
export const PAPERLESS_GREEN_HEX = '#17541f'
|
export const PAPERLESS_GREEN_HEX = '#17541f'
|
||||||
|
|
||||||
export const SETTINGS_KEYS = {
|
export const SETTINGS_KEYS = {
|
||||||
@@ -51,6 +55,8 @@ export const SETTINGS_KEYS = {
|
|||||||
NOTES_ENABLED: 'general-settings:notes-enabled',
|
NOTES_ENABLED: 'general-settings:notes-enabled',
|
||||||
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
||||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||||
|
ATTRIBUTES_SECTIONS_COLLAPSED:
|
||||||
|
'general-settings:attributes-sections-collapsed',
|
||||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||||
UPDATE_CHECKING_BACKEND_SETTING:
|
UPDATE_CHECKING_BACKEND_SETTING:
|
||||||
'general-settings:update-checking:backend-setting',
|
'general-settings:update-checking:backend-setting',
|
||||||
@@ -112,6 +118,11 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
type: 'array',
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -310,7 +321,7 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: ZoomSetting.PageWidth,
|
default: PdfZoomScale.PageWidth,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.AI_ENABLED,
|
key: SETTINGS_KEYS.AI_ENABLED,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { TestBed } from '@angular/core/testing'
|
|||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { routes } from '../app-routing.module'
|
import { routes } from '../app-routing.module'
|
||||||
import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
|
||||||
import { DocumentListComponent } from '../components/document-list/document-list.component'
|
import { DocumentListComponent } from '../components/document-list/document-list.component'
|
||||||
@@ -30,6 +31,7 @@ describe('DirtySavedViewGuard', () => {
|
|||||||
DocumentListComponent,
|
DocumentListComponent,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { TestBed } from '@angular/core/testing'
|
import { TestBed } from '@angular/core/testing'
|
||||||
import { ActivatedRoute, RouterState } from '@angular/router'
|
import { ActivatedRoute, RouterState } from '@angular/router'
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { provideUiTour, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
} from '../services/permissions.service'
|
} from '../services/permissions.service'
|
||||||
import { ToastService } from '../services/toast.service'
|
import { ToastService } from '../services/toast.service'
|
||||||
import { PermissionsGuard } from './permissions.guard'
|
import { PermissionsGuard } from './permissions.guard'
|
||||||
@@ -45,6 +45,7 @@ describe('PermissionsGuard', () => {
|
|||||||
},
|
},
|
||||||
TourService,
|
TourService,
|
||||||
ToastService,
|
ToastService,
|
||||||
|
provideUiTour(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -95,4 +96,52 @@ describe('PermissionsGuard', () => {
|
|||||||
expect(canActivate).toHaveProperty('root') // returns UrlTree
|
expect(canActivate).toHaveProperty('root') // returns UrlTree
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should activate when any required permission is granted', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserCan')
|
||||||
|
.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.Tag
|
||||||
|
})
|
||||||
|
|
||||||
|
const canActivate = guard.canActivate(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
requiredPermissionAny: [
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.Tag },
|
||||||
|
{
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
routerState.snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(canActivate).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not activate when no required permission is granted', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserCan')
|
||||||
|
.mockImplementation(() => false)
|
||||||
|
|
||||||
|
const canActivate = guard.canActivate(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
requiredPermissionAny: [
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.Tag },
|
||||||
|
{
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
routerState.snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(canActivate).toHaveProperty('root')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,12 +20,20 @@ export class PermissionsGuard {
|
|||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot
|
state: RouterStateSnapshot
|
||||||
): boolean | UrlTree {
|
): boolean | UrlTree {
|
||||||
|
const requiredPermissionAny: { action: any; type: any }[] =
|
||||||
|
route.data.requiredPermissionAny
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
||||||
(route.data.requiredPermission &&
|
(route.data.requiredPermission &&
|
||||||
!this.permissionsService.currentUserCan(
|
!this.permissionsService.currentUserCan(
|
||||||
route.data.requiredPermission.action,
|
route.data.requiredPermission.action,
|
||||||
route.data.requiredPermission.type
|
route.data.requiredPermission.type
|
||||||
|
)) ||
|
||||||
|
(Array.isArray(requiredPermissionAny) &&
|
||||||
|
requiredPermissionAny.length > 0 &&
|
||||||
|
!requiredPermissionAny.some((p) =>
|
||||||
|
this.permissionsService.currentUserCan(p.action, p.type)
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
// Check if tour is running 1 = TourState.ON
|
// Check if tour is running 1 = TourState.ON
|
||||||
|
|||||||
@@ -70,4 +70,26 @@ describe('LocalizedDateParserFormatter', () => {
|
|||||||
dateStr = dateParserFormatter.format(dateStruct)
|
dateStr = dateParserFormatter.format(dateStruct)
|
||||||
expect(dateStr).toEqual('04.05.2023')
|
expect(dateStr).toEqual('04.05.2023')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should handle years when current year % 100 < 50', () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
jest.setSystemTime(new Date(2026, 5, 15))
|
||||||
|
let val = dateParserFormatter.parse('5/4/26')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 2026 })
|
||||||
|
|
||||||
|
val = dateParserFormatter.parse('5/4/75')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 2075 })
|
||||||
|
|
||||||
|
val = dateParserFormatter.parse('5/4/99')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 1999 })
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle years when current year % 100 >= 50', () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
jest.setSystemTime(new Date(2076, 5, 15))
|
||||||
|
const val = dateParserFormatter.parse('5/4/00')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 2100 })
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -106,15 +106,25 @@ export class LocalizedDateParserFormatter extends NgbDateParserFormatter {
|
|||||||
value = this.preformatDateInput(value)
|
value = this.preformatDateInput(value)
|
||||||
let match = this.getDateParseRegex().exec(value)
|
let match = this.getDateParseRegex().exec(value)
|
||||||
if (match) {
|
if (match) {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const currentCentury = currentYear - (currentYear % 100)
|
||||||
|
|
||||||
|
let year = +match.groups.year
|
||||||
|
if (year < 100) {
|
||||||
|
let fourDigitYear = currentCentury + year
|
||||||
|
// Mimic python-dateutil: keep result within -50/+49 years of current year
|
||||||
|
if (fourDigitYear > currentYear + 49) {
|
||||||
|
fourDigitYear -= 100
|
||||||
|
} else if (fourDigitYear <= currentYear - 50) {
|
||||||
|
fourDigitYear += 100
|
||||||
|
}
|
||||||
|
year = fourDigitYear
|
||||||
|
}
|
||||||
|
|
||||||
let dateStruct = {
|
let dateStruct = {
|
||||||
day: +match.groups.day,
|
day: +match.groups.day,
|
||||||
month: +match.groups.month,
|
month: +match.groups.month,
|
||||||
year: +match.groups.year,
|
year,
|
||||||
}
|
|
||||||
if (dateStruct.year <= new Date().getFullYear() - 2000) {
|
|
||||||
dateStruct.year += 2000
|
|
||||||
} else if (dateStruct.year < 100) {
|
|
||||||
dateStruct.year += 1900
|
|
||||||
}
|
}
|
||||||
return dateStruct
|
return dateStruct
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user