Compare commits

..

26 Commits

Author SHA1 Message Date
shamoon cdfb67f71b some edit dialogs to signals 2026-07-07 18:35:53 -07:00
shamoon c3a2794a01 permissions dialogs etc 2026-07-07 17:00:59 -07:00
shamoon 4dd84d7367 trash 2026-07-07 17:00:44 -07:00
shamoon 2f3e409a2e signals for logs and workflow components 2026-07-07 17:00:21 -07:00
shamoon b1e81615b0 signals for settings display fields 2026-07-07 16:59:46 -07:00
shamoon 1233a33b66 signals for open docuemtns service, saved views 2026-07-07 16:58:56 -07:00
shamoon f2bbd8bb65 Add signal handling to app frame, global search 2026-07-07 16:58:09 -07:00
shamoon 5a93d46a0c Convert management component, custom fields to signals 2026-07-07 16:56:23 -07:00
shamoon cca4d3dbb7 Fix doc detail view, touches settings too 2026-07-07 07:56:33 -07:00
shamoon 2b48ebaa59 Fix sidebar via saved view + tasks service signals 2026-07-06 20:24:51 -07:00
shamoon da23edc9eb Fix tasks / users-groups and mail 2026-07-06 20:19:13 -07:00
shamoon 872465ee8a Add back the nice fade-in 2026-07-06 20:13:41 -07:00
shamoon 3755070e1f Update some global components with loading signals 2026-07-06 20:09:58 -07:00
shamoon 0c5cfb2254 remove deprecated pnpm stuff from package.json 2026-07-05 18:03:32 -07:00
shamoon f3edc1ac36 Ok first real chunk of zoneless pngx
document list
dashboard / widget frame
stats
2026-07-04 09:19:37 -07:00
shamoon 2caa6ff43f Use zoneless 2026-07-04 09:19:07 -07:00
shamoon 56cc2146cd Remove zone import from polyfill 2026-07-04 09:18:59 -07:00
shamoon 040eacc189 Mechanical stuff for angular 22 2026-07-04 08:28:24 -07:00
dependabot[bot] 31d28d87db Chore(deps): Bump the actions group across 1 directory with 14 updates (#13079)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 22:21:19 +00:00
GitHub Actions 8ad321b743 Auto translate strings 2026-07-02 22:03:59 +00:00
dependabot[bot] ea1102b7f2 Chore(deps): Bump pdfjs-dist from 5.7.284 to 6.0.227 in /src-ui (#13080)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 22:02:20 +00:00
dependabot[bot] 627875b1cf Chore(deps-dev): Bump @playwright/test from 1.60.0 to 1.61.1 in /src-ui (#13078)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 08:25:57 -07:00
dependabot[bot] 7ffaba6aac Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates (#13076)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 15:23:09 +00:00
dependabot[bot] fe81f3ee98 Chore(deps): Bump uuid from 14.0.0 to 14.0.1 in /src-ui (#13077)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 15:07:48 +00:00
dependabot[bot] ed9372c808 Chore(deps-dev): Bump jest-preset-angular from 16.1.5 to 17.0.0 in /src-ui in the frontend-jest-dependencies group (#13075)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 14:50:59 +00:00
dependabot[bot] b804da0140 Chore(deps-dev): Bump @types/node from 25.9.1 to 26.0.0 in /src-ui (#13081)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 07:30:21 -07:00
75 changed files with 4166 additions and 3506 deletions
+10 -10
View File
@@ -24,7 +24,7 @@ jobs:
backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -87,7 +87,7 @@ jobs:
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Start containers
@@ -96,11 +96,11 @@ jobs:
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
- name: Set up Python
id: setup-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -141,13 +141,13 @@ jobs:
pytest
- name: Upload test results to Codecov
if: always()
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: backend-python-${{ matrix.python-version }}
files: junit.xml
report_type: test_results
- name: Upload coverage to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: backend-python-${{ matrix.python-version }}
files: coverage.xml
@@ -169,16 +169,16 @@ jobs:
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
id: setup-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -202,7 +202,7 @@ jobs:
check \
src/
- name: Cache Mypy
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .mypy_cache
# Keyed by OS, Python version, and dependency hashes
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
ref-name: ${{ steps.ref.outputs.name }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Determine ref name
+4 -4
View File
@@ -21,7 +21,7 @@ jobs:
docs_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.docs == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -69,16 +69,16 @@ jobs:
steps:
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
id: setup-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
+19 -19
View File
@@ -21,7 +21,7 @@ jobs:
frontend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.frontend == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -77,11 +77,11 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -92,7 +92,7 @@ jobs:
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
@@ -109,11 +109,11 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -123,7 +123,7 @@ jobs:
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
@@ -148,11 +148,11 @@ jobs:
shard-count: [4]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -162,7 +162,7 @@ jobs:
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
@@ -174,13 +174,13 @@ jobs:
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload test results to Codecov
if: always()
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/
report_type: test_results
- name: Upload coverage to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/
@@ -191,7 +191,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
container: mcr.microsoft.com/playwright:v1.60.0-noble
container: mcr.microsoft.com/playwright:v1.61.1-noble
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -203,11 +203,11 @@ jobs:
shard-count: [2]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -217,7 +217,7 @@ jobs:
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
@@ -239,12 +239,12 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 2
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -254,7 +254,7 @@ jobs:
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
+2 -2
View File
@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.14"
- name: Run prek
+10 -10
View File
@@ -20,7 +20,7 @@ jobs:
statuses: read
steps:
- name: Wait for Docker build
uses: lewagon/wait-on-check-action@9312864dfbc9fd208e9c0417843430751c042800 # v1.7.0
uses: lewagon/wait-on-check-action@96d9100b431964d10e0136aff8b9ccb92470505e # v1.8.0
with:
ref: ${{ github.sha }}
check-name: 'Merge and Push Manifest'
@@ -34,12 +34,12 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# ---- Frontend Build ----
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -54,11 +54,11 @@ jobs:
# ---- Backend Setup ----
- name: Set up Python
id: setup-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
@@ -170,7 +170,7 @@ jobs:
fi
- name: Create release and changelog
id: create-release
uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
with:
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.version }}
@@ -181,7 +181,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive
uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1.10.1
uses: shogo82148/actions-upload-release-asset@394b3c11c3cfc038b5396ad265c074065cf875c3 # v1.10.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
@@ -201,17 +201,17 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
persist-credentials: true # for pushing changelog branch
- name: Set up Python
id: setup-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
+4 -4
View File
@@ -22,11 +22,11 @@ jobs:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
semgrep:
name: Semgrep CE
runs-on: ubuntu-24.04
@@ -38,13 +38,13 @@ jobs:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
if: always()
with:
sarif_file: results.sarif
+3 -3
View File
@@ -34,12 +34,12 @@ jobs:
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,4 +47,4 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
environment: translation-sync
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false
- name: crowdin action
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
with:
upload_translations: false
download_translations: true
+1 -1
View File
@@ -19,6 +19,6 @@ jobs:
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps:
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -5
View File
@@ -14,7 +14,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
env:
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
with:
@@ -23,13 +23,13 @@ jobs:
persist-credentials: true # for pushing translation branch
- name: Set up Python
id: setup-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -43,7 +43,7 @@ jobs:
PAPERLESS_SECRET_KEY: "ci-translate-not-a-real-secret"
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10
- name: Use Node.js 24
@@ -54,7 +54,7 @@ jobs:
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.pnpm-store
+5 -5
View File
@@ -21,7 +21,7 @@ dependencies = [
"channels~=4.2",
"channels-redis~=4.2",
"concurrent-log-handler~=0.9.25",
"dateparser~=1.4",
"dateparser~=1.2",
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.13",
@@ -32,7 +32,7 @@ dependencies = [
"django-cors-headers~=4.9.0",
"django-extensions~=4.1",
"django-filter~=25.1",
"django-guardian~=3.3.2",
"django-guardian~=3.3.0",
"django-multiselectfield~=1.0.1",
"django-rich~=2.2.0",
"django-soft-delete~=1.0.18",
@@ -42,7 +42,7 @@ dependencies = [
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2026.5.1",
"drf-writable-nested~=0.7.1",
"filelock~=3.29.4",
"filelock~=3.29.0",
"flower~=2.0.1",
"gotenberg-client~=0.14.0",
"httpx-oauth~=0.16",
@@ -50,7 +50,7 @@ dependencies = [
"imap-tools~=1.13.0",
"jinja2~=3.1.5",
"langdetect~=1.0.9",
"llama-index-core>=0.14.23",
"llama-index-core>=0.14.22",
"llama-index-embeddings-huggingface>=0.6.1",
"llama-index-embeddings-ollama>=0.9",
"llama-index-embeddings-openai-like>=0.2.2",
@@ -75,7 +75,7 @@ dependencies = [
"sqlite-vec==0.1.9",
"tantivy~=0.26.0",
"tika-client~=0.11.0",
"torch~=2.12.1",
"torch~=2.12.0",
"watchfiles>=1.1.1",
"whitenoise~=6.11",
"zxing-cpp~=3.0.0",
+3 -1
View File
@@ -155,7 +155,9 @@
"builder": "@angular-builders/jest:run",
"options": {
"tsConfig": "tsconfig.spec.json",
"zoneless": false
"zoneless": false,
"watch": false,
"coverage": true
}
},
"lint": {
+24 -5
View File
@@ -3,6 +3,27 @@ const angularTemplatePlugin = require('@angular-eslint/eslint-plugin-template')
const angularTemplateParser = require('@angular-eslint/template-parser')
const tsParser = require('@typescript-eslint/parser')
const angularTsRecommendedRules = {
'@angular-eslint/contextual-lifecycle': 'error',
'@angular-eslint/no-empty-lifecycle-method': 'error',
'@angular-eslint/no-input-rename': 'error',
'@angular-eslint/no-inputs-metadata-property': 'error',
'@angular-eslint/no-output-native': 'error',
'@angular-eslint/no-output-on-prefix': 'error',
'@angular-eslint/no-output-rename': 'error',
'@angular-eslint/no-outputs-metadata-property': 'error',
'@angular-eslint/prefer-inject': 'error',
'@angular-eslint/prefer-standalone': 'error',
'@angular-eslint/use-pipe-transform-interface': 'error',
'@angular-eslint/use-lifecycle-interface': 'warn',
}
const angularTemplateRecommendedRules = {
'@angular-eslint/template/banana-in-box': 'error',
'@angular-eslint/template/eqeqeq': 'error',
'@angular-eslint/template/no-negated-async': 'error',
'@angular-eslint/template/prefer-control-flow': 'error',
}
module.exports = [
{
ignores: ['projects/**/*', 'src/app/components/common/pdf-viewer/**'],
@@ -12,8 +33,6 @@ module.exports = [
languageOptions: {
parser: tsParser,
parserOptions: {
project: ['tsconfig.json'],
createDefaultProgram: true,
ecmaVersion: 2020,
sourceType: 'module',
},
@@ -22,9 +41,9 @@ module.exports = [
'@angular-eslint': angularEslintPlugin,
'@angular-eslint/template': angularTemplatePlugin,
},
processor: '@angular-eslint/template/extract-inline-html',
processor: angularTemplatePlugin.processors['extract-inline-html'],
rules: {
...angularEslintPlugin.configs.recommended.rules,
...angularTsRecommendedRules,
'@angular-eslint/directive-selector': [
'error',
{
@@ -52,7 +71,7 @@ module.exports = [
'@angular-eslint/template': angularTemplatePlugin,
},
rules: {
...angularTemplatePlugin.configs.recommended.rules,
...angularTemplateRecommendedRules,
},
},
]
+49 -49
View File
@@ -1277,7 +1277,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1762</context>
<context context-type="linenumber">1760</context>
</context-group>
</trans-unit>
<trans-unit id="1577733187050997705" datatype="html">
@@ -2177,7 +2177,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">637</context>
<context context-type="linenumber">635</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
@@ -3080,11 +3080,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1381</context>
<context context-type="linenumber">1379</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1763</context>
<context context-type="linenumber">1761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -3655,7 +3655,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1334</context>
<context context-type="linenumber">1332</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -3767,7 +3767,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1816</context>
<context context-type="linenumber">1814</context>
</context-group>
</trans-unit>
<trans-unit id="6661109599266152398" datatype="html">
@@ -3778,7 +3778,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1817</context>
<context context-type="linenumber">1815</context>
</context-group>
</trans-unit>
<trans-unit id="5162686434580248853" datatype="html">
@@ -3789,7 +3789,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1818</context>
<context context-type="linenumber">1816</context>
</context-group>
</trans-unit>
<trans-unit id="8157388568390631653" datatype="html">
@@ -5691,7 +5691,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1338</context>
<context context-type="linenumber">1336</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -7933,88 +7933,88 @@
<source>Enter Password</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">512</context>
<context context-type="linenumber">513</context>
</context-group>
</trans-unit>
<trans-unit id="5758784066858623886" datatype="html">
<source>Error retrieving metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">411</context>
<context context-type="linenumber">409</context>
</context-group>
</trans-unit>
<trans-unit id="2218903673684131427" datatype="html">
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">512,514</context>
<context context-type="linenumber">510,512</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">959,961</context>
<context context-type="linenumber">957,959</context>
</context-group>
</trans-unit>
<trans-unit id="6357361810318120957" datatype="html">
<source>Document was updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">632</context>
<context context-type="linenumber">630</context>
</context-group>
</trans-unit>
<trans-unit id="5154064822428631306" datatype="html">
<source>Document was updated at <x id="PH" equiv-text="formattedModified"/>.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">633</context>
<context context-type="linenumber">631</context>
</context-group>
</trans-unit>
<trans-unit id="8462497568316256794" datatype="html">
<source>Reload to discard your local unsaved edits and load the latest remote version.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">634</context>
<context context-type="linenumber">632</context>
</context-group>
</trans-unit>
<trans-unit id="7967484035994732534" datatype="html">
<source>Reload</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="2907037627372942104" datatype="html">
<source>Document reloaded with latest changes.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">692</context>
<context context-type="linenumber">690</context>
</context-group>
</trans-unit>
<trans-unit id="6435639868943916539" datatype="html">
<source>Document reloaded.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">703</context>
<context context-type="linenumber">701</context>
</context-group>
</trans-unit>
<trans-unit id="6142395741265832184" datatype="html">
<source>Next document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">805</context>
<context context-type="linenumber">803</context>
</context-group>
</trans-unit>
<trans-unit id="651985345816518480" datatype="html">
<source>Previous document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">815</context>
<context context-type="linenumber">813</context>
</context-group>
</trans-unit>
<trans-unit id="2885986061416655600" datatype="html">
<source>Close document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">823</context>
<context context-type="linenumber">821</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
@@ -8025,67 +8025,67 @@
<source>Save document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">830</context>
<context context-type="linenumber">828</context>
</context-group>
</trans-unit>
<trans-unit id="1784543155727940353" datatype="html">
<source>Save and close / next</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">839</context>
<context context-type="linenumber">837</context>
</context-group>
</trans-unit>
<trans-unit id="7427704425579737895" datatype="html">
<source>Error retrieving version content</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">943</context>
<context context-type="linenumber">941</context>
</context-group>
</trans-unit>
<trans-unit id="3456881259945295697" datatype="html">
<source>Error retrieving suggestions.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1002</context>
<context context-type="linenumber">1000</context>
</context-group>
</trans-unit>
<trans-unit id="2194092841814123758" datatype="html">
<source>Document &quot;<x id="PH" equiv-text="newValues.title"/>&quot; saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1214</context>
<context context-type="linenumber">1212</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1241</context>
<context context-type="linenumber">1239</context>
</context-group>
</trans-unit>
<trans-unit id="6626387786259219838" datatype="html">
<source>Error saving document &quot;<x id="PH" equiv-text="this.document.title"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1247</context>
<context context-type="linenumber">1245</context>
</context-group>
</trans-unit>
<trans-unit id="448882439049417053" datatype="html">
<source>Error saving document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1302</context>
<context context-type="linenumber">1300</context>
</context-group>
</trans-unit>
<trans-unit id="8410796510716511826" datatype="html">
<source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document.title"/>&quot; to the trash?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1335</context>
<context context-type="linenumber">1333</context>
</context-group>
</trans-unit>
<trans-unit id="282586936710748252" datatype="html">
<source>Documents can be restored prior to permanent deletion.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1336</context>
<context context-type="linenumber">1334</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -8096,14 +8096,14 @@
<source>Error deleting document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1357</context>
<context context-type="linenumber">1355</context>
</context-group>
</trans-unit>
<trans-unit id="619486176823357521" datatype="html">
<source>Reprocess confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1377</context>
<context context-type="linenumber">1375</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -8114,102 +8114,102 @@
<source>This operation will permanently recreate the archive file for this document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1378</context>
<context context-type="linenumber">1376</context>
</context-group>
</trans-unit>
<trans-unit id="302054111564709516" datatype="html">
<source>The archive file will be re-generated with the current settings.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1379</context>
<context context-type="linenumber">1377</context>
</context-group>
</trans-unit>
<trans-unit id="4700389117298802932" datatype="html">
<source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1389</context>
<context context-type="linenumber">1387</context>
</context-group>
</trans-unit>
<trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1400</context>
<context context-type="linenumber">1398</context>
</context-group>
</trans-unit>
<trans-unit id="6030453331794586802" datatype="html">
<source>Error downloading document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1463</context>
<context context-type="linenumber">1461</context>
</context-group>
</trans-unit>
<trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1543</context>
<context context-type="linenumber">1541</context>
</context-group>
</trans-unit>
<trans-unit id="4663705961777238777" datatype="html">
<source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1783</context>
<context context-type="linenumber">1781</context>
</context-group>
</trans-unit>
<trans-unit id="9043972994040261999" datatype="html">
<source>Error executing PDF edit operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1795</context>
<context context-type="linenumber">1793</context>
</context-group>
</trans-unit>
<trans-unit id="6172690334763056188" datatype="html">
<source>Please enter the current password before attempting to remove it.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1806</context>
<context context-type="linenumber">1804</context>
</context-group>
</trans-unit>
<trans-unit id="968660764814228922" datatype="html">
<source>Password removal operation for &quot;<x id="PH" equiv-text="this.document.title"/>&quot; will begin in the background.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1840</context>
<context context-type="linenumber">1838</context>
</context-group>
</trans-unit>
<trans-unit id="2282118435712883014" datatype="html">
<source>Error executing password removal operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1854</context>
<context context-type="linenumber">1852</context>
</context-group>
</trans-unit>
<trans-unit id="3740891324955700797" datatype="html">
<source>Print failed.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1900</context>
<context context-type="linenumber">1898</context>
</context-group>
</trans-unit>
<trans-unit id="6457245677384603573" datatype="html">
<source>Error loading document for printing.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1913</context>
<context context-type="linenumber">1911</context>
</context-group>
</trans-unit>
<trans-unit id="6085793215710522488" datatype="html">
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1978</context>
<context context-type="linenumber">1976</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1982</context>
<context context-type="linenumber">1980</context>
</context-group>
</trans-unit>
<trans-unit id="4958946940233632319" datatype="html">
+38 -47
View File
@@ -6,22 +6,22 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test --no-watch --coverage",
"test": "ng test",
"lint": "ng lint"
},
"private": true,
"dependencies": {
"@angular/cdk": "^21.2.12",
"@angular/common": "~21.2.17",
"@angular/compiler": "~21.2.17",
"@angular/core": "~21.2.17",
"@angular/forms": "~21.2.14",
"@angular/localize": "~21.2.14",
"@angular/platform-browser": "~21.2.14",
"@angular/platform-browser-dynamic": "~21.2.14",
"@angular/router": "~21.2.14",
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
"@ng-select/ng-select": "^21.8.2",
"@angular/cdk": "^22.0.3",
"@angular/common": "~22.0.5",
"@angular/compiler": "~22.0.5",
"@angular/core": "~22.0.5",
"@angular/forms": "~22.0.5",
"@angular/localize": "~22.0.5",
"@angular/platform-browser": "~22.0.5",
"@angular/platform-browser-dynamic": "~22.0.5",
"@angular/router": "~22.0.5",
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "^23.2.0",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
@@ -29,56 +29,47 @@
"mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.1.0",
"ngx-cookie-service": "^21.3.1",
"ngx-device-detector": "^11.0.0",
"ngx-ui-tour-ng-bootstrap": "^18.0.0",
"ngx-cookie-service": "^22.0.0",
"ngx-device-detector": "^12.0.0",
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
"normalize-diacritics": "^5.0.0",
"pdfjs-dist": "^5.7.284",
"pdfjs-dist": "^6.0.227",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^14.0.0",
"uuid": "^14.0.1",
"zone.js": "^0.16.2"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^21.0.3",
"@angular-builders/jest": "^21.0.3",
"@angular-devkit/core": "^21.2.12",
"@angular-devkit/schematics": "^21.2.12",
"@angular-eslint/builder": "21.4.0",
"@angular-eslint/eslint-plugin": "21.4.0",
"@angular-eslint/eslint-plugin-template": "21.4.0",
"@angular-eslint/schematics": "21.4.0",
"@angular-eslint/template-parser": "21.4.0",
"@angular/build": "^21.2.12",
"@angular/cli": "~21.2.12",
"@angular/compiler-cli": "~21.2.14",
"@angular-builders/custom-webpack": "^22.0.1",
"@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.0.5",
"@angular-devkit/schematics": "^22.0.5",
"@angular-eslint/builder": "22.0.0",
"@angular-eslint/eslint-plugin": "22.0.0",
"@angular-eslint/eslint-plugin-template": "22.0.0",
"@angular-eslint/schematics": "22.0.0",
"@angular-eslint/template-parser": "22.0.0",
"@angular/build": "^22.0.5",
"@angular/cli": "~22.0.5",
"@angular/compiler-cli": "~22.0.5",
"@codecov/webpack-plugin": "^2.0.1",
"@playwright/test": "^1.60.0",
"@playwright/test": "^1.61.1",
"@types/jest": "^30.0.0",
"@types/node": "^25.9.1",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/parser": "^8.60.0",
"@typescript-eslint/utils": "^8.60.0",
"eslint": "^10.4.0",
"@types/node": "^26.0.0",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@typescript-eslint/utils": "^8.62.0",
"eslint": "^10.5.0",
"jest": "30.4.2",
"jest-environment-jsdom": "^30.4.1",
"jest-junit": "^17.0.0",
"jest-preset-angular": "^16.1.5",
"jest-preset-angular": "^17.0.0",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1",
"typescript": "^5.9.3",
"typescript": "^6.0.3",
"webpack": "^5.107.2"
},
"packageManager": "pnpm@10.17.1",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"canvas",
"esbuild",
"lmdb",
"msgpackr-extract"
]
}
"packageManager": "pnpm@10.17.1"
}
+2282 -2885
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
packages:
- "."
allowBuilds:
"@parcel/watcher": true
canvas: true
esbuild: true
lmdb: true
msgpackr-extract: true
normalize-diacritics: false
unrs-resolver: false
onlyBuiltDependencies:
- "@parcel/watcher"
- canvas
- esbuild
- lmdb
- msgpackr-extract
ignoredBuiltDependencies:
- normalize-diacritics
- unrs-resolver
@@ -7,6 +7,7 @@ import {
OnInit,
ViewChild,
inject,
signal,
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
@@ -34,22 +35,70 @@ export class LogsComponent
private logService = inject(LogService)
private changedetectorRef = inject(ChangeDetectorRef)
public logs: Array<{ message: string; level: number }> = []
private logsSignal = signal<Array<{ message: string; level: number }>>([])
public logFiles: string[] = []
private logFilesSignal = signal<string[]>([])
public activeLog: string
private activeLogSignal = signal<string>(undefined)
public autoRefreshEnabled: boolean = true
private autoRefreshEnabledSignal = signal(true)
public limit: number = 5000
private limitSignal = signal(5000)
public showJumpToBottom = false
private showJumpToBottomSignal = signal(false)
private readonly limitChange$ = new Subject<number>()
@ViewChild('logContainer') logContainer: ElementRef<HTMLElement>
public get logs(): Array<{ message: string; level: number }> {
return this.logsSignal()
}
public set logs(logs: Array<{ message: string; level: number }>) {
this.logsSignal.set(logs)
}
public get logFiles(): string[] {
return this.logFilesSignal()
}
public set logFiles(logFiles: string[]) {
this.logFilesSignal.set(logFiles)
}
public get activeLog(): string {
return this.activeLogSignal()
}
public set activeLog(activeLog: string) {
this.activeLogSignal.set(activeLog)
}
public get autoRefreshEnabled(): boolean {
return this.autoRefreshEnabledSignal()
}
public set autoRefreshEnabled(autoRefreshEnabled: boolean) {
this.autoRefreshEnabledSignal.set(autoRefreshEnabled)
}
public get limit(): number {
return this.limitSignal()
}
public set limit(limit: number) {
this.limitSignal.set(limit)
}
public get showJumpToBottom(): boolean {
return this.showJumpToBottomSignal()
}
public set showJumpToBottom(showJumpToBottom: boolean) {
this.showJumpToBottomSignal.set(showJumpToBottom)
}
ngOnInit(): void {
this.limitChange$
.pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier))
@@ -1,5 +1,5 @@
import { JsonPipe, NgTemplateOutlet } from '@angular/common'
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router, RouterLink } from '@angular/router'
import {
@@ -166,18 +166,42 @@ export class TasksComponent
public autoRefreshEnabled: boolean = true
public readonly pageSize = 25
public page: number = 1
public totalTasks: number = 0
public sectionCounts: Record<TaskSection, number> = {
private totalTasksSignal = signal(0)
private sectionCountsSignal = signal<Record<TaskSection, number>>({
[TaskSection.All]: 0,
[TaskSection.NeedsAttention]: 0,
[TaskSection.InProgress]: 0,
[TaskSection.Completed]: 0,
}
public pagedTasks: PaperlessTask[] = []
})
private pagedTasksSignal = signal<PaperlessTask[]>([])
public selectedSection: TaskSection = TaskSection.All
public selectedTaskType: PaperlessTaskType | null = null
public selectedTriggerSource: PaperlessTaskTriggerSource | null = null
public get totalTasks(): number {
return this.totalTasksSignal()
}
public set totalTasks(value: number) {
this.totalTasksSignal.set(value)
}
public get sectionCounts(): Record<TaskSection, number> {
return this.sectionCountsSignal()
}
public set sectionCounts(value: Record<TaskSection, number>) {
this.sectionCountsSignal.set(value)
}
public get pagedTasks(): PaperlessTask[] {
return this.pagedTasksSignal()
}
public set pagedTasks(value: PaperlessTask[]) {
this.pagedTasksSignal.set(value)
}
private _filterText: string = ''
get filterText() {
return this._filterText
@@ -480,6 +504,13 @@ export class TasksComponent
return this.sectionCounts[section]
}
private setSectionCount(section: TaskSection, count: number) {
this.sectionCountsSignal.update((counts) => ({
...counts,
[section]: count,
}))
}
sectionShowsResults(section: TaskSection): boolean {
return section !== TaskSection.InProgress
}
@@ -664,10 +695,12 @@ export class TasksComponent
.statusCounts(this.getParamsForSection(TaskSection.All))
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe((counts) => {
this.sectionCounts[TaskSection.All] = counts.all
this.sectionCounts[TaskSection.NeedsAttention] = counts.needs_attention
this.sectionCounts[TaskSection.InProgress] = counts.in_progress
this.sectionCounts[TaskSection.Completed] = counts.completed
this.sectionCounts = {
[TaskSection.All]: counts.all,
[TaskSection.NeedsAttention]: counts.needs_attention,
[TaskSection.InProgress]: counts.in_progress,
[TaskSection.Completed]: counts.completed,
}
})
}
@@ -735,9 +768,9 @@ export class TasksComponent
next: (result) => {
this.pagedTasks = result.results
this.totalTasks = result.count
this.sectionCounts[TaskSection.All] = result.count
this.setSectionCount(TaskSection.All, result.count)
if (this.selectedSection !== TaskSection.All) {
this.sectionCounts[this.selectedSection] = result.count
this.setSectionCount(this.selectedSection, result.count)
}
this.loading = false
if (
@@ -1,4 +1,4 @@
import { Component, OnDestroy, inject } from '@angular/core'
import { Component, OnDestroy, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import {
@@ -7,7 +7,7 @@ import {
NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs'
import { takeUntil, tap } from 'rxjs'
import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service'
@@ -42,11 +42,51 @@ export class TrashComponent
private settingsService = inject(SettingsService)
private router = inject(Router)
public documentsInTrash: Document[] = []
public selectedDocuments: Set<number> = new Set()
public allToggled: boolean = false
public page: number = 1
public totalDocuments: number
private documentsInTrashSignal = signal<Document[]>([])
private selectedDocumentsSignal = signal<Set<number>>(new Set())
private allToggledSignal = signal(false)
private pageSignal = signal(1)
private totalDocumentsSignal = signal<number>(undefined)
public get documentsInTrash(): Document[] {
return this.documentsInTrashSignal()
}
public set documentsInTrash(documentsInTrash: Document[]) {
this.documentsInTrashSignal.set(documentsInTrash)
}
public get selectedDocuments(): Set<number> {
return this.selectedDocumentsSignal()
}
public set selectedDocuments(selectedDocuments: Set<number>) {
this.selectedDocumentsSignal.set(selectedDocuments)
}
public get allToggled(): boolean {
return this.allToggledSignal()
}
public set allToggled(allToggled: boolean) {
this.allToggledSignal.set(allToggled)
}
public get page(): number {
return this.pageSignal()
}
public set page(page: number) {
this.pageSignal.set(page)
}
public get totalDocuments(): number {
return this.totalDocumentsSignal()
}
public set totalDocuments(totalDocuments: number) {
this.totalDocumentsSignal.set(totalDocuments)
}
constructor() {
super()
@@ -61,10 +101,9 @@ export class TrashComponent
tap((r) => {
this.documentsInTrash = r.results
this.totalDocuments = r.count
this.selectedDocuments.clear()
this.selectedDocuments = new Set()
this.loading = false
}),
delay(100)
})
)
.subscribe(() => {
this.show = true
@@ -186,17 +225,20 @@ export class TrashComponent
}
toggleSelected(object: Document) {
this.selectedDocuments.has(object.id)
? this.selectedDocuments.delete(object.id)
: this.selectedDocuments.add(object.id)
const selectedDocuments = new Set(this.selectedDocuments)
selectedDocuments.has(object.id)
? selectedDocuments.delete(object.id)
: selectedDocuments.add(object.id)
this.selectedDocuments = selectedDocuments
}
clearSelection() {
this.allToggled = false
this.selectedDocuments.clear()
this.selectedDocuments = new Set()
}
getDaysRemaining(document: Document): number {
this.settingsService.trackChanges()
const delay = this.settingsService.get(SETTINGS_KEYS.EMPTY_TRASH_DELAY)
const diff = new Date().getTime() - new Date(document.deleted_at).getTime()
const days = Math.ceil(diff / (1000 * 3600 * 24))
@@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, first, takeUntil } from 'rxjs'
@@ -43,8 +43,24 @@ export class UsersAndGroupsComponent
permissionsService = inject(PermissionsService)
private settings = inject(SettingsService)
users: User[]
groups: Group[]
private usersSignal = signal<User[]>(null)
private groupsSignal = signal<Group[]>(null)
get users(): User[] {
return this.usersSignal()
}
set users(value: User[]) {
this.usersSignal.set(value)
}
get groups(): Group[] {
return this.groupsSignal()
}
set groups(value: Group[]) {
this.groupsSignal.set(value)
}
unsubscribeNotifier: Subject<any> = new Subject()
@@ -6,7 +6,7 @@ import {
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { NgClass } from '@angular/common'
import { Component, HostListener, inject, OnInit } from '@angular/core'
import { Component, HostListener, inject, OnInit, signal } from '@angular/core'
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
import {
NgbCollapseModule,
@@ -90,16 +90,48 @@ export class AppFrameComponent
permissionsService = inject(PermissionsService)
private djangoMessagesService = inject(DjangoMessagesService)
appRemoteVersion: AppRemoteVersion
private appRemoteVersionSignal = signal<AppRemoteVersion>(null)
isMenuCollapsed: boolean = true
private isMenuCollapsedSignal = signal(true)
slimSidebarAnimating: boolean = false
private slimSidebarAnimatingSignal = signal(false)
public mobileSearchHidden: boolean = false
private mobileSearchHiddenSignal = signal(false)
private lastScrollY: number = 0
get appRemoteVersion(): AppRemoteVersion {
return this.appRemoteVersionSignal()
}
set appRemoteVersion(value: AppRemoteVersion) {
this.appRemoteVersionSignal.set(value)
}
get isMenuCollapsed(): boolean {
return this.isMenuCollapsedSignal()
}
set isMenuCollapsed(value: boolean) {
this.isMenuCollapsedSignal.set(value)
}
get slimSidebarAnimating(): boolean {
return this.slimSidebarAnimatingSignal()
}
set slimSidebarAnimating(value: boolean) {
this.slimSidebarAnimatingSignal.set(value)
}
get mobileSearchHidden(): boolean {
return this.mobileSearchHiddenSignal()
}
set mobileSearchHidden(value: boolean) {
this.mobileSearchHiddenSignal.set(value)
}
constructor() {
super()
const permissionsService = this.permissionsService
@@ -178,10 +210,12 @@ export class AppFrameComponent
}
get versionString(): string {
this.settingsService.trackChanges()
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
}
get customAppTitle(): string {
this.settingsService.trackChanges()
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
}
@@ -224,6 +258,7 @@ export class AppFrameComponent
}
get slimSidebarEnabled(): boolean {
this.settingsService.trackChanges()
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
}
@@ -243,6 +278,7 @@ export class AppFrameComponent
}
get attributesSectionsCollapsed(): boolean {
this.settingsService.trackChanges()
return this.settingsService
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
?.includes(CollapsibleSection.ATTRIBUTES)
@@ -268,6 +304,7 @@ export class AppFrameComponent
}
get aiEnabled(): boolean {
this.settingsService.trackChanges()
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
}
@@ -419,6 +456,7 @@ export class AppFrameComponent
}
get showSidebarCounts(): boolean {
this.settingsService.trackChanges()
return (
this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT) &&
!this.settingsService.organizingSidebarSavedViews
@@ -7,6 +7,7 @@ import {
ViewChild,
ViewChildren,
inject,
signal,
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router'
@@ -82,12 +83,36 @@ export class GlobalSearchComponent implements OnInit {
private locationStrategy = inject(LocationStrategy)
public DataType = DataType
public query: string
private querySignal = signal<string>(null)
public queryDebounce: Subject<string>
public searchResults: GlobalSearchResult
private searchResultsSignal = signal<GlobalSearchResult>(null)
private currentItemIndex: number = -1
private domIndex: number = -1
public loading: boolean = false
private loadingSignal = signal(false)
public get query(): string {
return this.querySignal()
}
public set query(value: string) {
this.querySignal.set(value)
}
public get searchResults(): GlobalSearchResult {
return this.searchResultsSignal()
}
public set searchResults(value: GlobalSearchResult) {
this.searchResultsSignal.set(value)
}
public get loading(): boolean {
return this.loadingSignal()
}
public set loading(value: boolean) {
this.loadingSignal.set(value)
}
@ViewChild('searchInput') searchInput: ElementRef
@ViewChild('resultsDropdown') resultsDropdown: NgbDropdown
@@ -96,6 +121,7 @@ export class GlobalSearchComponent implements OnInit {
@ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>
get useAdvancedForFullSearch(): boolean {
this.settingsService.trackChanges()
return (
this.settingsService.get(SETTINGS_KEYS.SEARCH_FULL_TYPE) ===
GlobalSearchType.ADVANCED
@@ -1,5 +1,12 @@
import { DecimalPipe } from '@angular/common'
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
import {
Component,
EventEmitter,
Input,
Output,
inject,
signal,
} from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject } from 'rxjs'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@@ -13,6 +20,17 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
activeModal = inject(NgbActiveModal)
private titleSignal = signal($localize`Confirmation`)
private messageBoldSignal = signal<string>(undefined)
private messageSignal = signal<string>(undefined)
private btnClassSignal = signal('btn-primary')
private btnCaptionSignal = signal($localize`Confirm`)
private alternativeBtnClassSignal = signal('btn-secondary')
private alternativeBtnCaptionSignal = signal<string>(undefined)
private cancelBtnClassSignal = signal('btn-outline-secondary')
private cancelBtnCaptionSignal = signal($localize`Cancel`)
private buttonsEnabledSignal = signal(true)
@Output()
public confirmClicked = new EventEmitter()
@@ -20,34 +38,94 @@ export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
public alternativeClicked = new EventEmitter()
@Input()
title = $localize`Confirmation`
get title(): string {
return this.titleSignal()
}
set title(title: string) {
this.titleSignal.set(title)
}
@Input()
messageBold
get messageBold(): string {
return this.messageBoldSignal()
}
set messageBold(messageBold: string) {
this.messageBoldSignal.set(messageBold)
}
@Input()
message
get message(): string {
return this.messageSignal()
}
set message(message: string) {
this.messageSignal.set(message)
}
@Input()
btnClass = 'btn-primary'
get btnClass(): string {
return this.btnClassSignal()
}
set btnClass(btnClass: string) {
this.btnClassSignal.set(btnClass)
}
@Input()
btnCaption = $localize`Confirm`
get btnCaption(): string {
return this.btnCaptionSignal()
}
set btnCaption(btnCaption: string) {
this.btnCaptionSignal.set(btnCaption)
}
@Input()
alternativeBtnClass = 'btn-secondary'
get alternativeBtnClass(): string {
return this.alternativeBtnClassSignal()
}
set alternativeBtnClass(alternativeBtnClass: string) {
this.alternativeBtnClassSignal.set(alternativeBtnClass)
}
@Input()
alternativeBtnCaption
get alternativeBtnCaption(): string {
return this.alternativeBtnCaptionSignal()
}
set alternativeBtnCaption(alternativeBtnCaption: string) {
this.alternativeBtnCaptionSignal.set(alternativeBtnCaption)
}
@Input()
cancelBtnClass = 'btn-outline-secondary'
get cancelBtnClass(): string {
return this.cancelBtnClassSignal()
}
set cancelBtnClass(cancelBtnClass: string) {
this.cancelBtnClassSignal.set(cancelBtnClass)
}
@Input()
cancelBtnCaption = $localize`Cancel`
get cancelBtnCaption(): string {
return this.cancelBtnCaptionSignal()
}
set cancelBtnCaption(cancelBtnCaption: string) {
this.cancelBtnCaptionSignal.set(cancelBtnCaption)
}
@Input()
buttonsEnabled = true
get buttonsEnabled(): boolean {
return this.buttonsEnabledSignal()
}
set buttonsEnabled(buttonsEnabled: boolean) {
this.buttonsEnabledSignal.set(buttonsEnabled)
}
confirmButtonEnabled = true
alternativeButtonEnabled = true
@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@@ -14,18 +14,11 @@ export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponen
includeMetadata: boolean = true
deleteOriginal: boolean = false
@Input()
override title = $localize`Remove password protection`
@Input()
override message =
$localize`Create an unprotected copy or replace the existing file.`
@Input()
override btnCaption = $localize`Start`
constructor() {
super()
this.title = $localize`Remove password protection`
this.message = $localize`Create an unprotected copy or replace the existing file.`
this.btnCaption = $localize`Start`
}
onUpdateDocumentChange(updateDocument: boolean) {
@@ -1,5 +1,5 @@
import { NgStyle } from '@angular/common'
import { Component, inject } from '@angular/core'
import { Component, inject, signal } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@@ -13,11 +13,30 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
documentService = inject(DocumentService)
public documentID: number
public showPDFNote: boolean = true
private documentIDSignal = signal<number>(undefined)
private showPDFNoteSignal = signal(true)
private rotationSignal = signal(0)
public get documentID(): number {
return this.documentIDSignal()
}
public set documentID(documentID: number) {
this.documentIDSignal.set(documentID)
}
public get showPDFNote(): boolean {
return this.showPDFNoteSignal()
}
public set showPDFNote(showPDFNote: boolean) {
this.showPDFNoteSignal.set(showPDFNote)
}
// animation is better if we dont normalize yet
public rotation: number = 0
public get rotation(): number {
return this.rotationSignal()
}
public get degrees(): number {
let degrees = this.rotation % 360
@@ -30,6 +49,6 @@ export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
}
rotate(clockwise: boolean = true) {
this.rotation += clockwise ? 90 : -90
this.rotationSignal.update((rotation) => rotation + (clockwise ? 90 : -90))
}
}
@@ -5,6 +5,7 @@ import {
OnInit,
Output,
inject,
signal,
} from '@angular/core'
import { FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
@@ -45,10 +46,20 @@ export abstract class EditDialogComponent<
protected settingsService = inject(SettingsService)
protected permissionsService = inject(PermissionsService)
users: User[]
private usersSignal = signal<User[]>(undefined)
private dialogModeSignal = signal(EditDialogMode.CREATE, {
equal: () => false,
})
@Input()
dialogMode: EditDialogMode = EditDialogMode.CREATE
get dialogMode(): EditDialogMode {
return this.dialogModeSignal()
}
set dialogMode(dialogMode: EditDialogMode) {
this.dialogModeSignal.set(dialogMode)
}
@Input()
object: T
@@ -59,11 +70,43 @@ export abstract class EditDialogComponent<
@Output()
failed = new EventEmitter()
networkActive = false
private networkActiveSignal = signal(false)
closeEnabled = false
private closeEnabledSignal = signal(false)
error = null
private errorSignal = signal<any>(null)
get users(): User[] {
return this.usersSignal()
}
set users(users: User[]) {
this.usersSignal.set(users)
}
get networkActive(): boolean {
return this.networkActiveSignal()
}
set networkActive(networkActive: boolean) {
this.networkActiveSignal.set(networkActive)
}
get closeEnabled(): boolean {
return this.closeEnabledSignal()
}
set closeEnabled(closeEnabled: boolean) {
this.closeEnabledSignal.set(closeEnabled)
}
get error(): any {
return this.errorSignal()
}
set error(error: any) {
this.errorSignal.set(error)
}
abstract getForm(): FormGroup
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'
import { Component, inject, signal } from '@angular/core'
import {
FormControl,
FormGroup,
@@ -158,9 +158,33 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
private correspondentService: CorrespondentService
private documentTypeService: DocumentTypeService
accounts: MailAccount[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
private accountsSignal = signal<MailAccount[]>(undefined)
private correspondentsSignal = signal<Correspondent[]>(undefined)
private documentTypesSignal = signal<DocumentType[]>(undefined)
get accounts(): MailAccount[] {
return this.accountsSignal()
}
set accounts(accounts: MailAccount[]) {
this.accountsSignal.set(accounts)
}
get correspondents(): Correspondent[] {
return this.correspondentsSignal()
}
set correspondents(correspondents: Correspondent[]) {
this.correspondentsSignal.set(correspondents)
}
get documentTypes(): DocumentType[] {
return this.documentTypesSignal()
}
set documentTypes(documentTypes: DocumentType[]) {
this.documentTypesSignal.set(documentTypes)
}
constructor() {
super()
@@ -67,11 +67,11 @@ export class StoragePathEditDialogComponent
private testDocument: Document
public testResult: string
public testFailed: boolean = false
public loading = false
public testLoading = false
constructor() {
super()
this.loading = false
this.service = inject(StoragePathService)
this.userService = inject(UserService)
this.settingsService = inject(SettingsService)
@@ -1,4 +1,4 @@
import { Component, OnInit, inject } from '@angular/core'
import { Component, OnInit, inject, signal } from '@angular/core'
import {
FormControl,
FormGroup,
@@ -40,9 +40,33 @@ export class UserEditDialogComponent
private toastService = inject(ToastService)
private groupsService: GroupService
groups: Group[]
passwordIsSet: boolean = false
public totpLoading: boolean = false
private groupsSignal = signal<Group[]>(undefined)
private passwordIsSetSignal = signal(false)
private totpLoadingSignal = signal(false)
get groups(): Group[] {
return this.groupsSignal()
}
set groups(groups: Group[]) {
this.groupsSignal.set(groups)
}
get passwordIsSet(): boolean {
return this.passwordIsSetSignal()
}
set passwordIsSet(passwordIsSet: boolean) {
this.passwordIsSetSignal.set(passwordIsSet)
}
public get totpLoading(): boolean {
return this.totpLoadingSignal()
}
public set totpLoading(totpLoading: boolean) {
this.totpLoadingSignal.set(totpLoading)
}
constructor() {
super()
@@ -103,7 +127,7 @@ export class UserEditDialogComponent
if (!groupsVal) return []
else
return groupsVal.flatMap(
(id) => this.groups.find((g) => g.id == id)?.permissions
(id) => this.groups?.find((g) => g.id == id)?.permissions
)
}
@@ -4,7 +4,7 @@ import {
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { NgTemplateOutlet } from '@angular/common'
import { Component, OnInit, inject } from '@angular/core'
import { Component, OnInit, inject, signal } from '@angular/core'
import {
AbstractControl,
FormArray,
@@ -476,17 +476,73 @@ export class WorkflowEditDialogComponent
private mailRuleService: MailRuleService
private customFieldsService: CustomFieldsService
templates: Workflow[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
mailRules: MailRule[]
customFields: CustomField[]
dateCustomFields: CustomField[]
private templatesSignal = signal<Workflow[]>(undefined)
private correspondentsSignal = signal<Correspondent[]>(undefined)
private documentTypesSignal = signal<DocumentType[]>(undefined)
private storagePathsSignal = signal<StoragePath[]>(undefined)
private mailRulesSignal = signal<MailRule[]>(undefined)
private customFieldsSignal = signal<CustomField[]>(undefined)
private dateCustomFieldsSignal = signal<CustomField[]>(undefined)
get templates(): Workflow[] {
return this.templatesSignal()
}
set templates(templates: Workflow[]) {
this.templatesSignal.set(templates)
}
get correspondents(): Correspondent[] {
return this.correspondentsSignal()
}
set correspondents(correspondents: Correspondent[]) {
this.correspondentsSignal.set(correspondents)
}
get documentTypes(): DocumentType[] {
return this.documentTypesSignal()
}
set documentTypes(documentTypes: DocumentType[]) {
this.documentTypesSignal.set(documentTypes)
}
get storagePaths(): StoragePath[] {
return this.storagePathsSignal()
}
set storagePaths(storagePaths: StoragePath[]) {
this.storagePathsSignal.set(storagePaths)
}
get mailRules(): MailRule[] {
return this.mailRulesSignal()
}
set mailRules(mailRules: MailRule[]) {
this.mailRulesSignal.set(mailRules)
}
get customFields(): CustomField[] {
return this.customFieldsSignal()
}
set customFields(customFields: CustomField[]) {
this.customFieldsSignal.set(customFields)
}
get dateCustomFields(): CustomField[] {
return this.dateCustomFieldsSignal()
}
set dateCustomFields(dateCustomFields: CustomField[]) {
this.dateCustomFieldsSignal.set(dateCustomFields)
}
expandedItem: number = null
private allowedActionTypes = []
private allowedActionTypesSignal = signal([])
private readonly triggerFilterOptionsMap = new WeakMap<
FormArray,
@@ -1294,9 +1350,18 @@ export class WorkflowEditDialogComponent
}
get actionTypeOptions() {
this.settingsService.trackChanges()
return this.allowedActionTypes
}
get allowedActionTypes() {
return this.allowedActionTypesSignal()
}
set allowedActionTypes(allowedActionTypes) {
this.allowedActionTypesSignal.set(allowedActionTypes)
}
getActionTypeOptionName(type: WorkflowActionType): string {
return this.actionTypeOptions.find((t) => t.id === type)?.name ?? ''
}
@@ -1,4 +1,4 @@
import { Component, Input, inject } from '@angular/core'
import { Component, Input, inject, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -17,23 +17,37 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
private documentService = inject(DocumentService)
private toastService = inject(ToastService)
@Input()
documentIds: number[]
private documentIdsSignal = signal<number[]>(undefined)
private hasArchiveVersionSignal = signal(true)
private useArchiveVersionSignal = signal(true)
private _hasArchiveVersion: boolean = true
@Input()
get documentIds(): number[] {
return this.documentIdsSignal()
}
set documentIds(documentIds: number[]) {
this.documentIdsSignal.set(documentIds)
}
get useArchiveVersion(): boolean {
return this.useArchiveVersionSignal()
}
set useArchiveVersion(useArchiveVersion: boolean) {
this.useArchiveVersionSignal.set(useArchiveVersion)
}
@Input()
set hasArchiveVersion(value: boolean) {
this._hasArchiveVersion = value
this.hasArchiveVersionSignal.set(value)
this.useArchiveVersion = value
}
get hasArchiveVersion(): boolean {
return this._hasArchiveVersion
return this.hasArchiveVersionSignal()
}
public useArchiveVersion: boolean = true
public emailAddress: string = ''
public emailSubject: string = ''
public emailMessage: string = ''
@@ -5,6 +5,7 @@ import {
Input,
OnInit,
Output,
signal,
ViewChild,
} from '@angular/core'
import { ControlValueAccessor } from '@angular/forms'
@@ -61,7 +62,15 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
@Output()
removed: EventEmitter<AbstractInputComponent<any>> = new EventEmitter()
value: T
private valueSignal = signal<T>(undefined)
get value(): T {
return this.valueSignal()
}
set value(value: T) {
this.valueSignal.set(value)
}
ngOnInit(): void {
this.inputId = uuidv4()
@@ -1,4 +1,4 @@
import { Component, forwardRef, inject } from '@angular/core'
import { Component, forwardRef, inject, signal } from '@angular/core'
import {
FormsModule,
NG_VALUE_ACCESSOR,
@@ -24,7 +24,15 @@ import { AbstractInputComponent } from '../../abstract-input'
imports: [NgSelectComponent, FormsModule, ReactiveFormsModule],
})
export class PermissionsGroupComponent extends AbstractInputComponent<Group> {
groups: Group[]
private groupsSignal = signal<Group[]>(undefined)
get groups(): Group[] {
return this.groupsSignal()
}
set groups(groups: Group[]) {
this.groupsSignal.set(groups)
}
constructor() {
const groupService = inject(GroupService)
@@ -1,4 +1,4 @@
import { Component, forwardRef, inject } from '@angular/core'
import { Component, forwardRef, inject, signal } from '@angular/core'
import {
FormsModule,
NG_VALUE_ACCESSOR,
@@ -24,7 +24,15 @@ import { AbstractInputComponent } from '../../abstract-input'
imports: [NgSelectComponent, FormsModule, ReactiveFormsModule],
})
export class PermissionsUserComponent extends AbstractInputComponent<User[]> {
users: User[]
private usersSignal = signal<User[]>(undefined)
get users(): User[] {
return this.usersSignal()
}
set users(users: User[]) {
this.usersSignal.set(users)
}
constructor() {
const userService = inject(UserService)
@@ -6,6 +6,7 @@ import {
Input,
OnInit,
Output,
signal,
ViewChild,
} from '@angular/core'
import {
@@ -109,7 +110,15 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
@ViewChild('tagSelect') select: NgSelectComponent
value: number[] = []
private valueSignal = signal<number[]>([])
get value(): number[] {
return this.valueSignal()
}
set value(value: number[]) {
this.valueSignal.set(value)
}
tags: Tag[] = []
@@ -3,7 +3,7 @@ import {
DragDropModule,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { Component, inject } from '@angular/core'
import { Component, inject, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -45,8 +45,25 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
private readonly settingsService = inject(SettingsService)
activeModal: NgbActiveModal = inject(NgbActiveModal)
documentID: number
versionID?: number
private documentIDSignal = signal<number>(undefined)
private versionIDSignal = signal<number>(undefined)
get documentID(): number {
return this.documentIDSignal()
}
set documentID(documentID: number) {
this.documentIDSignal.set(documentID)
}
get versionID(): number {
return this.versionIDSignal()
}
set versionID(versionID: number) {
this.versionIDSignal.set(versionID)
}
pages: PageOperation[] = []
totalPages = 0
editMode: PdfEditorEditMode = this.settingsService.get(
@@ -27,7 +27,6 @@ import {
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import {
PdfRenderMode,
PdfSource,
PdfZoomLevel,
PdfZoomScale,
PngxPdfDocumentProxy,
@@ -43,7 +42,8 @@ export class PngxPdfViewerComponent
{
private readonly document = inject<Document>(DOCUMENT)
@Input() src!: PdfSource
@Input() src!: string
@Input() password?: string
@Input() page?: number
@Output() pageChange = new EventEmitter<number>()
@Input() rotation?: number
@@ -93,7 +93,7 @@ export class PngxPdfViewerComponent
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['src']) {
if (changes['src'] || changes['password']) {
this.resetViewerState()
if (this.src) {
this.loadDocument()
@@ -174,8 +174,12 @@ export class PngxPdfViewerComponent
'assets/js/pdf.worker.min.mjs',
this.document.baseURI
).toString()
this.loadingTask = getDocument(this.src)
let initOptions = {
url: this.src,
password: this.password,
withCredentials: true,
}
this.loadingTask = getDocument(initOptions)
try {
const pdf = await this.loadingTask.promise
this.pdf = pdf
@@ -2,8 +2,6 @@ export type PngxPdfDocumentProxy = {
numPages: number
}
export type PdfSource = string | { url: string; password?: string }
export enum PdfRenderMode {
Single = 'single',
All = 'all',
@@ -1,4 +1,11 @@
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
import {
Component,
EventEmitter,
Input,
Output,
inject,
signal,
} from '@angular/core'
import {
FormControl,
FormGroup,
@@ -27,21 +34,44 @@ export class PermissionsDialogComponent {
activeModal = inject(NgbActiveModal)
private userService = inject(UserService)
users: User[]
private usersSignal = signal<User[]>(undefined)
private titleSignal = signal($localize`Set permissions`)
private noteSignal = signal<string>(null)
private buttonsEnabledSignal = signal(true)
private o: ObjectWithPermissions = undefined
constructor() {
this.userService.listAll().subscribe((r) => (this.users = r.results))
}
get users(): User[] {
return this.usersSignal()
}
set users(users: User[]) {
this.usersSignal.set(users)
}
@Output()
public confirmClicked = new EventEmitter()
@Input()
title = $localize`Set permissions`
get title(): string {
return this.titleSignal()
}
set title(title: string) {
this.titleSignal.set(title)
}
@Input()
note: string = null
get note(): string {
return this.noteSignal()
}
set note(note: string) {
this.noteSignal.set(note)
}
@Input()
set object(o: ObjectWithPermissions) {
@@ -65,7 +95,13 @@ export class PermissionsDialogComponent {
merge: new FormControl(true),
})
buttonsEnabled: boolean = true
get buttonsEnabled(): boolean {
return this.buttonsEnabledSignal()
}
set buttonsEnabled(buttonsEnabled: boolean) {
this.buttonsEnabledSignal.set(buttonsEnabled)
}
get permissions() {
return {
@@ -1,5 +1,5 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { Component, Input, OnInit, inject } from '@angular/core'
import { Component, Input, OnInit, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -27,50 +27,74 @@ export class ShareLinksDialogComponent implements OnInit {
readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS
@Input()
title = $localize`Share Links`
private titleSignal = signal($localize`Share Links`)
private documentIdSignal = signal<number>(undefined)
private hasArchiveVersionSignal = signal(true)
private shareLinksSignal = signal<ShareLink[]>(undefined)
private copiedSignal = signal<number>(undefined)
_documentId: number
@Input()
get title(): string {
return this.titleSignal()
}
set title(title: string) {
this.titleSignal.set(title)
}
@Input()
set documentId(id: number) {
if (id !== undefined) {
this._documentId = id
this.documentIdSignal.set(id)
this.refresh()
}
}
private _hasArchiveVersion: boolean = true
get documentId(): number {
return this.documentIdSignal()
}
@Input()
set hasArchiveVersion(value: boolean) {
this._hasArchiveVersion = value
this.hasArchiveVersionSignal.set(value)
this.useArchiveVersion = value
}
get hasArchiveVersion(): boolean {
return this._hasArchiveVersion
return this.hasArchiveVersionSignal()
}
shareLinks: ShareLink[]
get shareLinks(): ShareLink[] {
return this.shareLinksSignal()
}
set shareLinks(shareLinks: ShareLink[]) {
this.shareLinksSignal.set(shareLinks)
}
loading: boolean = false
copied: number
get copied(): number {
return this.copiedSignal()
}
set copied(copied: number) {
this.copiedSignal.set(copied)
}
expirationDays: number = 7
useArchiveVersion: boolean = true
ngOnInit(): void {
if (this._documentId !== undefined) this.refresh()
if (this.documentId !== undefined) this.refresh()
}
refresh() {
if (this._documentId === undefined) return
if (this.documentId === undefined) return
this.loading = true
this.shareLinkService
.getLinksForDocument(this._documentId)
.getLinksForDocument(this.documentId)
.pipe(first())
.subscribe({
next: (results) => {
@@ -141,7 +165,7 @@ export class ShareLinksDialogComponent implements OnInit {
this.loading = true
this.shareLinkService
.createLinkForDocument(
this._documentId,
this.documentId,
this.useArchiveVersion ? FileVersion.Archive : FileVersion.Original,
expiration
)
@@ -1,4 +1,4 @@
import { Component, inject, Input } from '@angular/core'
import { Component, inject, Input, signal } from '@angular/core'
import { Tag } from 'src/app/data/tag'
import {
PermissionAction,
@@ -16,16 +16,16 @@ export class TagComponent {
private permissionsService = inject(PermissionsService)
private tagService = inject(TagService)
private _tag: Tag
private tagSignal = signal<Tag>(null)
private _tagID: number
@Input()
public set tag(tag: Tag) {
this._tag = tag
this.tagSignal.set(tag)
}
public get tag(): Tag {
return this._tag
return this.tagSignal()
}
@Input()
@@ -5,7 +5,7 @@ import {
DragDropModule,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { Component, inject } from '@angular/core'
import { Component, inject, signal } from '@angular/core'
import { RouterModule } from '@angular/router'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap, TourService } from 'ngx-ui-tour-ng-bootstrap'
@@ -47,7 +47,15 @@ export class DashboardComponent extends ComponentWithPermissions {
private tourService = inject(TourService)
private toastService = inject(ToastService)
public dashboardViews: SavedView[] = []
private dashboardViewsSignal = signal<SavedView[]>([])
get dashboardViews(): SavedView[] {
return this.dashboardViewsSignal()
}
set dashboardViews(value: SavedView[]) {
this.dashboardViewsSignal.set(value)
}
constructor() {
super()
@@ -1,11 +1,14 @@
<pngx-widget-frame
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
[title]="savedView.name"
[badge]="count"
[loading]="loading"
[loading]="false"
[draggable]="savedView"
>
@if (count !== null) {
<span title-badge class="badge bg-info text-dark ms-2">{{count}}</span>
}
@if (documents.length) {
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
}
@@ -30,11 +33,24 @@
</tr>
</thead>
<tbody>
@if (loading && !documents.length) {
@for (row of placeholderRows; track row) {
<tr>
@for (field of displayFields; track field; let j = $index) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': j > 1 }">
<div class="placeholder-glow text-start">
<span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (row * 1/placeholderRows.length) }"></span>
</div>
</td>
}
</tr>
}
}
@for (doc of documents; track doc.id; let i = $index) {
<tr>
@for (field of displayFields; track field; let j = $index) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': j > 1 }">
@if (loading && show) {
@if (loading && !documents.length) {
<div class="placeholder-glow text-start">
<span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (i * 1/documents.length) }"></span>
</div>
@@ -115,12 +131,22 @@
</table>
} @else if (displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards my-n2">
@if (loading && !documents.length) {
@for (row of placeholderRows; track row) {
<pngx-document-card-small
class="p-0"
[ngStyle]="{ opacity: 1 - (row * 1/placeholderRows.length) }"
[document]="null"
[displayFields]="displayFields">
</pngx-document-card-small>
}
}
@for (d of documents; track d.id; let i = $index) {
<pngx-document-card-small
class="p-0"
[ngStyle]="{ opacity: !loading && show ? 1 : 1 - (i * 1/documents.length) }"
[ngStyle]="{ opacity: show ? 1 : 1 - (i * 1/documents.length) }"
(dblClickDocument)="openDocumentDetail(d)"
[document]="!loading && show ? d : null"
[document]="d"
[displayFields]="displayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
@@ -131,11 +157,20 @@
</div>
} @else if (displayMode === DisplayMode.LARGE_CARDS) {
<div class="row my-n2">
@if (loading && !documents.length) {
@for (row of placeholderRows; track row) {
<pngx-document-card-large
[document]="null"
[ngStyle]="{ opacity: 1 - (row * 1/placeholderRows.length) }"
[displayFields]="displayFields">
</pngx-document-card-large>
}
}
@for (d of documents; track d.id; let i = $index) {
<pngx-document-card-large
(dblClickDocument)="openDocumentDetail(d)"
[document]="!loading && show ? d : null"
[ngStyle]="{ opacity: !loading && show ? 1 : 1 - (i * 1/documents.length) }"
[document]="d"
[ngStyle]="{ opacity: show ? 1 : 1 - (i * 1/documents.length) }"
[displayFields]="displayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
@@ -6,12 +6,13 @@ import {
OnDestroy,
OnInit,
QueryList,
signal,
ViewChildren,
} from '@angular/core'
import { Router, RouterModule } from '@angular/router'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, Subject, takeUntil, tap } from 'rxjs'
import { Subject, takeUntil } from 'rxjs'
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
import { TagComponent } from 'src/app/components/common/tag/tag.component'
@@ -106,7 +107,7 @@ export class SavedViewWidgetComponent
@Input()
savedView: SavedView
documents: Document[] = []
private documentsSignal = signal<Document[]>([])
unsubscribeNotifier: Subject<any> = new Subject()
@@ -116,21 +117,56 @@ export class SavedViewWidgetComponent
mouseOnPreview = false
popoverHidden = true
displayMode: DisplayMode
private displayModeSignal = signal<DisplayMode>(null)
displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
private displayFieldsSignal = signal<DisplayField[]>(
DEFAULT_DASHBOARD_DISPLAY_FIELDS
)
count: number
private countSignal = signal<number>(null)
placeholderRows: number[] = []
get documents(): Document[] {
return this.documentsSignal()
}
set documents(value: Document[]) {
this.documentsSignal.set(value)
}
get displayMode(): DisplayMode {
return this.displayModeSignal()
}
set displayMode(value: DisplayMode) {
this.displayModeSignal.set(value)
}
get displayFields(): DisplayField[] {
return this.displayFieldsSignal()
}
set displayFields(value: DisplayField[]) {
this.displayFieldsSignal.set(value)
}
get count(): number {
return this.countSignal()
}
set count(value: number) {
this.countSignal.set(value)
}
ngOnInit(): void {
this.reload()
this.placeholderRows = Array.from(
{
length: this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
},
(_, index) => index
)
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
this.websocketStatusService
.onDocumentConsumptionFinished()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.reload()
})
if (
this.permissionsService.currentUserCan(
@@ -159,6 +195,14 @@ export class SavedViewWidgetComponent
this.settingsService.allDisplayFields.find((f) => f.id === field) !==
undefined
)
this.reload()
this.websocketStatusService
.onDocumentConsumptionFinished()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.reload()
})
}
ngOnDestroy(): void {
@@ -168,6 +212,7 @@ export class SavedViewWidgetComponent
reload() {
this.loading = this.documents.length == 0
this.show = true
this.documentService
.listFiltered(
1,
@@ -177,18 +222,13 @@ export class SavedViewWidgetComponent
this.savedView.filter_rules,
{ truncate_content: true }
)
.pipe(
takeUntil(this.unsubscribeNotifier),
tap((result) => {
this.show = true
this.documents = result.results
this.count = result.count
this.savedViewService.setDocumentCount(this.savedView, result.count)
}),
delay(500)
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((result) => {
this.documents = result.results
this.count = result.count
this.savedViewService.setDocumentCount(this.savedView, result.count)
this.loading = false
this.show = true
})
}
@@ -1,6 +1,6 @@
import { DecimalPipe } from '@angular/common'
import { HttpClient } from '@angular/common/http'
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'
import { RouterModule } from '@angular/router'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import * as mimeTypeNames from 'mime-names'
@@ -55,9 +55,24 @@ export class StatisticsWidgetComponent
private websocketConnectionService = inject(WebsocketStatusService)
private documentListViewService = inject(DocumentListViewService)
loading: boolean = false
private loadingSignal = signal(false)
private statisticsSignal = signal<Statistics>({})
statistics: Statistics = {}
get loading(): boolean {
return this.loadingSignal()
}
set loading(value: boolean) {
this.loadingSignal.set(value)
}
get statistics(): Statistics {
return this.statisticsSignal()
}
set statistics(value: Statistics) {
this.statisticsSignal.set(value)
}
subscription: Subscription
private unsubscribeNotifer: Subject<any> = new Subject()
@@ -1,19 +1,20 @@
@if (!cardless) {
<div class="card shadow-sm bg-light fade" [class.show]="show" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
@if (!cardless()) {
<div class="card shadow-sm bg-light fade" [class.show]="show" cdkDrag [cdkDragDisabled]="!draggable()" cdkDragPreviewContainer="parent">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
@if (draggable) {
@if (draggable()) {
<div class="ms-n2 me-1" cdkDragHandle>
<i-bs name="grip-vertical"></i-bs>
</div>
}
<h6 class="card-title mb-0">{{title}}</h6>
@if (badge) {
<span class="badge bg-info text-dark ms-2">{{badge}}</span>
<h6 class="card-title mb-0">{{title()}}</h6>
<ng-content select="[title-badge]"></ng-content>
@if (badge() !== null && badge() !== undefined) {
<span class="badge bg-info text-dark ms-2">{{badge()}}</span>
}
</div>
@if (loading) {
@if (loading()) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
}
@@ -1,3 +1,23 @@
i-bs {
cursor: move;
}
.fade.show {
animation: pngx-entry-fade 160ms ease-out;
}
@keyframes pngx-entry-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.fade.show {
animation: none;
}
}
@@ -45,21 +45,19 @@ describe('WidgetFrameComponent', () => {
})
it('should show title', () => {
component.title = 'Foo'
fixture.componentRef.setInput('title', 'Foo')
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain('Foo')
})
it('should show loading indicator', () => {
expect(fixture.debugElement.query(By.css('.spinner-border'))).toBeNull()
component.loading = true
fixture.componentRef.setInput('loading', true)
fixture.detectChanges()
expect(fixture.debugElement.query(By.css('.spinner-border'))).not.toBeNull()
})
it('should show', () => {
expect(component.show).toBeFalsy()
jest.advanceTimersByTime(100)
expect(component.show).toBeTruthy()
})
})
@@ -1,8 +1,7 @@
import { DragDropModule } from '@angular/cdk/drag-drop'
import { NgTemplateOutlet } from '@angular/common'
import { AfterViewInit, Component, Input } from '@angular/core'
import { AfterViewInit, Component, input, signal } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
@Component({
selector: 'pngx-widget-frame',
@@ -10,32 +9,28 @@ import { LoadingComponentWithPermissions } from 'src/app/components/loading-comp
styleUrls: ['./widget-frame.component.scss'],
imports: [DragDropModule, NgxBootstrapIconsModule, NgTemplateOutlet],
})
export class WidgetFrameComponent
extends LoadingComponentWithPermissions
implements AfterViewInit
{
constructor() {
super()
export class WidgetFrameComponent implements AfterViewInit {
private showSignal = signal(false)
loading = input(false)
get show(): boolean {
return this.showSignal()
}
@Input()
title: string
set show(value: boolean) {
this.showSignal.set(value)
}
@Input()
loading: boolean = false
title = input<string>()
@Input()
draggable: any
draggable = input<any>()
@Input()
cardless: boolean = false
cardless = input(false)
@Input()
badge: string
badge = input<string | number>(null)
ngAfterViewInit(): void {
setTimeout(() => {
this.show = true
}, 100)
this.show = true
}
}
@@ -445,8 +445,7 @@
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
@if (hasNext()) {
<button type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; next</button>
}
@if (!hasNext()) {
} @else {
<button type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; close</button>
}
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
@@ -473,6 +472,7 @@
<div class="preview-sticky pdf-viewer-container">
<pngx-pdf-viewer
[src]="pdfSource"
[password]="pdfPassword"
[renderMode]="PdfRenderMode.All"
[(page)]="previewCurrentPage"
[zoomScale]="previewZoomScale"
@@ -853,6 +853,7 @@ describe('DocumentDetailComponent', () => {
.find((b) => b.nativeElement.textContent === 'Save & next')
).toBeUndefined()
nextSpy.mockReturnValue(true)
component.networkActive = true
fixture.detectChanges()
expect(
fixture.debugElement
@@ -1530,11 +1531,13 @@ describe('DocumentDetailComponent', () => {
})
it('should clear pdf source if preview URL is empty', () => {
component.pdfSource = { url: '/preview', password: 'secret' } as any
component.pdfSource = '/preview'
component.pdfPassword = 'secret'
component.previewUrl = null
;(component as any).updatePdfSource()
expect(component.pdfSource).toEqual({ url: null, password: undefined })
expect(component.pdfSource).toEqual(null)
expect(component.pdfPassword).toBeUndefined()
})
it('should close incoming update modal if one is open', () => {
@@ -1871,9 +1874,8 @@ describe('DocumentDetailComponent', () => {
expect(component.thumbUrl).toBe('thumb-version')
expect(component.previewText).toBe('version text')
expect(component.documentForm.get('content').value).toBe('version-content')
const pdfSource = component.pdfSource as { url: string; password?: string }
expect(pdfSource.url).toBe('preview-version')
expect(pdfSource.password).toBeUndefined()
expect(component.pdfSource).toBe('preview-version')
expect(component.pdfPassword).toBeUndefined()
previewSpy.mockReturnValueOnce('preview-error')
component.selectVersion(11)
@@ -1,6 +1,13 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
import { HttpClient, HttpResponse } from '@angular/common/http'
import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'
import {
Component,
inject,
OnDestroy,
OnInit,
signal,
ViewChild,
} from '@angular/core'
import {
FormArray,
FormControl,
@@ -115,7 +122,6 @@ import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
import { PngxPdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
import {
PdfRenderMode,
PdfSource,
PdfZoomLevel,
PdfZoomScale,
PngxPdfDocumentProxy,
@@ -228,33 +234,34 @@ export class DocumentDetailComponent
expandOriginalMetadata = false
expandArchivedMetadata = false
error: any
private errorSignal = signal<any>(undefined)
networkActive = false
private networkActiveSignal = signal(false)
documentId: number
document: Document
metadata: DocumentMetadata
suggestions: DocumentSuggestions
suggestionsLoading: boolean = false
users: User[]
private documentIdSignal = signal<number>(undefined)
private documentSignal = signal<Document>(undefined)
private metadataSignal = signal<DocumentMetadata>(undefined)
private suggestionsSignal = signal<DocumentSuggestions>(undefined)
private suggestionsLoadingSignal = signal(false)
private usersSignal = signal<User[]>(undefined)
title: string
private titleSignal = signal<string>(undefined)
titleSubject: Subject<string> = new Subject()
previewUrl: string
pdfSource?: PdfSource
thumbUrl: string
previewText: string
previewLoaded: boolean = false
tiffURL: string
tiffError: string
private previewUrlSignal = signal<string>(undefined)
private pdfSourceSignal = signal<string>(undefined)
private pdfPasswordSignal = signal<string>(undefined)
private thumbUrlSignal = signal<string>(undefined)
private previewTextSignal = signal<string>(undefined)
private previewLoadedSignal = signal(false)
private tiffURLSignal = signal<string>(undefined)
private tiffErrorSignal = signal<string>(undefined)
// Versioning
selectedVersionId: number
private selectedVersionIdSignal = signal<number>(undefined)
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
private correspondentsSignal = signal<Correspondent[]>(undefined)
private documentTypesSignal = signal<DocumentType[]>(undefined)
private storagePathsSignal = signal<StoragePath[]>(undefined)
documentForm: FormGroup = new FormGroup({
title: new FormControl(''),
@@ -269,10 +276,10 @@ export class DocumentDetailComponent
custom_fields: new FormArray([]),
})
previewCurrentPage: number = 1
previewNumPages: number
previewZoomSetting: PdfZoomLevel = PdfZoomLevel.One
previewZoomScale: PdfZoomScale = PdfZoomScale.PageWidth
private previewCurrentPageSignal = signal(1)
private previewNumPagesSignal = signal<number>(undefined)
private previewZoomSettingSignal = signal<PdfZoomLevel>(PdfZoomLevel.One)
private previewZoomScaleSignal = signal<PdfZoomScale>(PdfZoomScale.PageWidth)
store: BehaviorSubject<any>
isDirty$: Observable<boolean>
@@ -287,9 +294,9 @@ export class DocumentDetailComponent
ogDate: Date
customFields: CustomField[]
private customFieldsSignal = signal<CustomField[]>(undefined)
public downloading: boolean = false
private downloadingSignal = signal(false)
public useFormattedFilename: boolean = false
public readonly CustomFieldDataType = CustomFieldDataType
@@ -314,13 +321,238 @@ export class DocumentDetailComponent
}
DocumentDetailNavIDs = DocumentDetailNavIDs
activeNavID: number
private activeNavIDSignal = signal<number>(undefined)
get error(): any {
return this.errorSignal()
}
set error(value: any) {
this.errorSignal.set(value)
}
get networkActive(): boolean {
return this.networkActiveSignal()
}
set networkActive(value: boolean) {
this.networkActiveSignal.set(value)
}
get documentId(): number {
return this.documentIdSignal()
}
set documentId(value: number) {
this.documentIdSignal.set(value)
}
get document(): Document {
return this.documentSignal()
}
set document(value: Document) {
this.documentSignal.set(value)
}
get metadata(): DocumentMetadata {
return this.metadataSignal()
}
set metadata(value: DocumentMetadata) {
this.metadataSignal.set(value)
}
get suggestions(): DocumentSuggestions {
return this.suggestionsSignal()
}
set suggestions(value: DocumentSuggestions) {
this.suggestionsSignal.set(value)
}
get suggestionsLoading(): boolean {
return this.suggestionsLoadingSignal()
}
set suggestionsLoading(value: boolean) {
this.suggestionsLoadingSignal.set(value)
}
get users(): User[] {
return this.usersSignal()
}
set users(value: User[]) {
this.usersSignal.set(value)
}
get title(): string {
return this.titleSignal()
}
set title(value: string) {
this.titleSignal.set(value)
}
get previewUrl(): string {
return this.previewUrlSignal()
}
set previewUrl(value: string) {
this.previewUrlSignal.set(value)
}
get pdfSource(): string {
return this.pdfSourceSignal()
}
set pdfSource(value: string) {
this.pdfSourceSignal.set(value)
}
get pdfPassword(): string {
return this.pdfPasswordSignal()
}
set pdfPassword(value: string) {
this.pdfPasswordSignal.set(value)
}
get thumbUrl(): string {
return this.thumbUrlSignal()
}
set thumbUrl(value: string) {
this.thumbUrlSignal.set(value)
}
get previewText(): string {
return this.previewTextSignal()
}
set previewText(value: string) {
this.previewTextSignal.set(value)
}
get previewLoaded(): boolean {
return this.previewLoadedSignal()
}
set previewLoaded(value: boolean) {
this.previewLoadedSignal.set(value)
}
get tiffURL(): string {
return this.tiffURLSignal()
}
set tiffURL(value: string) {
this.tiffURLSignal.set(value)
}
get tiffError(): string {
return this.tiffErrorSignal()
}
set tiffError(value: string) {
this.tiffErrorSignal.set(value)
}
get selectedVersionId(): number {
return this.selectedVersionIdSignal()
}
set selectedVersionId(value: number) {
this.selectedVersionIdSignal.set(value)
}
get correspondents(): Correspondent[] {
return this.correspondentsSignal()
}
set correspondents(value: Correspondent[]) {
this.correspondentsSignal.set(value)
}
get documentTypes(): DocumentType[] {
return this.documentTypesSignal()
}
set documentTypes(value: DocumentType[]) {
this.documentTypesSignal.set(value)
}
get storagePaths(): StoragePath[] {
return this.storagePathsSignal()
}
set storagePaths(value: StoragePath[]) {
this.storagePathsSignal.set(value)
}
get previewCurrentPage(): number {
return this.previewCurrentPageSignal()
}
set previewCurrentPage(value: number) {
this.previewCurrentPageSignal.set(value)
}
get previewNumPages(): number {
return this.previewNumPagesSignal()
}
set previewNumPages(value: number) {
this.previewNumPagesSignal.set(value)
}
get previewZoomSetting(): PdfZoomLevel {
return this.previewZoomSettingSignal()
}
set previewZoomSetting(value: PdfZoomLevel) {
this.previewZoomSettingSignal.set(value)
}
get previewZoomScale(): PdfZoomScale {
return this.previewZoomScaleSignal()
}
set previewZoomScale(value: PdfZoomScale) {
this.previewZoomScaleSignal.set(value)
}
get downloading(): boolean {
return this.downloadingSignal()
}
set downloading(value: boolean) {
this.downloadingSignal.set(value)
}
get activeNavID(): number {
return this.activeNavIDSignal()
}
set activeNavID(value: number) {
this.activeNavIDSignal.set(value)
}
get customFields(): CustomField[] {
return this.customFieldsSignal()
}
set customFields(value: CustomField[]) {
this.customFieldsSignal.set(value)
}
titleKeyUp(event) {
this.titleSubject.next(event.target?.value)
}
get useNativePdfViewer(): boolean {
this.settings.trackChanges()
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
}
@@ -329,10 +561,12 @@ export class DocumentDetailComponent
}
get aiEnabled(): boolean {
this.settings.trackChanges()
return this.settings.get(SETTINGS_KEYS.AI_ENABLED)
}
get archiveContentRenderType(): ContentRenderType {
this.settings.trackChanges()
const hasArchiveVersion =
this.metadata?.has_archive_version ?? !!this.document?.archived_file_name
return hasArchiveVersion
@@ -343,16 +577,19 @@ export class DocumentDetailComponent
}
get originalContentRenderType(): ContentRenderType {
this.settings.trackChanges()
return this.getRenderType(
this.metadata?.original_mime_type || this.document?.mime_type
)
}
get showThumbnailOverlay(): boolean {
this.settings.trackChanges()
return this.settings.get(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL)
}
isFieldHidden(fieldId: DocumentDetailFieldID): boolean {
this.settings.trackChanges()
return this.settings
.get(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
.includes(fieldId)
@@ -375,10 +612,8 @@ export class DocumentDetailComponent
}
private updatePdfSource() {
this.pdfSource = {
url: this.previewUrl,
password: this.password,
}
this.pdfSource = this.previewUrl
this.pdfPassword = this.password
}
private loadMetadataForSelectedVersion() {
@@ -1494,9 +1729,7 @@ export class DocumentDetailComponent
pdfPreviewLoaded(pdf: PngxPdfDocumentProxy) {
this.previewNumPages = pdf.numPages
if (this.password) this.requiresPassword = false
setTimeout(() => {
this.previewLoaded = true
}, 300)
this.previewLoaded = true
}
onError(event) {
@@ -2,6 +2,26 @@
overflow-wrap: anywhere;
}
.fade.show {
animation: pngx-entry-fade 160ms ease-out;
}
@keyframes pngx-entry-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.fade.show {
animation: none;
}
}
.doc-img-container {
position: relative;
}
@@ -14,7 +14,6 @@ import {
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, of } from 'rxjs'
import {
DEFAULT_DISPLAY_FIELDS,
DisplayField,
@@ -108,11 +107,7 @@ export class DocumentCardLargeComponent
popoverHidden = true
ngAfterViewInit(): void {
of(true)
.pipe(delay(50))
.subscribe(() => {
this.show = true
})
this.show = true
}
get searchScoreClass() {
@@ -2,6 +2,26 @@
font-size: 90%;
}
.fade.show {
animation: pngx-entry-fade 160ms ease-out;
}
@keyframes pngx-entry-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.fade.show {
animation: none;
}
}
.doc-img {
object-fit: cover;
object-position: top left;
@@ -14,8 +14,6 @@ import {
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { delay } from 'rxjs/operators'
import {
DEFAULT_DISPLAY_FIELDS,
DisplayField,
@@ -101,11 +99,7 @@ export class DocumentCardSmallComponent
@ViewChild('popupPreview') popupPreview: PreviewPopupComponent
ngAfterViewInit(): void {
of(true)
.pipe(delay(50))
.subscribe(() => {
this.show = true
})
this.show = true
}
getIsThumbInverted() {
@@ -5,6 +5,7 @@ import {
OnInit,
Output,
inject,
signal,
} from '@angular/core'
import {
FormControl,
@@ -32,29 +33,42 @@ import { TextComponent } from '../../common/input/text/text.component'
})
export class SaveViewConfigDialogComponent implements OnInit {
private modal = inject(NgbActiveModal)
private errorSignal = signal(undefined)
private buttonsEnabledSignal = signal(true)
private defaultNameSignal = signal('')
@Output()
public saveClicked = new EventEmitter()
@Input()
error
get error() {
return this.errorSignal()
}
set error(error) {
this.errorSignal.set(error)
}
@Input()
buttonsEnabled = true
get buttonsEnabled(): boolean {
return this.buttonsEnabledSignal()
}
set buttonsEnabled(buttonsEnabled: boolean) {
this.buttonsEnabledSignal.set(buttonsEnabled)
}
closeEnabled = false
users: User[]
_defaultName = ''
get defaultName() {
return this._defaultName
return this.defaultNameSignal()
}
@Input()
set defaultName(value: string) {
this._defaultName = value
this.defaultNameSignal.set(value)
this.saveViewConfigForm.patchValue({ name: value })
}
@@ -1,4 +1,4 @@
import { Directive, OnDestroy } from '@angular/core'
import { Directive, OnDestroy, signal } from '@angular/core'
import { Subject } from 'rxjs'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
@@ -7,8 +7,24 @@ export abstract class LoadingComponentWithPermissions
extends ComponentWithPermissions
implements OnDestroy
{
public loading: boolean = true
public show: boolean = false
private loadingSignal = signal(true)
private showSignal = signal(false)
public get loading(): boolean {
return this.loadingSignal()
}
public set loading(value: boolean) {
this.loadingSignal.set(value)
}
public get show(): boolean {
return this.showSignal()
}
public set show(value: boolean) {
this.showSignal.set(value)
}
protected unsubscribeNotifier: Subject<any> = new Subject()
@@ -1,4 +1,4 @@
import { Component, OnInit, inject } from '@angular/core'
import { Component, OnInit, inject, signal } from '@angular/core'
import { RouterModule } from '@angular/router'
import {
NgbDropdownModule,
@@ -6,7 +6,7 @@ import {
NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs'
import { 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'
@@ -51,21 +51,29 @@ export class CustomFieldsComponent
private readonly documentService = inject(DocumentService)
private readonly savedViewService = inject(SavedViewService)
public fields: CustomField[] = []
private fieldsSignal = signal<CustomField[]>([])
public get fields(): CustomField[] {
return this.fieldsSignal()
}
public set fields(fields: CustomField[]) {
this.fieldsSignal.set(fields)
}
ngOnInit() {
this.reload()
}
reload() {
this.loading = true
this.customFieldsService
.listAll()
.pipe(
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.fields = r.results
}),
delay(100)
})
)
.subscribe(() => {
this.show = true
@@ -1,11 +1,10 @@
import { NgComponentOutlet } from '@angular/common'
import {
AfterViewChecked,
ChangeDetectorRef,
Component,
inject,
OnDestroy,
OnInit,
signal,
Type,
ViewChild,
} from '@angular/core'
@@ -70,13 +69,10 @@ interface DocumentAttributesSection {
ClearableBadgeComponent,
],
})
export class DocumentAttributesComponent
implements OnInit, OnDestroy, AfterViewChecked
{
export class DocumentAttributesComponent implements OnInit, OnDestroy {
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
@@ -136,11 +132,20 @@ export class DocumentAttributesComponent
]
@ViewChild('activeOutlet', { read: NgComponentOutlet })
private readonly activeOutlet?: NgComponentOutlet
set activeOutlet(outlet: NgComponentOutlet | undefined) {
this.activeComponentSignal.set(outlet?.componentInstance ?? null)
}
private lastHeaderLoading: boolean
private activeComponentSignal = signal<unknown>(null)
private activeNavIDSignal = signal<number>(null)
activeNavID: number = null
get activeNavID(): number {
return this.activeNavIDSignal()
}
set activeNavID(activeNavID: number) {
this.activeNavIDSignal.set(activeNavID)
}
get visibleSections(): DocumentAttributesSection[] {
return this.sections.filter((section) =>
@@ -163,14 +168,14 @@ export class DocumentAttributesComponent
this.activeSection?.kind !== DocumentAttributesSectionKind.ManagementList
)
return null
const instance = this.activeOutlet?.componentInstance
const instance = this.activeComponentSignal()
return instance instanceof ManagementListComponent ? instance : null
}
get activeCustomFields(): CustomFieldsComponent | null {
if (this.activeSection?.kind !== DocumentAttributesSectionKind.CustomFields)
return null
const instance = this.activeOutlet?.componentInstance
const instance = this.activeComponentSignal()
return instance instanceof CustomFieldsComponent ? instance : null
}
@@ -221,14 +226,6 @@ export class DocumentAttributesComponent
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) {
@@ -5,13 +5,13 @@ import {
OnDestroy,
OnInit,
QueryList,
signal,
ViewChildren,
} from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject } from 'rxjs'
import {
debounceTime,
delay,
distinctUntilChanged,
takeUntil,
tap,
@@ -88,25 +88,89 @@ export abstract class ManagementListComponent<T extends MatchingModel>
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
public data: T[] = []
private unfilteredData: T[] = []
private dataSignal = signal<T[]>([])
private unfilteredDataSignal = signal<T[]>([])
private currentExtraParams: { [key: string]: any } = null
private allSelectionActive = false
public page = 1
private pageSignal = signal(1)
public collectionSize = 0
public displayCollectionSize = 0
private collectionSizeSignal = signal(0)
private displayCollectionSizeSignal = signal(0)
public sortField: string
public sortReverse: boolean
private sortFieldSignal = signal<string>(undefined)
private sortReverseSignal = signal<boolean>(undefined)
private nameFilterDebounce: Subject<string>
protected unsubscribeNotifier: Subject<any> = new Subject()
protected _nameFilter: string
public selectedObjects: Set<number> = new Set()
public togggleAll: boolean = false
private togggleAllSignal = signal(false)
public get data(): T[] {
return this.dataSignal()
}
public set data(data: T[]) {
this.dataSignal.set(data)
}
private get unfilteredData(): T[] {
return this.unfilteredDataSignal()
}
private set unfilteredData(data: T[]) {
this.unfilteredDataSignal.set(data)
}
public get page(): number {
return this.pageSignal()
}
public set page(page: number) {
this.pageSignal.set(page)
}
public get collectionSize(): number {
return this.collectionSizeSignal()
}
public set collectionSize(collectionSize: number) {
this.collectionSizeSignal.set(collectionSize)
}
public get displayCollectionSize(): number {
return this.displayCollectionSizeSignal()
}
public set displayCollectionSize(displayCollectionSize: number) {
this.displayCollectionSizeSignal.set(displayCollectionSize)
}
public get sortField(): string {
return this.sortFieldSignal()
}
public set sortField(sortField: string) {
this.sortFieldSignal.set(sortField)
}
public get sortReverse(): boolean {
return this.sortReverseSignal()
}
public set sortReverse(sortReverse: boolean) {
this.sortReverseSignal.set(sortReverse)
}
public get togggleAll(): boolean {
return this.togggleAllSignal()
}
public set togggleAll(togggleAll: boolean) {
this.togggleAllSignal.set(togggleAll)
}
public get hasSelection(): boolean {
return this.selectedObjects.size > 0 || this.allSelectionActive
@@ -201,8 +265,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
this.data = this.filterData(c.results)
this.collectionSize = this.getCollectionSize(c)
this.displayCollectionSize = this.getDisplayCollectionSize(c)
}),
delay(100)
})
)
.subscribe({
error: (error: HttpErrorResponse) => {
@@ -1,9 +1,9 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ActivatedRoute } from '@angular/router'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, delay, first, takeUntil, tap } from 'rxjs'
import { Subject, first, takeUntil, tap } from 'rxjs'
import { MailAccount, MailAccountType } from 'src/app/data/mail-account'
import { MailRule } from 'src/app/data/mail-rule'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
@@ -56,19 +56,32 @@ export class MailComponent
public MailAccountType = MailAccountType
private _mailAccounts: MailAccount[] = []
private mailAccountsSignal = signal<MailAccount[]>([])
public get mailAccounts() {
return this._mailAccounts
return this.mailAccountsSignal()
}
private set mailAccounts(accounts: MailAccount[]) {
this._mailAccounts = accounts
this.mailAccountsSignal.set(accounts)
this.mailAccountsById = new Map(
accounts.map((account) => [account.id, account])
)
}
public mailAccountsById: Map<number, MailAccount> = new Map()
public mailRules: MailRule[] = []
private mailAccountsByIdSignal = signal<Map<number, MailAccount>>(new Map())
public get mailAccountsById(): Map<number, MailAccount> {
return this.mailAccountsByIdSignal()
}
private set mailAccountsById(value: Map<number, MailAccount>) {
this.mailAccountsByIdSignal.set(value)
}
private mailRulesSignal = signal<MailRule[]>([])
public get mailRules(): MailRule[] {
return this.mailRulesSignal()
}
public set mailRules(value: MailRule[]) {
this.mailRulesSignal.set(value)
}
unsubscribeNotifier: Subject<any> = new Subject()
oAuthAccountId: number
@@ -81,10 +94,35 @@ export class MailComponent
return this.settingsService.get(SETTINGS_KEYS.OUTLOOK_OAUTH_URL)
}
public loadingRules: boolean = true
public showRules: boolean = false
public loadingAccounts: boolean = true
public showAccounts: boolean = false
private loadingRulesSignal = signal(true)
private showRulesSignal = signal(false)
private loadingAccountsSignal = signal(true)
private showAccountsSignal = signal(false)
public get loadingRules(): boolean {
return this.loadingRulesSignal()
}
public set loadingRules(value: boolean) {
this.loadingRulesSignal.set(value)
}
public get showRules(): boolean {
return this.showRulesSignal()
}
public set showRules(value: boolean) {
this.showRulesSignal.set(value)
}
public get loadingAccounts(): boolean {
return this.loadingAccountsSignal()
}
public set loadingAccounts(value: boolean) {
this.loadingAccountsSignal.set(value)
}
public get showAccounts(): boolean {
return this.showAccountsSignal()
}
public set showAccounts(value: boolean) {
this.showAccountsSignal.set(value)
}
ngOnInit(): void {
this.mailAccountService
@@ -94,6 +132,8 @@ export class MailComponent
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.mailAccounts = r.results
this.loadingAccounts = false
this.showAccounts = true
if (this.oAuthAccountId) {
this.editMailAccount(
this.mailAccounts.find(
@@ -101,15 +141,11 @@ export class MailComponent
)
)
}
}),
delay(100)
})
)
.subscribe({
next: () => {
this.loadingAccounts = false
this.showAccounts = true
},
error: (e) => {
this.loadingAccounts = false
this.toastService.showError(
$localize`Error retrieving mail accounts`,
e
@@ -124,15 +160,13 @@ export class MailComponent
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.mailRules = r.results
}),
delay(100)
)
.subscribe({
next: (r) => {
this.loadingRules = false
this.showRules = true
},
})
)
.subscribe({
error: (e) => {
this.loadingRules = false
this.toastService.showError($localize`Error retrieving mail rules`, e)
},
})
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common'
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import {
FormControl,
FormGroup,
@@ -56,7 +56,7 @@ export class SavedViewsComponent
DisplayMode = DisplayMode
public savedViews: SavedView[]
private savedViewsSignal = signal<SavedView[]>(undefined)
private savedViewsGroup = new FormGroup({})
public savedViewsForm: FormGroup = new FormGroup({
savedViews: this.savedViewsGroup,
@@ -69,6 +69,14 @@ export class SavedViewsComponent
return this.settings.allDisplayFields
}
public get savedViews(): SavedView[] {
return this.savedViewsSignal()
}
public set savedViews(savedViews: SavedView[]) {
this.savedViewsSignal.set(savedViews)
}
constructor() {
super()
this.settings.organizingSidebarSavedViews = true
@@ -1,8 +1,8 @@
import { Component, OnInit, inject } from '@angular/core'
import { Component, OnInit, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs'
import { takeUntil, tap } from 'rxjs'
import { Workflow } from 'src/app/data/workflow'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsService } from 'src/app/services/permissions.service'
@@ -39,7 +39,15 @@ export class WorkflowsComponent
private modalService = inject(NgbModal)
private toastService = inject(ToastService)
public workflows: Workflow[] = []
private workflowsSignal = signal<Workflow[]>([])
public get workflows(): Workflow[] {
return this.workflowsSignal()
}
public set workflows(workflows: Workflow[]) {
this.workflowsSignal.set(workflows)
}
ngOnInit() {
this.reload()
@@ -51,8 +59,7 @@ export class WorkflowsComponent
.listAll()
.pipe(
takeUntil(this.unsubscribeNotifier),
tap((r) => (this.workflows = r.results)),
delay(100)
tap((r) => (this.workflows = r.results))
)
.subscribe(() => {
this.show = true
@@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core'
import { Injectable, inject, signal } from '@angular/core'
import { ParamMap, Router, UrlTree } from '@angular/router'
import { Observable, Subject, takeUntil } from 'rxjs'
import {
@@ -118,6 +118,7 @@ export class DocumentListViewService {
isReloading: boolean = false
initialized: boolean = false
error: string = null
private stateVersion = signal(0)
rangeSelectionAnchorIndex: number
lastRangeSelectionToIndex: number
@@ -132,6 +133,14 @@ export class DocumentListViewService {
private displayFieldsInitialized: boolean = false
private markChanged(): void {
this.stateVersion.update((version) => version + 1)
}
private trackState(): void {
this.stateVersion()
}
private restoreListViewState(savedState: unknown): ListViewState {
const newState = this.defaultListViewState()
@@ -159,10 +168,12 @@ export class DocumentListViewService {
}
get activeSavedViewId() {
this.trackState()
return this._activeSavedViewId
}
get activeSavedViewTitle() {
this.trackState()
return this.activeListViewState.title
}
@@ -191,6 +202,7 @@ export class DocumentListViewService {
)
this.saveDocumentListView()
}
this.markChanged()
})
}
@@ -243,6 +255,7 @@ export class DocumentListViewService {
} else {
this._activeSavedViewId = null
}
this.markChanged()
}
activateSavedViewWithQueryParams(view: SavedView, queryParams: ParamMap) {
@@ -267,6 +280,7 @@ export class DocumentListViewService {
this.activeListViewState.displayFields = view.display_fields
this.reduceSelectionToFilter()
this.markChanged()
if (!this.router.routerState.snapshot.url.includes('/view/')) {
this.router.navigate(['view', view.id])
@@ -297,6 +311,7 @@ export class DocumentListViewService {
this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage
this.markChanged()
this.reload(null, paramsEmpty) // update the params if there aren't any
}
}
@@ -305,6 +320,7 @@ export class DocumentListViewService {
this.cancelPending()
this.isReloading = true
this.error = null
this.markChanged()
let activeListViewState = this.activeListViewState
this.documentService
.listFiltered(
@@ -325,6 +341,7 @@ export class DocumentListViewService {
activeListViewState.documents = result.results
this.selectionData = resultWithSelectionData.selection_data ?? null
this.syncSelectedToCurrentPage()
this.markChanged()
if (updateQueryParams && !this._activeSavedViewId) {
let base = ['/documents']
@@ -343,12 +360,14 @@ export class DocumentListViewService {
onFinish()
}
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
this.markChanged()
},
error: (error) => {
this.isReloading = false
if (activeListViewState.currentPage != 1 && error.status == 404) {
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
activeListViewState.currentPage = 1
this.markChanged()
this.reload()
} else if (
activeListViewState.sortField.indexOf('custom_field') === 0 &&
@@ -381,6 +400,7 @@ export class DocumentListViewService {
errorMessage = error.error
}
this.error = errorMessage
this.markChanged()
}
},
})
@@ -397,12 +417,14 @@ export class DocumentListViewService {
if (resetPage) {
this.activeListViewState.currentPage = 1
}
this.markChanged()
this.reload()
this.reduceSelectionToFilter()
this.saveDocumentListView()
}
get filterRules(): FilterRule[] {
this.trackState()
return this.activeListViewState.filterRules
}
@@ -416,48 +438,58 @@ export class DocumentListViewService {
set sortField(field: string) {
this.activeListViewState.sortField = field
this.markChanged()
this.reload()
this.saveDocumentListView()
}
get sortField(): string {
this.trackState()
return this.activeListViewState.sortField
}
set sortReverse(reverse: boolean) {
this.activeListViewState.sortReverse = reverse
this.markChanged()
this.reload()
this.saveDocumentListView()
}
get sortReverse(): boolean {
this.trackState()
return this.activeListViewState.sortReverse
}
get collectionSize(): number {
this.trackState()
return this.activeListViewState.collectionSize
}
get currentPage(): number {
this.trackState()
return this.activeListViewState.currentPage
}
set currentPage(page: number) {
if (this.activeListViewState.currentPage == page) return
this.activeListViewState.currentPage = page
this.markChanged()
this.reload()
this.saveDocumentListView()
}
get documents(): Document[] {
this.trackState()
return this.activeListViewState.documents
}
get selected(): Set<number> {
this.trackState()
return this.activeListViewState.selected
}
get allSelected(): boolean {
this.trackState()
return this.activeListViewState.allSelected ?? false
}
@@ -474,16 +506,19 @@ export class DocumentListViewService {
setSort(field: string, reverse: boolean) {
this.activeListViewState.sortField = field
this.activeListViewState.sortReverse = reverse
this.markChanged()
this.reload()
this.saveDocumentListView()
}
set displayMode(mode: DisplayMode) {
this.activeListViewState.displayMode = mode
this.markChanged()
this.saveDocumentListView()
}
get displayMode(): DisplayMode {
this.trackState()
const mode = this.activeListViewState.displayMode ?? DisplayMode.SMALL_CARDS
if (mode === ('details' as any)) {
// legacy
@@ -494,11 +529,13 @@ export class DocumentListViewService {
set pageSize(size: number) {
this.activeListViewState.pageSize = size
this.markChanged()
this.reload()
this.saveDocumentListView()
}
get pageSize(): number {
this.trackState()
return (
this.activeListViewState.pageSize ??
this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
@@ -506,6 +543,7 @@ export class DocumentListViewService {
}
get displayFields(): DisplayField[] {
this.trackState()
return this.activeListViewState.displayFields ?? LIST_DEFAULT_DISPLAY_FIELDS
}
@@ -517,6 +555,7 @@ export class DocumentListViewService {
undefined
)
: fields
this.markChanged()
this.saveDocumentListView()
}
@@ -540,6 +579,7 @@ export class DocumentListViewService {
quickFilter(filterRules: FilterRule[]) {
this._activeSavedViewId = null
this.markChanged()
this.setFilterRules(filterRules)
this.router.navigate(['documents'])
}
@@ -628,6 +668,7 @@ export class DocumentListViewService {
this.activeListViewState.allSelected = false
this.selected.clear()
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
this.markChanged()
}
reduceSelectionToFilter() {
@@ -644,6 +685,7 @@ export class DocumentListViewService {
this.selected.delete(id)
}
}
this.markChanged()
})
}
}
@@ -651,6 +693,7 @@ export class DocumentListViewService {
selectAll() {
this.activeListViewState.allSelected = true
this.syncSelectedToCurrentPage()
this.markChanged()
}
selectPage() {
@@ -659,6 +702,7 @@ export class DocumentListViewService {
this.documents.forEach((doc) => {
this.selected.add(doc.id)
})
this.markChanged()
}
isSelected(d: Document) {
@@ -673,6 +717,7 @@ export class DocumentListViewService {
else this.selected.add(d.id)
this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id)
this.lastRangeSelectionToIndex = null
this.markChanged()
}
selectRangeTo(d: Document) {
@@ -710,6 +755,7 @@ export class DocumentListViewService {
this.selected.add(d.id)
})
this.lastRangeSelectionToIndex = documentToIndex
this.markChanged()
} else {
// e.g. shift key but was first click
this.toggleSelected(d)
@@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core'
import { Injectable, inject, signal } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Observable, Subject, of } from 'rxjs'
import { first } from 'rxjs/operators'
@@ -15,6 +15,7 @@ export class OpenDocumentsService {
private modalService = inject(NgbModal)
private MAX_OPEN_DOCUMENTS = 5
private stateVersion = signal(0)
constructor() {
this.load()
@@ -36,6 +37,14 @@ export class OpenDocumentsService {
private openDocuments: Document[] = []
private dirtyDocuments: Set<number> = new Set<number>()
private trackState(): void {
this.stateVersion()
}
private markChanged(): void {
this.stateVersion.update((version) => version + 1)
}
refreshDocument(id: number) {
let index = this.openDocuments.findIndex((doc) => doc.id == id)
if (index > -1) {
@@ -53,10 +62,12 @@ export class OpenDocumentsService {
}
getOpenDocuments(): Document[] {
this.trackState()
return this.openDocuments
}
getOpenDocument(id: number): Document {
this.trackState()
return this.openDocuments.find((d) => d.id == id)
}
@@ -101,10 +112,12 @@ export class OpenDocumentsService {
}
hasDirty(): boolean {
this.trackState()
return this.dirtyDocuments.size > 0
}
isDirty(doc: Document): boolean {
this.trackState()
return this.dirtyDocuments.has(doc.id)
}
@@ -170,6 +183,7 @@ export class OpenDocumentsService {
}
save() {
this.markChanged()
try {
sessionStorage.setItem(
OPEN_DOCUMENT_SERVICE.DOCUMENTS,
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import { inject, Injectable, signal } from '@angular/core'
import { combineLatest, Observable, Subject } from 'rxjs'
import { takeUntil, tap } from 'rxjs/operators'
import { Results } from 'src/app/data/results'
@@ -17,8 +17,8 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
private settingsService = inject(SettingsService)
private documentService = inject(DocumentService)
private savedViews: SavedView[] = []
private savedViewDocumentCounts: Map<number, number> = new Map()
private savedViewsSignal = signal<SavedView[]>([])
private savedViewDocumentCountsSignal = signal<Map<number, number>>(new Map())
private unsubscribeNotifier: Subject<void> = new Subject<void>()
constructor() {
@@ -26,6 +26,22 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
this.resourceName = 'saved_views'
}
private get savedViews(): SavedView[] {
return this.savedViewsSignal()
}
private set savedViews(views: SavedView[]) {
this.savedViewsSignal.set(views)
}
private get savedViewDocumentCounts(): Map<number, number> {
return this.savedViewDocumentCountsSignal()
}
private set savedViewDocumentCounts(counts: Map<number, number>) {
this.savedViewDocumentCountsSignal.set(counts)
}
public list(
page?: number,
pageSize?: number,
@@ -176,7 +192,9 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
}
public setDocumentCount(view: SavedView, count: number) {
this.savedViewDocumentCounts.set(view.id, count)
const counts = new Map(this.savedViewDocumentCounts)
counts.set(view.id, count)
this.savedViewDocumentCounts = counts
}
public getDocumentCount(view: SavedView): number {
+97 -53
View File
@@ -7,6 +7,7 @@ import {
LOCALE_ID,
Renderer2,
RendererFactory2,
signal,
} from '@angular/core'
import { Meta } from '@angular/platform-browser'
import { CookieService } from 'ngx-cookie-service'
@@ -294,7 +295,16 @@ export class SettingsService {
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
private settings: Record<string, any> = {}
currentUser: User
private settingsVersion = signal(0)
private currentUserSignal = signal<User>(undefined)
get currentUser(): User {
return this.currentUserSignal()
}
set currentUser(value: User) {
this.currentUserSignal.set(value)
}
public settingsSaved: EventEmitter<any> = new EventEmitter()
@@ -303,16 +313,48 @@ export class SettingsService {
return this._renderer
}
public dashboardIsEmpty: boolean = false
private dashboardIsEmptySignal = signal(false)
private globalDropzoneEnabledSignal = signal(true)
private globalDropzoneActiveSignal = signal(false)
private organizingSidebarSavedViewsSignal = signal(false)
public globalDropzoneEnabled: boolean = true
public globalDropzoneActive: boolean = false
public organizingSidebarSavedViews: boolean = false
public get dashboardIsEmpty(): boolean {
return this.dashboardIsEmptySignal()
}
private _allDisplayFields: Array<{ id: DisplayField; name: string }> =
DEFAULT_DISPLAY_FIELDS
public set dashboardIsEmpty(value: boolean) {
this.dashboardIsEmptySignal.set(value)
}
public get globalDropzoneEnabled(): boolean {
return this.globalDropzoneEnabledSignal()
}
public set globalDropzoneEnabled(value: boolean) {
this.globalDropzoneEnabledSignal.set(value)
}
public get globalDropzoneActive(): boolean {
return this.globalDropzoneActiveSignal()
}
public set globalDropzoneActive(value: boolean) {
this.globalDropzoneActiveSignal.set(value)
}
public get organizingSidebarSavedViews(): boolean {
return this.organizingSidebarSavedViewsSignal()
}
public set organizingSidebarSavedViews(value: boolean) {
this.organizingSidebarSavedViewsSignal.set(value)
}
private allDisplayFieldsSignal = signal<
Array<{ id: DisplayField; name: string }>
>(DEFAULT_DISPLAY_FIELDS)
public get allDisplayFields(): Array<{ id: DisplayField; name: string }> {
return this._allDisplayFields
return this.allDisplayFieldsSignal()
}
public displayFieldsInit: EventEmitter<boolean> = new EventEmitter()
@@ -326,6 +368,10 @@ export class SettingsService {
return !UNSAFE_OBJECT_KEYS.has(key)
}
public trackChanges(): void {
this.settingsVersion()
}
private assignSafeSettings(source: Record<string, any>) {
if (!source || typeof source !== 'object' || Array.isArray(source)) {
return
@@ -375,44 +421,39 @@ export class SettingsService {
}
public initializeDisplayFields() {
this._allDisplayFields = DEFAULT_DISPLAY_FIELDS
const displayFields = DEFAULT_DISPLAY_FIELDS?.map((field) => {
if (
field.id === DisplayField.NOTES &&
!this.get(SETTINGS_KEYS.NOTES_ENABLED)
) {
return null
}
this._allDisplayFields = this._allDisplayFields
?.map((field) => {
if (
field.id === DisplayField.NOTES &&
!this.get(SETTINGS_KEYS.NOTES_ENABLED)
) {
return null
}
if (
[
DisplayField.TITLE,
DisplayField.CREATED,
DisplayField.ADDED,
DisplayField.ASN,
DisplayField.PAGE_COUNT,
DisplayField.SHARED,
].includes(field.id)
) {
return field
}
if (
[
DisplayField.TITLE,
DisplayField.CREATED,
DisplayField.ADDED,
DisplayField.ASN,
DisplayField.PAGE_COUNT,
DisplayField.SHARED,
].includes(field.id)
) {
return field
}
let type: PermissionType = Object.values(PermissionType).find((t) =>
t.includes(field.id)
)
if (field.id === DisplayField.OWNER) {
type = PermissionType.User
}
return this.permissionsService.currentUserCan(PermissionAction.View, type)
? field
: null
}).filter((f) => f)
let type: PermissionType = Object.values(PermissionType).find((t) =>
t.includes(field.id)
)
if (field.id === DisplayField.OWNER) {
type = PermissionType.User
}
return this.permissionsService.currentUserCan(
PermissionAction.View,
type
)
? field
: null
})
.filter((f) => f)
this.allDisplayFieldsSignal.set(displayFields)
if (
this.permissionsService.currentUserCan(
@@ -421,13 +462,15 @@ export class SettingsService {
)
) {
this.customFieldsService.listAll().subscribe((r) => {
this._allDisplayFields = this._allDisplayFields.concat(
r.results.map((field) => {
return {
id: `${DisplayField.CUSTOM_FIELD}${field.id}` as any,
name: field.name,
}
})
this.allDisplayFieldsSignal.set(
displayFields.concat(
r.results.map((field) => {
return {
id: `${DisplayField.CUSTOM_FIELD}${field.id}` as any,
name: field.name,
}
})
)
)
this.displayFieldsInit.emit(true)
})
@@ -438,8 +481,8 @@ export class SettingsService {
get displayName(): string {
return (
this.currentUser.first_name ??
this.currentUser.username ??
this.currentUser?.first_name ??
this.currentUser?.username ??
''
).trim()
}
@@ -572,7 +615,7 @@ export class SettingsService {
// special case to fallback
if (key === SETTINGS_KEYS.DEFAULT_PERMS_OWNER && value === undefined) {
return this.currentUser.id
return this.currentUser?.id
}
if (value !== undefined) {
@@ -606,6 +649,7 @@ export class SettingsService {
if (index == keys.length - 1) settingObj[keyPart] = value
else settingObj = settingObj[keyPart]
})
this.settingsVersion.update((version) => version + 1)
}
private settingIsSet(key: string): boolean {
+10 -2
View File
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { Injectable, inject, signal } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { first, map, takeUntil, tap } from 'rxjs/operators'
import {
@@ -23,10 +23,18 @@ export class TasksService {
public loading: boolean = false
private fileTasks: PaperlessTask[] = []
private fileTasksSignal = signal<PaperlessTask[]>([])
private unsubscribeNotifer: Subject<any> = new Subject()
private get fileTasks(): PaperlessTask[] {
return this.fileTasksSignal()
}
private set fileTasks(tasks: PaperlessTask[]) {
this.fileTasksSignal.set(tasks)
}
public get total(): number {
return this.fileTasks.length
}
+2 -2
View File
@@ -2,7 +2,7 @@ import {
importProvidersFrom,
inject,
provideAppInitializer,
provideZoneChangeDetection,
provideZonelessChangeDetection,
} from '@angular/core'
import { DragDropModule } from '@angular/cdk/drag-drop'
@@ -372,7 +372,7 @@ const icons = {
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection(),
provideZonelessChangeDetection(),
importProvidersFrom(
BrowserModule,
AppRoutingModule,
-5
View File
@@ -46,11 +46,6 @@ import '@angular/localize/init'
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js' // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
+3
View File
@@ -10,6 +10,8 @@
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"ignoreDeprecations": "6.0",
"strict": false,
"target": "ES2022",
"module": "es2020",
"lib": [
@@ -20,5 +22,6 @@
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictTemplates": false
}
}
Generated
+53 -49
View File
@@ -783,16 +783,16 @@ wheels = [
[[package]]
name = "daphne"
version = "4.2.2"
version = "4.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "autobahn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "twisted", extra = ["tls"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/d3/65ff32c01cc64d44441b038dbb7cfb0c6a5507a1c937b3d41bd99af7bdc4/daphne-4.2.2.tar.gz", hash = "sha256:6c3527d4ce32630ae054dfb0ef5578e9a35d2f39f0ebcd02ef4f9129a121ce8d", size = 47601, upload-time = "2026-06-03T10:53:13.31Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/9d/322b605fdc03b963cf2d33943321c8f4405e8d82e698bf49d1eed1ca40c4/daphne-4.2.1.tar.gz", hash = "sha256:5f898e700a1fda7addf1541d7c328606415e96a7bd768405f0463c312fcb31b3", size = 45600, upload-time = "2025-07-02T12:57:04.935Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/ab/85534d9cbca09f3c10f58fc2093659062280ed5703707c0b41dbca8ec297/daphne-4.2.2-py3-none-any.whl", hash = "sha256:466ba8a7c31c5b758953095b451dbad9dc23e5783c68a3716e5fc7aa5f26d168", size = 29466, upload-time = "2026-06-03T10:53:11.841Z" },
{ url = "https://files.pythonhosted.org/packages/01/34/6171ab34715ed210bcd6c2b38839cc792993cff4fe2493f50bc92b0086a0/daphne-4.2.1-py3-none-any.whl", hash = "sha256:881e96b387b95b35ad85acd855f229d7f5b79073d6649089c8a33f661885e055", size = 29015, upload-time = "2025-07-02T12:57:03.793Z" },
]
[[package]]
@@ -810,7 +810,7 @@ wheels = [
[[package]]
name = "dateparser"
version = "1.4.1"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -818,9 +818,9 @@ dependencies = [
{ name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" }
sdist = { url = "https://files.pythonhosted.org/packages/46/2d/a0ccdb78788064fa0dc901b8524e50615c42be1d78b78d646d0b28d09180/dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4", size = 321512, upload-time = "2026-03-26T09:56:10.292Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" },
{ url = "https://files.pythonhosted.org/packages/b4/0b/3c3bb7cbe757279e693a0be6049048012f794d01f81099609ecd53b899f0/dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378", size = 300379, upload-time = "2026-03-26T09:56:08.409Z" },
]
[[package]]
@@ -1013,15 +1013,15 @@ wheels = [
[[package]]
name = "django-guardian"
version = "3.3.2"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/89/0b0b268b158cd9155c1b49ebd1429a186228af359c84ab6fd74ecea572e4/django_guardian-3.3.2.tar.gz", hash = "sha256:f8edcf1576eb15d1593e23955693723ef351a7340667f9159e7bec786559c1ab", size = 110499, upload-time = "2026-06-08T12:02:39.643Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/9a/e332f886bc38dd8bb7b0cdf7305d8dcf6acc376459b87b1ce73697666d55/django_guardian-3.3.1.tar.gz", hash = "sha256:7a50eada72161e355a9dbf9fe0838855c0ec851dd9b39f4f16e2decb5f4c7988", size = 109557, upload-time = "2026-03-30T09:44:59.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/80/5d3793ee277968dcc0c7e8a7752c9dc51d0fec632d9ab12229fa89172846/django_guardian-3.3.2-py3-none-any.whl", hash = "sha256:d5245d6be77a6c632f5b0c739221a44e6469dfa4925c75653ea49cec388e6765", size = 146962, upload-time = "2026-06-08T12:02:38.21Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4f/f798f8a2cb2f84def1fd2bf38f497c45a2ebd093f6bdb1f349eb52faa466/django_guardian-3.3.1-py3-none-any.whl", hash = "sha256:2a3a4b94cdc7e8fc99b74eef163db69d17703dc397c3e9085c7231f2b2bcf431", size = 146125, upload-time = "2026-03-30T09:44:57.599Z" },
]
[[package]]
@@ -1230,11 +1230,11 @@ wheels = [
[[package]]
name = "filelock"
version = "3.29.4"
version = "3.29.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" },
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
]
[[package]]
@@ -2128,7 +2128,7 @@ wheels = [
[[package]]
name = "llama-index-core"
version = "0.14.23"
version = "0.14.22"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -2160,9 +2160,9 @@ dependencies = [
{ name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/ac/f885ae14317af43a026c909ea4d2083fcee2f0d014f90426b5b9aa1f9912/llama_index_core-0.14.23.tar.gz", hash = "sha256:c4baf2f2ab4f84e95090fe7941e0c87d6c514304f7bd2a749b8fa22164c1822b", size = 11588373, upload-time = "2026-06-24T19:35:55.43Z" }
sdist = { url = "https://files.pythonhosted.org/packages/96/7f/94a4b940ef0d069840df0fd6d361a2aa832a2dd73b4cecdf86e8f8c353c8/llama_index_core-0.14.22.tar.gz", hash = "sha256:1384410f89bdbd32349aab444ef4f5c828c338787bc65bd1ffd8e86dfb44ac41", size = 11584786, upload-time = "2026-05-14T20:21:37.271Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/d5/05d61f34c01c6578fb758d0a3ddef58d36c6ffa9a9f84a5c9a16262ad94d/llama_index_core-0.14.23-py3-none-any.whl", hash = "sha256:6a54d267826732a8507f81df40785b107f7592af20f451a39a59005147caf84c", size = 11924908, upload-time = "2026-06-24T19:35:52.833Z" },
{ url = "https://files.pythonhosted.org/packages/39/15/e1a26d8d56aa55fa07587a3e9c7e85294d2df5af6c2229193019bc549ef6/llama_index_core-0.14.22-py3-none-any.whl", hash = "sha256:9cfffde46fd5b7937101e1c0c9bb5c21bd7ff8c8a56937810b87ba3542f31225", size = 11920774, upload-time = "2026-05-14T20:21:40.409Z" },
]
[[package]]
@@ -2923,8 +2923,8 @@ dependencies = [
{ name = "sqlite-vec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tantivy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "torch", version = "2.12.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.12.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "zxing-cpp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3020,7 +3020,7 @@ requires-dist = [
{ name = "channels", specifier = "~=4.2" },
{ name = "channels-redis", specifier = "~=4.2" },
{ name = "concurrent-log-handler", specifier = "~=0.9.25" },
{ name = "dateparser", specifier = "~=1.4" },
{ name = "dateparser", specifier = "~=1.2" },
{ name = "django", specifier = "~=5.2.13" },
{ name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.16.0" },
{ name = "django-auditlog", specifier = "~=3.4.1" },
@@ -3029,7 +3029,7 @@ requires-dist = [
{ name = "django-cors-headers", specifier = "~=4.9.0" },
{ name = "django-extensions", specifier = "~=4.1" },
{ name = "django-filter", specifier = "~=25.1" },
{ name = "django-guardian", specifier = "~=3.3.2" },
{ name = "django-guardian", specifier = "~=3.3.0" },
{ name = "django-multiselectfield", specifier = "~=1.0.1" },
{ name = "django-rich", specifier = "~=2.2.0" },
{ name = "django-soft-delete", specifier = "~=1.0.18" },
@@ -3039,7 +3039,7 @@ requires-dist = [
{ name = "drf-spectacular", specifier = "~=0.28" },
{ name = "drf-spectacular-sidecar", specifier = "~=2026.5.1" },
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
{ name = "filelock", specifier = "~=3.29.4" },
{ name = "filelock", specifier = "~=3.29.0" },
{ name = "flower", specifier = "~=2.0.1" },
{ name = "gotenberg-client", specifier = "~=0.14.0" },
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.7.0" },
@@ -3048,7 +3048,7 @@ requires-dist = [
{ name = "imap-tools", specifier = "~=1.13.0" },
{ name = "jinja2", specifier = "~=3.1.5" },
{ name = "langdetect", specifier = "~=1.0.9" },
{ name = "llama-index-core", specifier = ">=0.14.23" },
{ name = "llama-index-core", specifier = ">=0.14.22" },
{ name = "llama-index-embeddings-huggingface", specifier = ">=0.6.1" },
{ name = "llama-index-embeddings-ollama", specifier = ">=0.9" },
{ name = "llama-index-embeddings-openai-like", specifier = ">=0.2.2" },
@@ -3079,7 +3079,7 @@ requires-dist = [
{ name = "sqlite-vec", specifier = "==0.1.9" },
{ name = "tantivy", specifier = "~=0.26.0" },
{ name = "tika-client", specifier = "~=0.11.0" },
{ name = "torch", specifier = "~=2.12.1", index = "https://download.pytorch.org/whl/cpu" },
{ name = "torch", specifier = "~=2.12.0", index = "https://download.pytorch.org/whl/cpu" },
{ name = "watchfiles", specifier = ">=1.1.1" },
{ name = "whitenoise", specifier = "~=6.11" },
{ name = "zxing-cpp", specifier = "~=3.0.0" },
@@ -4509,8 +4509,8 @@ dependencies = [
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "scipy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "torch", version = "2.12.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.12.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
{ name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -4952,7 +4952,7 @@ wheels = [
[[package]]
name = "torch"
version = "2.12.1"
version = "2.12.0"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform == 'darwin'",
@@ -4969,16 +4969,17 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" },
]
wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ef81f503912effea2ce3d9b12a2e3a6ed488943e91271c90c7a829f60baf6aa2", upload-time = "2026-06-17T15:43:57Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d2dd0f2c5f7ccbddaf34cade0deaf476808368f902b9cdb7f36a2ab42301bc0e", upload-time = "2026-06-17T15:44:03Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c75e93173c700bccd6bfcc4a9d19ce242ab6dacd1f1781483027a16239b9e650", upload-time = "2026-06-17T15:44:09Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e9b6f7d2dd66ea87a3ae620069d31335d594c06effb1a383bdd21cfe61e44ece", upload-time = "2026-06-17T15:44:16Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2afbb2bdaa8a95040e733f05492ddf133c3967c9b7ce0abd218d704b6cab437d", upload-time = "2026-06-17T15:44:24Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:10802fd383bbfed646212e765a72c37d2185205d4f26eb197a254e8ac7ddcb25", upload-time = "2026-05-12T16:20:07Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", upload-time = "2026-05-12T16:20:12Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", upload-time = "2026-05-12T16:20:17Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", upload-time = "2026-05-12T16:20:21Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", upload-time = "2026-05-12T16:20:26Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", upload-time = "2026-05-12T16:20:31Z" },
]
[[package]]
name = "torch"
version = "2.12.1+cpu"
version = "2.12.0+cpu"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
@@ -4997,21 +4998,24 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'linux'" },
]
wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:c6cf299bad55b045abdc0969f298385845c25bc8ace587215e5538d6a73e7712", upload-time = "2026-06-18T01:56:56Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6d1b61e53a2c000e1e5cc49fc88aebc665bdf02c63910c243116d395d7cbc164", upload-time = "2026-06-18T01:57:02Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:04cd8b002c03dd6a246fbb4ae5abf1edd42adf0a9929ad82162c973e5737b5ac", upload-time = "2026-06-18T01:57:09Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:900b253fc8c739bebffb63d7f75abff4cd53d79947265a00c16cb53b68ecdb91", upload-time = "2026-06-18T01:57:23Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d1620bc7bcf8087f3e48821b5db994a03e32ddb083d58c000b8e032f8a6e2d15", upload-time = "2026-06-18T01:57:29Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ae4bb28409f5370852bd71af221066236c38d647f780d9b0a7240c330a9c12df", upload-time = "2026-06-18T01:57:35Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:4ca206a35f2aeba7da944a69ef857103358e8d4bc6b06b49b9e554dcfa57777d", upload-time = "2026-06-18T01:57:47Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8d47e0cfc59679d4e367646c3df4cf433c7d1595b31307e0e7b2391c58ca2160", upload-time = "2026-06-18T01:57:52Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:77d049968c7817456dbdebf0aadcac0813e302218b0e857a8723463331339d55", upload-time = "2026-06-18T01:57:59Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:ef228ec70f5e73762c213f145c0fdb672e5770a6de760801c58b933b0d5b6957", upload-time = "2026-06-18T01:58:09Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:1b9ad70d0a300d6bd883fffc153a6c0f9bc64bf4190519aeeed0373807bffbcc", upload-time = "2026-06-18T01:58:15Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:618120d79360688595e61162147d37e1d69fc3f2e13d60584a1eb6a74c74735a", upload-time = "2026-06-18T01:58:22Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:0706eb7b53c4b1381b2acc418f6d89d6ed18a308677125ced7ca30519c57dc42", upload-time = "2026-06-18T01:58:33Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:21ed774c10a0fd24ab2cfaabfa98a9260fc28d518946c98c0151b6e447730250", upload-time = "2026-06-18T01:58:39Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.1%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:e3c3b5c275dd57cff1aa34a0ab8f84beab8976c63c8dc1d84f29430eac0590ea", upload-time = "2026-06-18T01:58:46Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:aaa9c1f5e8c518d7be1e3c3e1b090ca7d63b6e353a1abd6cfdaf902405093467", upload-time = "2026-05-12T23:16:01Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4ecd8ecdb9ea1affa5f35d10501809d62dc713f7de9635e8098e760ddbeb852c", upload-time = "2026-05-12T23:16:08Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d85bdbc271bf22ef1931375a81b0366ab11081509728c58df730cf194a090818", upload-time = "2026-05-12T23:16:15Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:b9d0e8eed0af9321ffb12b75f4aca371b071254f12cf75875d5a8e7cc8f52b51", upload-time = "2026-05-12T23:16:33Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ce2ddb880b0813fcc91a737f08fdd973a8115a74c64ccb34e9c09a7964b4d448", upload-time = "2026-05-12T23:16:40Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5e3dc83725581fa38b7b2e45c58692e30b2a3cde19191af54b675ffcac3840a6", upload-time = "2026-05-12T23:16:48Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:5e0da19e1c3bfdc9b92638c552579eac678354485d61fc8921b0461fd6c40449", upload-time = "2026-05-12T23:17:05Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:68b7ddd4db4603a03e106e74c7098c8d8c8943d33c1e5ada009ca4cd885759c3", upload-time = "2026-05-12T23:17:12Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ada78018bdfa30d1c766596cd32d910dbf5b03424cd859231b6d2a00533de922", upload-time = "2026-05-12T23:17:20Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:32b9b7a0974cd6149cb98def0a28a49d92d7c14a384273d5539da9624239e950", upload-time = "2026-05-12T23:17:36Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:93ed8dc52c113580daf6124982b3232629045dccc5cd83a8f5ed478f7bac7340", upload-time = "2026-05-12T23:17:43Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1c86abd4ed15a0736cf2663ad69642ae5d1288c99e30346070e6241018a0a9", upload-time = "2026-05-12T23:17:54Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:ee1f329acfd0c2a1ccaa3393bcaf9857ea58759549bb2d67e271a6eab42382b3", upload-time = "2026-05-12T23:18:08Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:797c066367792c92eb97cafba7fd0caa8d7455e6078a4ee880630077378dc372", upload-time = "2026-05-12T23:18:15Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a8f419ce3f25388d36e67153ec63b3a1b17059c49f5a7759a7e91ac4843660d3", upload-time = "2026-05-12T23:18:22Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:d0d2080cb13c94ebc0c884d237e404490743d0f40192c8a180abf3b6b6f334cf", upload-time = "2026-05-12T23:18:35Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7bc15972acad257723775237cdd120024cca844b8bc64701822fa596bcb7e14", upload-time = "2026-05-12T23:18:42Z" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4d79f961250d1763487ecbc90af019a80009f9e87cadc5366b3ec4ba5671fea6", upload-time = "2026-05-12T23:18:50Z" },
]
[[package]]
@@ -5153,11 +5157,11 @@ wheels = [
[[package]]
name = "types-dateparser"
version = "1.4.1.20260617"
version = "1.4.0.20260328"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/1d/647618b84fe99f40ff485c859de1b0db45c95a955704f5952e719f16834c/types_dateparser-1.4.1.20260617.tar.gz", hash = "sha256:aaf1958a88e2bca849ff36b4696476c829c7b0ad06ff8d473635a8a985468346", size = 17703, upload-time = "2026-06-17T06:56:49.371Z" }
sdist = { url = "https://files.pythonhosted.org/packages/73/4a/2977f29ca9a667353f8c5b833722399b40fc97e2a4409934509145683b61/types_dateparser-1.4.0.20260328.tar.gz", hash = "sha256:c6beef9fec92e8a253605d8c841d56de80cee39663d8c70ececfa50b617e9880", size = 17529, upload-time = "2026-03-28T04:07:58.295Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/1f/75b08cc011dc70cfa9b31715dd56b0603d42dfd98f45c70131f7d2fc2da5/types_dateparser-1.4.1.20260617-py3-none-any.whl", hash = "sha256:af5619f005b56880898c54a9cb25bbc1c145751710d456be4f94c50751316ddb", size = 24889, upload-time = "2026-06-17T06:56:48.315Z" },
{ url = "https://files.pythonhosted.org/packages/33/ff/4150ae5e72634007d4ac905e1caff6a0e542b25ac398da8ad76e8f31fd53/types_dateparser-1.4.0.20260328-py3-none-any.whl", hash = "sha256:f8a6f195282dddd0897a33d04e02ff418b3d13c5fec57da54b9de4363788945a", size = 24875, upload-time = "2026-03-28T04:07:57.369Z" },
]
[[package]]