mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-16 15:53:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27cc2497b8 | ||
|
|
d88b281eb2 | ||
|
|
8446036777 | ||
|
|
8ad5e8cca3 | ||
|
|
1606a46b53 | ||
|
|
f647f304da | ||
|
|
31746371f4 | ||
|
|
0e5fbc973a | ||
|
|
3322c92837 | ||
|
|
db15c82804 | ||
|
|
fe5d09a123 | ||
|
|
a0feb827c9 | ||
|
|
b599b13f72 | ||
|
|
01c12d9ea4 | ||
|
|
f5c0d118f7 | ||
|
|
ff13847d0a | ||
|
|
634f803872 | ||
|
|
639d566a7c | ||
|
|
0a94f8f0d4 | ||
|
|
2a8579f610 | ||
|
|
4789fe9a52 | ||
|
|
e150c8c7c0 | ||
|
|
879cd4a30a | ||
|
|
6a02b87dde | ||
|
|
59a2651804 | ||
|
|
a99f63e059 | ||
|
|
939cb52f6e | ||
|
|
855669ddf9 | ||
|
|
62089df2d8 | ||
|
|
5e5f6a88a3 | ||
|
|
02e6c49c62 | ||
|
|
3be64da4cb | ||
|
|
c28c532bef | ||
|
|
1d61f7fc62 | ||
|
|
aa67fd3aef | ||
|
|
17dc482872 | ||
|
|
b0e0e8a353 | ||
|
|
fc242bb570 | ||
|
|
b192a419fd | ||
|
|
3986150f95 | ||
|
|
ee5588ade3 | ||
|
|
2635a12281 | ||
|
|
1f396e51f3 | ||
|
|
3eb784b34d | ||
|
|
cb03a0b33e | ||
|
|
a96d0b15b8 | ||
|
|
376b61938f | ||
|
|
d1f5eb0335 | ||
|
|
d283205f57 | ||
|
|
42908ad2b9 | ||
|
|
db6842e710 | ||
|
|
d5f8cd59fb | ||
|
|
91d6741b4f | ||
|
|
972758fc72 | ||
|
|
15d9829b6d | ||
|
|
5ad34dfe03 | ||
|
|
52a0484f74 | ||
|
|
71e2f86f70 | ||
|
|
1824e5fafd | ||
|
|
7b9e56ef22 | ||
|
|
2b9bed749d | ||
|
|
ef49414162 | ||
|
|
a488ba6f90 | ||
|
|
47727de116 | ||
|
|
e86adb6c36 | ||
|
|
74aa10f31f | ||
|
|
2ea4a792fe | ||
|
|
3f5c770af4 | ||
|
|
765313926f | ||
|
|
1f1725ba56 | ||
|
|
c6cecd3c4e | ||
|
|
5e54259db9 | ||
|
|
3a484a3ff2 | ||
|
|
92a1b24583 | ||
|
|
8709dd807b | ||
|
|
5a5a0ffd21 | ||
|
|
d7aa0cb862 | ||
|
|
eafb494066 | ||
|
|
4ca1863dee | ||
|
|
d5d22c2c0f | ||
|
|
991d99bb7c | ||
|
|
30f11814ff | ||
|
|
0ca5c66ca7 | ||
|
|
dd99c2289d | ||
|
|
23c11f49d0 | ||
|
|
cfa1d3b058 | ||
|
|
68bd8f8f63 | ||
|
|
41d5d86637 | ||
|
|
2a50425902 | ||
|
|
b67ac5a7d7 | ||
|
|
b48c434266 | ||
|
|
92baa89638 | ||
|
|
4c7b3dd307 | ||
|
|
fe48752a6f | ||
|
|
4692de5595 | ||
|
|
141d74464b | ||
|
|
c05d069e97 | ||
|
|
f5e47a86c3 | ||
|
|
fe196eaf0e | ||
|
|
7fda0e877b | ||
|
|
5b8f4db1ca |
+37
-35
@@ -1,49 +1,51 @@
|
|||||||
categories:
|
categories:
|
||||||
|
- type: 'pre-include'
|
||||||
|
when:
|
||||||
|
- label: 'enhancement'
|
||||||
|
- label: 'bug'
|
||||||
|
- label: 'chore'
|
||||||
|
- label: 'deployment'
|
||||||
|
- label: 'translation'
|
||||||
|
- label: 'dependencies'
|
||||||
|
- label: 'documentation'
|
||||||
|
- label: 'frontend'
|
||||||
|
- label: 'backend'
|
||||||
|
- label: 'ci-cd'
|
||||||
|
- label: 'breaking-change'
|
||||||
|
- label: 'notable'
|
||||||
|
- type: 'pre-exclude'
|
||||||
|
when:
|
||||||
|
- label: 'skip-changelog'
|
||||||
- title: 'Breaking Changes'
|
- title: 'Breaking Changes'
|
||||||
labels:
|
when:
|
||||||
- 'breaking-change'
|
- label: 'breaking-change'
|
||||||
- title: 'Notable Changes'
|
- title: 'Notable Changes'
|
||||||
labels:
|
when:
|
||||||
- 'notable'
|
- label: 'notable'
|
||||||
- title: 'Features / Enhancements'
|
- title: 'Features / Enhancements'
|
||||||
labels:
|
when:
|
||||||
- 'enhancement'
|
- label: 'enhancement'
|
||||||
- title: 'Bug Fixes'
|
- title: 'Bug Fixes'
|
||||||
labels:
|
when:
|
||||||
- 'bug'
|
- label: 'bug'
|
||||||
- title: 'Documentation'
|
- title: 'Documentation'
|
||||||
labels:
|
when:
|
||||||
- 'documentation'
|
- label: 'documentation'
|
||||||
- title: 'Maintenance'
|
- title: 'Maintenance'
|
||||||
labels:
|
when:
|
||||||
- 'chore'
|
- label: 'chore'
|
||||||
- 'deployment'
|
- label: 'deployment'
|
||||||
- 'translation'
|
- label: 'translation'
|
||||||
- 'ci-cd'
|
- label: 'ci-cd'
|
||||||
- title: 'Dependencies'
|
- title: 'Dependencies'
|
||||||
collapse-after: 3
|
collapse-after: 3
|
||||||
labels:
|
when:
|
||||||
- 'dependencies'
|
- label: 'dependencies'
|
||||||
- title: 'All App Changes'
|
- title: 'All App Changes'
|
||||||
labels:
|
|
||||||
- 'frontend'
|
|
||||||
- 'backend'
|
|
||||||
collapse-after: 1
|
collapse-after: 1
|
||||||
include-labels:
|
when:
|
||||||
- 'enhancement'
|
- label: 'frontend'
|
||||||
- 'bug'
|
- label: 'backend'
|
||||||
- 'chore'
|
|
||||||
- 'deployment'
|
|
||||||
- 'translation'
|
|
||||||
- 'dependencies'
|
|
||||||
- 'documentation'
|
|
||||||
- 'frontend'
|
|
||||||
- 'backend'
|
|
||||||
- 'ci-cd'
|
|
||||||
- 'breaking-change'
|
|
||||||
- 'notable'
|
|
||||||
exclude-labels:
|
|
||||||
- 'skip-changelog'
|
|
||||||
filter-by-commitish: true
|
filter-by-commitish: true
|
||||||
category-template: '### $TITLE'
|
category-template: '### $TITLE'
|
||||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }}
|
backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- name: Detect changes
|
- name: Detect changes
|
||||||
id: filter
|
id: filter
|
||||||
if: steps.force.outputs.run_all != 'true'
|
if: steps.force.outputs.run_all != 'true'
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
with:
|
with:
|
||||||
base: ${{ steps.range.outputs.base }}
|
base: ${{ steps.range.outputs.base }}
|
||||||
ref: ${{ steps.range.outputs.ref }}
|
ref: ${{ steps.range.outputs.ref }}
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Start containers
|
- name: Start containers
|
||||||
@@ -96,11 +96,11 @@ jobs:
|
|||||||
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -169,16 +169,16 @@ jobs:
|
|||||||
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
|
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.DEFAULT_PYTHON }}"
|
python-version: "${{ env.DEFAULT_PYTHON }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
ref-name: ${{ steps.ref.outputs.name }}
|
ref-name: ${{ steps.ref.outputs.name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Determine ref name
|
- name: Determine ref name
|
||||||
@@ -106,9 +106,9 @@ jobs:
|
|||||||
echo "repository=${repo_name}"
|
echo "repository=${repo_name}"
|
||||||
echo "name=${repo_name}" >> $GITHUB_OUTPUT
|
echo "name=${repo_name}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
- name: Docker metadata
|
- name: Docker metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}
|
${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -182,29 +182,29 @@ jobs:
|
|||||||
echo "Downloaded digests:"
|
echo "Downloaded digests:"
|
||||||
ls -la /tmp/digests/
|
ls -la /tmp/digests/
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||||
- name: Docker metadata
|
- name: Docker metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }}
|
${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
docs_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.docs == 'true' }}
|
docs_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.docs == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
- name: Detect changes
|
- name: Detect changes
|
||||||
id: filter
|
id: filter
|
||||||
if: steps.force.outputs.run_all != 'true'
|
if: steps.force.outputs.run_all != 'true'
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
with:
|
with:
|
||||||
base: ${{ steps.range.outputs.base }}
|
base: ${{ steps.range.outputs.base }}
|
||||||
ref: ${{ steps.range.outputs.ref }}
|
ref: ${{ steps.range.outputs.ref }}
|
||||||
@@ -69,16 +69,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
frontend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.frontend == 'true' }}
|
frontend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.frontend == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- name: Detect changes
|
- name: Detect changes
|
||||||
id: filter
|
id: filter
|
||||||
if: steps.force.outputs.run_all != 'true'
|
if: steps.force.outputs.run_all != 'true'
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
with:
|
with:
|
||||||
base: ${{ steps.range.outputs.base }}
|
base: ${{ steps.range.outputs.base }}
|
||||||
ref: ${{ steps.range.outputs.ref }}
|
ref: ${{ steps.range.outputs.ref }}
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -129,8 +129,8 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: cd src-ui && pnpm run lint
|
run: cd src-ui && pnpm run lint
|
||||||
unit-tests:
|
unit-tests:
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
shard-count: [4]
|
shard-count: [4]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -168,8 +168,8 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Run Jest unit tests
|
- name: Run Jest unit tests
|
||||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
container: mcr.microsoft.com/playwright:v1.61.1-noble
|
container: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
@@ -203,7 +203,7 @@ jobs:
|
|||||||
shard-count: [2]
|
shard-count: [2]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -223,23 +223,20 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm install --no-frozen-lockfile
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Run Playwright E2E tests
|
- name: Run Playwright E2E tests
|
||||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
bundle-analysis:
|
frontend-build:
|
||||||
name: Bundle Analysis
|
name: Frontend Build
|
||||||
needs: [changes, unit-tests, e2e-tests]
|
needs: [changes, unit-tests, e2e-tests]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
environment: bundle-analysis
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -248,7 +245,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -260,21 +257,19 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Build and analyze
|
- name: Build
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
run: cd src-ui && pnpm run build --configuration=production
|
run: cd src-ui && pnpm run build --configuration=production
|
||||||
gate:
|
gate:
|
||||||
name: Frontend CI Gate
|
name: Frontend CI Gate
|
||||||
needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, bundle-analysis]
|
needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, frontend-build]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim
|
||||||
steps:
|
steps:
|
||||||
- name: Check gate
|
- name: Check gate
|
||||||
env:
|
env:
|
||||||
BUNDLE_ANALYSIS_RESULT: ${{ needs['bundle-analysis'].result }}
|
BUILD_RESULT: ${{ needs['frontend-build'].result }}
|
||||||
E2E_RESULT: ${{ needs['e2e-tests'].result }}
|
E2E_RESULT: ${{ needs['e2e-tests'].result }}
|
||||||
FRONTEND_CHANGED: ${{ needs.changes.outputs.frontend_changed }}
|
FRONTEND_CHANGED: ${{ needs.changes.outputs.frontend_changed }}
|
||||||
INSTALL_RESULT: ${{ needs['install-dependencies'].result }}
|
INSTALL_RESULT: ${{ needs['install-dependencies'].result }}
|
||||||
@@ -306,8 +301,8 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${BUNDLE_ANALYSIS_RESULT}" != "success" ]]; then
|
if [[ "${BUILD_RESULT}" != "success" ]]; then
|
||||||
echo "::error::Frontend bundle-analysis job result: ${BUNDLE_ANALYSIS_RESULT}"
|
echo "::error::Frontend build job result: ${BUILD_RESULT}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
- name: Run prek
|
- name: Run prek
|
||||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
uses: j178/prek-action@5337cb91e0fa35a7ff31b9ca345126d8bbbcdf16 # v2.0.6
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
statuses: read
|
statuses: read
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for Docker build
|
- name: Wait for Docker build
|
||||||
uses: lewagon/wait-on-check-action@96d9100b431964d10e0136aff8b9ccb92470505e # v1.8.0
|
uses: lewagon/wait-on-check-action@2271c86c146b96545b4e871b855e10ffa6f50773 # v1.9.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
check-name: 'Merge and Push Manifest'
|
check-name: 'Merge and Push Manifest'
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# ---- Frontend Build ----
|
# ---- Frontend Build ----
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
@@ -55,11 +55,11 @@ jobs:
|
|||||||
# ---- Backend Setup ----
|
# ---- Backend Setup ----
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Create release and changelog
|
- name: Create release and changelog
|
||||||
id: create-release
|
id: create-release
|
||||||
uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
|
uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
|
||||||
with:
|
with:
|
||||||
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
||||||
tag: ${{ steps.get-version.outputs.version }}
|
tag: ${{ steps.get-version.outputs.version }}
|
||||||
@@ -202,17 +202,17 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
persist-credentials: true # for pushing changelog branch
|
persist-credentials: true # for pushing changelog branch
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||||
semgrep:
|
semgrep:
|
||||||
name: Semgrep CE
|
name: Semgrep CE
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -38,13 +38,13 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run Semgrep
|
- name: Run Semgrep
|
||||||
run: semgrep scan --config auto --sarif-output results.sarif
|
run: semgrep scan --config auto --sarif-output results.sarif
|
||||||
- name: Upload results to GitHub code scanning
|
- name: Upload results to GitHub code scanning
|
||||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ jobs:
|
|||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -47,4 +47,4 @@ jobs:
|
|||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
environment: translation-sync
|
environment: translation-sync
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||||
with:
|
with:
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Label PR by file path or branch name
|
- name: Label PR by file path or branch name
|
||||||
# see .github/labeler.yml for the labeler config
|
# see .github/labeler.yml for the labeler config
|
||||||
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Label by size
|
- name: Label by size
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||||
steps:
|
steps:
|
||||||
- name: Label PR with release-drafter
|
- name: Label PR with release-drafter
|
||||||
uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
|
uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 7
|
days-before-stale: 7
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
env:
|
env:
|
||||||
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
||||||
with:
|
with:
|
||||||
@@ -23,13 +23,13 @@ jobs:
|
|||||||
persist-credentials: true # for pushing translation branch
|
persist-credentials: true # for pushing translation branch
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends gettext
|
sudo apt-get install -qq --no-install-recommends gettext
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -61,16 +61,13 @@ jobs:
|
|||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
run: cd src-ui && pnpm install
|
|
||||||
- name: Re-link Angular cli
|
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
|
||||||
- name: Generate frontend translation strings
|
- name: Generate frontend translation strings
|
||||||
run: |
|
run: |
|
||||||
cd src-ui
|
cd src-ui
|
||||||
pnpm run ng extract-i18n
|
pnpm run ng extract-i18n
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||||
with:
|
with:
|
||||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||||
commit_message: "Auto translate strings"
|
commit_message: "Auto translate strings"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ repos:
|
|||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.4.2
|
rev: v2.4.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
@@ -38,7 +38,7 @@ repos:
|
|||||||
- json
|
- json
|
||||||
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/rbubley/mirrors-prettier
|
||||||
rev: 'v3.9.4'
|
rev: 'v3.9.6'
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or:
|
types_or:
|
||||||
@@ -46,16 +46,16 @@ repos:
|
|||||||
- ts
|
- ts
|
||||||
- markdown
|
- markdown
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- prettier@3.9.4
|
- prettier@3.9.6
|
||||||
- 'prettier-plugin-organize-imports@4.3.0'
|
- 'prettier-plugin-organize-imports@4.3.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.20
|
rev: v0.16.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.25.1"
|
rev: "v2.26.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.11.28-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.11.32-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
greenmail:
|
greenmail:
|
||||||
image: docker.io/greenmail/standalone:2.1.9
|
image: docker.io/greenmail/standalone:2.1.11
|
||||||
hostname: greenmail
|
hostname: greenmail
|
||||||
container_name: greenmail
|
container_name: greenmail
|
||||||
environment:
|
environment:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- "3143:3143" # IMAP
|
- "3143:3143" # IMAP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
nginx:
|
nginx:
|
||||||
image: docker.io/nginx:1.31.2-alpine
|
image: docker.io/nginx:1.31.3-alpine
|
||||||
hostname: nginx
|
hostname: nginx
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ optional arguments:
|
|||||||
-sm, --split-manifest
|
-sm, --split-manifest
|
||||||
-z, --zip
|
-z, --zip
|
||||||
-zn, --zip-name
|
-zn, --zip-name
|
||||||
|
--zip-compression
|
||||||
|
--zip-compression-level
|
||||||
--data-only
|
--data-only
|
||||||
--no-progress-bar
|
--no-progress-bar
|
||||||
--passphrase
|
--passphrase
|
||||||
@@ -361,6 +363,19 @@ If `-z` or `--zip` is provided, the export will be a zip file
|
|||||||
in the target directory, named according to the current local date or the
|
in the target directory, named according to the current local date or the
|
||||||
value set in `-zn` or `--zip-name`.
|
value set in `-zn` or `--zip-name`.
|
||||||
|
|
||||||
|
The compression method for the zip can be set with `--zip-compression`
|
||||||
|
(`stored`, `deflated` (default), `bzip2`, `lzma`, or `zstd`) and tuned with
|
||||||
|
`--zip-compression-level` (deflated: 0–9, bzip2: 1–9, zstd: -22–22; ignored
|
||||||
|
for `stored` and `lzma`). Both options require `--zip`.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
`zstd` compression requires Python 3.14 or newer on **both** the machine
|
||||||
|
creating the export and any machine importing it. An archive compressed with
|
||||||
|
`zstd` (or `lzma`/`bzip2` where those modules are unavailable) cannot be
|
||||||
|
imported on a runtime that lacks the codec; the importer will refuse it with
|
||||||
|
a clear error. The default `deflated` is universally readable.
|
||||||
|
|
||||||
If `--data-only` is provided, only the database will be exported. This option is intended
|
If `--data-only` is provided, only the database will be exported. This option is intended
|
||||||
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
|
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
|
||||||
|
|
||||||
@@ -699,6 +714,7 @@ document_fuzzy_match [--ratio] [--processes N]
|
|||||||
| --ratio | No | 85.0 | a number between 0 and 100, setting how similar a document must be for it to be reported. Higher numbers mean more similarity. |
|
| --ratio | No | 85.0 | a number between 0 and 100, setting how similar a document must be for it to be reported. Higher numbers mean more similarity. |
|
||||||
| --processes | No | 1/4 of system cores | Number of processes to use for matching. Setting 1 disables multiple processes |
|
| --processes | No | 1/4 of system cores | Number of processes to use for matching. Setting 1 disables multiple processes |
|
||||||
| --delete | No | False | If provided, one document of a matched pair above the ratio will be deleted. |
|
| --delete | No | False | If provided, one document of a matched pair above the ratio will be deleted. |
|
||||||
|
| --url | No | blank | If an instance URL is provided, the output table will show URLs to each documents instead of the document ID and name. |
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
|
|||||||
@@ -948,10 +948,11 @@ for display in the web interface.
|
|||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
The **remote OCR parser** (Azure AI) always produces a searchable
|
The **remote OCR parser** (Azure AI) also honors this setting: when
|
||||||
PDF and stores it as the archive copy, regardless of this setting.
|
no archive is requested (`never`, or `auto` with a born-digital PDF),
|
||||||
`ARCHIVE_FILE_GENERATION=never` has no effect when the remote
|
the remote engine is skipped entirely and locally-extracted text is
|
||||||
parser handles a document.
|
used instead, avoiding an unnecessary API call and a duplicate text
|
||||||
|
layer.
|
||||||
|
|
||||||
#### [`PAPERLESS_OCR_CLEAN=<mode>`](#PAPERLESS_OCR_CLEAN) {#PAPERLESS_OCR_CLEAN}
|
#### [`PAPERLESS_OCR_CLEAN=<mode>`](#PAPERLESS_OCR_CLEAN) {#PAPERLESS_OCR_CLEAN}
|
||||||
|
|
||||||
|
|||||||
@@ -187,10 +187,11 @@ PAPERLESS_ARCHIVE_FILE_GENERATION=auto
|
|||||||
|
|
||||||
### Remote OCR parser
|
### Remote OCR parser
|
||||||
|
|
||||||
If you use the **remote OCR parser** (Azure AI), note that it always produces a
|
If you use the **remote OCR parser** (Azure AI), `ARCHIVE_FILE_GENERATION` is
|
||||||
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never`
|
honored the same way as for the local engine: when no archive is requested
|
||||||
has no effect for documents handled by the remote parser - the archive is produced
|
(`never`, or `auto` with a born-digital PDF), the remote engine is skipped
|
||||||
unconditionally by the remote engine.
|
entirely and locally-extracted text is used instead, avoiding an unnecessary
|
||||||
|
API call and a duplicate text layer.
|
||||||
|
|
||||||
## Search Index (Whoosh -> Tantivy)
|
## Search Index (Whoosh -> Tantivy)
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -576,7 +576,9 @@ The following workflow action types are available:
|
|||||||
- Tags, correspondent, document type and storage path
|
- Tags, correspondent, document type and storage path
|
||||||
- Document owner
|
- Document owner
|
||||||
- View and / or edit permissions to users or groups
|
- View and / or edit permissions to users or groups
|
||||||
- Custom fields. Note that no value for the field will be set
|
- Custom fields, optionally with a value. If no value is set, the field is only added to the
|
||||||
|
document and any value it may already have is left untouched. If a value is set, it will
|
||||||
|
overwrite an existing value of that field on the document.
|
||||||
|
|
||||||
##### Removal {#workflow-action-removal}
|
##### Removal {#workflow-action-removal}
|
||||||
|
|
||||||
|
|||||||
+24
-24
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
"channels~=4.2",
|
"channels~=4.2",
|
||||||
"channels-redis~=4.2",
|
"channels-redis~=4.2",
|
||||||
"concurrent-log-handler~=0.9.25",
|
"concurrent-log-handler~=0.9.25",
|
||||||
"dateparser~=1.2",
|
"dateparser~=1.4",
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.13",
|
"django~=5.2.13",
|
||||||
@@ -32,33 +32,32 @@ dependencies = [
|
|||||||
"django-cors-headers~=4.9.0",
|
"django-cors-headers~=4.9.0",
|
||||||
"django-extensions~=4.1",
|
"django-extensions~=4.1",
|
||||||
"django-filter~=25.1",
|
"django-filter~=25.1",
|
||||||
"django-guardian~=3.3.0",
|
"django-guardian~=3.3.3",
|
||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=1.0.1",
|
||||||
"django-rich~=2.2.0",
|
"django-rich~=2.2.0",
|
||||||
"django-soft-delete~=1.0.18",
|
"django-soft-delete~=1.0.18",
|
||||||
"django-treenode>=0.24",
|
"django-treenode>=0.24",
|
||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"djangorestframework-guardian~=0.4.0",
|
"drf-spectacular~=0.30",
|
||||||
"drf-spectacular~=0.28",
|
"drf-spectacular-sidecar~=2026.7.1",
|
||||||
"drf-spectacular-sidecar~=2026.5.1",
|
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"filelock~=3.29.0",
|
"filelock~=3.32.0",
|
||||||
"flower~=2.0.1",
|
"flower~=2.0.1",
|
||||||
"gotenberg-client~=0.14.0",
|
"gotenberg-client~=0.14.0",
|
||||||
"httpx-oauth~=0.16",
|
"httpx-oauth~=0.17",
|
||||||
"ijson>=3.2",
|
"ijson>=3.5.1",
|
||||||
"imap-tools~=1.13.0",
|
"imap-tools~=1.14.0",
|
||||||
"jinja2~=3.1.5",
|
"jinja2~=3.1.5",
|
||||||
"langdetect~=1.0.9",
|
"langdetect~=1.0.9",
|
||||||
"llama-index-core>=0.14.22",
|
"llama-index-core>=0.14.23",
|
||||||
"llama-index-embeddings-huggingface>=0.6.1",
|
"llama-index-embeddings-huggingface>=0.6.1",
|
||||||
"llama-index-embeddings-ollama>=0.9",
|
"llama-index-embeddings-ollama>=0.9",
|
||||||
"llama-index-embeddings-openai-like>=0.2.2",
|
"llama-index-embeddings-openai-like>=0.2.2",
|
||||||
"llama-index-llms-ollama>=0.9.1",
|
"llama-index-llms-ollama>=0.9.1",
|
||||||
"llama-index-llms-openai-like>=0.7.1",
|
"llama-index-llms-openai-like>=0.7.1",
|
||||||
"nltk~=3.10.0",
|
"nltk~=3.10.0",
|
||||||
"ocrmypdf~=17.4.2",
|
"ocrmypdf~=17.7.0",
|
||||||
"openai>=2.32",
|
"openai>=2.48",
|
||||||
"pathvalidate~=3.3.1",
|
"pathvalidate~=3.3.1",
|
||||||
"pdf2image~=1.17.0",
|
"pdf2image~=1.17.0",
|
||||||
"python-dateutil~=2.9.0",
|
"python-dateutil~=2.9.0",
|
||||||
@@ -68,17 +67,17 @@ dependencies = [
|
|||||||
"python-magic~=0.4.27",
|
"python-magic~=0.4.27",
|
||||||
"rapidfuzz~=3.14.5",
|
"rapidfuzz~=3.14.5",
|
||||||
"redis[hiredis]~=5.2.1",
|
"redis[hiredis]~=5.2.1",
|
||||||
"regex>=2026.4.4",
|
"regex>=2026.7.19",
|
||||||
"scikit-learn~=1.8.0",
|
"scikit-learn~=1.9.0",
|
||||||
"sentence-transformers>=5.4.1",
|
"sentence-transformers>=5.6.1",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"sqlite-vec==0.1.9",
|
"sqlite-vec==0.1.9",
|
||||||
"tantivy~=0.26.0",
|
"tantivy~=0.26.0",
|
||||||
"tika-client~=0.11.0",
|
"tika-client~=0.11.0",
|
||||||
"torch~=2.13.0",
|
"torch~=2.13.0",
|
||||||
"watchfiles>=1.1.1",
|
"watchfiles>=1.2",
|
||||||
"whitenoise~=6.11",
|
"whitenoise~=6.11",
|
||||||
"zxing-cpp~=3.0.0",
|
"zxing-cpp~=3.1.0",
|
||||||
]
|
]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
mariadb = [
|
mariadb = [
|
||||||
@@ -101,25 +100,25 @@ dev = [
|
|||||||
{ include-group = "testing" },
|
{ include-group = "testing" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"zensical>=0.0.47",
|
"zensical>=0.0.51",
|
||||||
]
|
]
|
||||||
lint = [
|
lint = [
|
||||||
"prek~=0.3.10",
|
"prek~=0.4.11",
|
||||||
"ruff~=0.15.20",
|
"ruff~=0.16.1",
|
||||||
]
|
]
|
||||||
testing = [
|
testing = [
|
||||||
"daphne",
|
"daphne",
|
||||||
"factory-boy~=3.3.1",
|
"factory-boy~=3.3.1",
|
||||||
"faker~=40.15.0",
|
"faker~=40.36.0",
|
||||||
"imagehash",
|
"imagehash",
|
||||||
"pytest~=9.0.3",
|
"pytest~=9.1.1",
|
||||||
"pytest-cov~=7.1.0",
|
"pytest-cov~=7.1.0",
|
||||||
"pytest-django~=4.12.0",
|
"pytest-django~=4.12.0",
|
||||||
"pytest-env~=1.6.0",
|
"pytest-env~=1.7.0",
|
||||||
"pytest-httpx",
|
"pytest-httpx",
|
||||||
"pytest-mock~=3.15.1",
|
"pytest-mock~=3.15.1",
|
||||||
# "pytest-randomly~=4.0.1",
|
# "pytest-randomly~=4.0.1",
|
||||||
"pytest-rerunfailures~=16.1",
|
"pytest-rerunfailures~=16.4",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist~=3.8.0",
|
"pytest-xdist~=3.8.0",
|
||||||
"time-machine>=2.13",
|
"time-machine>=2.13",
|
||||||
@@ -183,6 +182,7 @@ output-format = "grouped"
|
|||||||
line-ending = "lf"
|
line-ending = "lf"
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# https://docs.astral.sh/ruff/rules/
|
# https://docs.astral.sh/ruff/rules/
|
||||||
|
select = [ "E4", "E7", "E9", "F" ]
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||||
|
|||||||
+12
-9
@@ -56,13 +56,13 @@
|
|||||||
},
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-builders/custom-webpack:browser",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"customWebpackConfig": {
|
"outputPath": {
|
||||||
"path": "./extra-webpack.config.ts"
|
"base": "dist/paperless-ui",
|
||||||
|
"browser": ""
|
||||||
},
|
},
|
||||||
"outputPath": "dist/paperless-ui",
|
"browser": "src/main.ts",
|
||||||
"main": "src/main.ts",
|
|
||||||
"outputHashing": "none",
|
"outputHashing": "none",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
@@ -97,6 +97,7 @@
|
|||||||
"scripts": [],
|
"scripts": [],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"file-saver",
|
"file-saver",
|
||||||
|
"mime-names",
|
||||||
"utif"
|
"utif"
|
||||||
],
|
],
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
@@ -117,11 +118,13 @@
|
|||||||
"with": "src/environments/environment.prod.ts"
|
"with": "src/environments/environment.prod.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputPath": "../src/documents/static/frontend/",
|
"outputPath": {
|
||||||
|
"base": "../src/documents/static/frontend/",
|
||||||
|
"browser": ""
|
||||||
|
},
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "none",
|
"outputHashing": "none",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
@@ -145,7 +148,7 @@
|
|||||||
"defaultConfiguration": ""
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "paperless-ui:build:en-US"
|
"buildTarget": "paperless-ui:build:en-US"
|
||||||
},
|
},
|
||||||
@@ -156,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-builders/custom-webpack:extract-i18n",
|
"builder": "@angular/build:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "paperless-ui:build"
|
"buildTarget": "paperless-ui:build"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import {
|
|
||||||
CustomWebpackBrowserSchema,
|
|
||||||
TargetOptions,
|
|
||||||
} from '@angular-builders/custom-webpack'
|
|
||||||
import * as webpack from 'webpack'
|
|
||||||
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
|
|
||||||
|
|
||||||
export default (
|
|
||||||
config: webpack.Configuration,
|
|
||||||
options: CustomWebpackBrowserSchema,
|
|
||||||
targetOptions: TargetOptions
|
|
||||||
) => {
|
|
||||||
if (config.plugins) {
|
|
||||||
config.plugins.push(
|
|
||||||
codecovWebpackPlugin({
|
|
||||||
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
|
|
||||||
bundleName: 'paperless-ngx',
|
|
||||||
uploadToken: process.env.CODECOV_TOKEN,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
+1343
-896
File diff suppressed because it is too large
Load Diff
+28
-31
@@ -11,16 +11,16 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.0.3",
|
"@angular/cdk": "^22.0.6",
|
||||||
"@angular/common": "~22.0.5",
|
"@angular/common": "~22.1.0",
|
||||||
"@angular/compiler": "~22.0.5",
|
"@angular/compiler": "~22.1.0",
|
||||||
"@angular/core": "~22.0.5",
|
"@angular/core": "~22.1.0",
|
||||||
"@angular/forms": "~22.0.5",
|
"@angular/forms": "~22.1.0",
|
||||||
"@angular/localize": "~22.0.5",
|
"@angular/localize": "~22.1.0",
|
||||||
"@angular/platform-browser": "~22.0.5",
|
"@angular/platform-browser": "~22.1.0",
|
||||||
"@angular/router": "~22.0.5",
|
"@angular/router": "~22.1.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
||||||
"@ng-select/ng-select": "^23.2.0",
|
"@ng-select/ng-select": "^23.5.0",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
@@ -32,33 +32,31 @@
|
|||||||
"ngx-device-detector": "^12.0.0",
|
"ngx-device-detector": "^12.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
|
||||||
"normalize-diacritics": "^5.0.0",
|
"normalize-diacritics": "^5.0.0",
|
||||||
"pdfjs-dist": "^6.0.227",
|
"pdfjs-dist": "^6.2.108",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"uuid": "^14.0.1"
|
"uuid": "^14.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^22.0.1",
|
|
||||||
"@angular-builders/jest": "^22.0.1",
|
"@angular-builders/jest": "^22.0.1",
|
||||||
"@angular-devkit/core": "^22.0.5",
|
"@angular-devkit/core": "^22.1.2",
|
||||||
"@angular-devkit/schematics": "^22.0.5",
|
"@angular-devkit/schematics": "^22.1.2",
|
||||||
"@angular-eslint/builder": "22.0.0",
|
"@angular-eslint/builder": "22.1.0",
|
||||||
"@angular-eslint/eslint-plugin": "22.0.0",
|
"@angular-eslint/eslint-plugin": "22.1.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "22.0.0",
|
"@angular-eslint/eslint-plugin-template": "22.1.0",
|
||||||
"@angular-eslint/schematics": "22.0.0",
|
"@angular-eslint/schematics": "22.1.0",
|
||||||
"@angular-eslint/template-parser": "22.0.0",
|
"@angular-eslint/template-parser": "22.1.0",
|
||||||
"@angular/build": "^22.0.5",
|
"@angular/build": "22.1.2",
|
||||||
"@angular/cli": "~22.0.5",
|
"@angular/cli": "22.1.2",
|
||||||
"@angular/compiler-cli": "~22.0.5",
|
"@angular/compiler-cli": "~22.1.0",
|
||||||
"@codecov/webpack-plugin": "^2.0.1",
|
"@playwright/test": "^1.62.0",
|
||||||
"@playwright/test": "^1.61.1",
|
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^26.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||||
"@typescript-eslint/parser": "^8.62.0",
|
"@typescript-eslint/parser": "^8.65.0",
|
||||||
"@typescript-eslint/utils": "^8.62.0",
|
"@typescript-eslint/utils": "^8.65.0",
|
||||||
"eslint": "^10.5.0",
|
"eslint": "^10.8.0",
|
||||||
"jest": "30.4.2",
|
"jest": "30.4.2",
|
||||||
"jest-environment-jsdom": "^30.4.1",
|
"jest-environment-jsdom": "^30.4.1",
|
||||||
"jest-junit": "^17.0.0",
|
"jest-junit": "^17.0.0",
|
||||||
@@ -66,8 +64,7 @@
|
|||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3"
|
||||||
"webpack": "^5.107.2"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.26.0"
|
"packageManager": "pnpm@11.15.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2177
-1981
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ trustPolicy: no-downgrade
|
|||||||
trustPolicyExclude:
|
trustPolicyExclude:
|
||||||
- "chokidar@4.0.3"
|
- "chokidar@4.0.3"
|
||||||
- "semver@6.3.1 || 5.7.2"
|
- "semver@6.3.1 || 5.7.2"
|
||||||
|
blockExoticSubdeps: true
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
"@parcel/watcher": true
|
"@parcel/watcher": true
|
||||||
canvas: true
|
canvas: true
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ describe('TasksComponent', () => {
|
|||||||
|
|
||||||
expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id]))
|
expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id]))
|
||||||
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
||||||
expect(modal.componentInstance.buttonsEnabled).toBe(true)
|
expect(modal.componentInstance.buttonsEnabled()).toBe(true)
|
||||||
expect(component.selectedTasks.size).toBe(0)
|
expect(component.selectedTasks.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -642,7 +642,7 @@ describe('TasksComponent', () => {
|
|||||||
|
|
||||||
expect(dismissSpy).toHaveBeenCalled()
|
expect(dismissSpy).toHaveBeenCalled()
|
||||||
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
||||||
expect(modal.componentInstance.buttonsEnabled).toBe(true)
|
expect(modal.componentInstance.buttonsEnabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should dismiss the currently visible scoped and filtered tasks', () => {
|
it('should dismiss the currently visible scoped and filtered tasks', () => {
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export class TasksComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.tasksService.dismissTasks(tasks).subscribe({
|
this.tasksService.dismissTasks(tasks).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -324,7 +324,7 @@ export class TasksComponent
|
|||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
this.toastService.showError($localize`Error dismissing tasks`, e)
|
this.toastService.showError($localize`Error dismissing tasks`, e)
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
@@ -350,7 +350,7 @@ export class TasksComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.tasksService.dismissAllTasks().subscribe({
|
this.tasksService.dismissAllTasks().subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -358,7 +358,7 @@ export class TasksComponent
|
|||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
this.toastService.showError($localize`Error dismissing tasks`, e)
|
this.toastService.showError($localize`Error dismissing tasks`, e)
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class TrashComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.trashService.emptyTrash([document.id]).subscribe({
|
this.trashService.emptyTrash([document.id]).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.showInfo(
|
this.toastService.showInfo(
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class UsersAndGroupsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.usersService.delete(user).subscribe({
|
this.usersService.delete(user).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
@@ -199,7 +199,7 @@ export class UsersAndGroupsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.groupsService.delete(group).subscribe({
|
this.groupsService.delete(group).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
@@ -104,14 +104,14 @@
|
|||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews()"
|
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews() || !canSaveSettings"
|
||||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||||
(cdkDragEnded)="onDragEnd($event)">
|
(cdkDragEnded)="onDragEnd($event)">
|
||||||
<a class="nav-link" routerLink="view/{{view.id}}"
|
<a class="nav-link" routerLink="view/{{view.id}}"
|
||||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
||||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="funnel"></i-bs><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
|
<i-bs class="me-2" [name]="view.icon || 'funnel'"></i-bs><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
|
||||||
@if (showSidebarCounts && !slimSidebarEnabled) {
|
@if (showSidebarCounts && !slimSidebarEnabled) {
|
||||||
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span>
|
<span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarSavedViews()) {
|
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
||||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
||||||
<i-bs name="grip-vertical"></i-bs>
|
<i-bs name="grip-vertical"></i-bs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
height: 0.8em;
|
height: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-heading,
|
||||||
|
.text-uppercase {
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
.view-name {
|
.view-name {
|
||||||
max-width: calc(100% - 50px)
|
max-width: calc(100% - 50px)
|
||||||
}
|
}
|
||||||
@@ -47,11 +52,12 @@
|
|||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
max-height: 4.5rem;
|
max-height: 4.5rem;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease;
|
transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease;
|
||||||
|
|
||||||
&.mobile-hidden {
|
&.mobile-hidden {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
@@ -80,7 +86,7 @@ main {
|
|||||||
|
|
||||||
.sidebar li.nav-item span,
|
.sidebar li.nav-item span,
|
||||||
.sidebar .sidebar-heading span {
|
.sidebar .sidebar-heading span {
|
||||||
transition: all .1s ease;
|
transition: opacity .1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(min-width: 768px) {
|
@media(min-width: 768px) {
|
||||||
@@ -148,7 +154,7 @@ main {
|
|||||||
z-index: 996;
|
z-index: 996;
|
||||||
--bs-btn-padding-x: 0.35rem;
|
--bs-btn-padding-x: 0.35rem;
|
||||||
--bs-btn-padding-y: 0.125rem;
|
--bs-btn-padding-y: 0.125rem;
|
||||||
transition: all .2s ease;
|
transition: left .2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.slim .sidebar-slim-toggler {
|
.sidebar.slim .sidebar-slim-toggler {
|
||||||
@@ -180,6 +186,8 @@ main {
|
|||||||
.sidebar .nav-link {
|
.sidebar .nav-link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: color .15s ease-in-out;
|
||||||
|
|
||||||
&:hover, &.active, &:focus {
|
&:hover, &.active, &:focus {
|
||||||
color: var(--bs-primary);
|
color: var(--bs-primary);
|
||||||
@@ -192,6 +200,7 @@ main {
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
border-left-color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
i-bs {
|
i-bs {
|
||||||
@@ -200,6 +209,17 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sub-page gets marker only
|
||||||
|
.nav-item:has(.attributes-submenu.show .nav-link.active) > .attributes-row > .nav-link.active {
|
||||||
|
border-left-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bring sub-menu markers back out to L edge
|
||||||
|
.attributes-submenu .nav-link {
|
||||||
|
margin-left: -0.5rem;
|
||||||
|
padding-left: calc(var(--bs-nav-link-padding-x) + 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
.attributes-row .attributes-expand-btn {
|
.attributes-row .attributes-expand-btn {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
transition: opacity 0.15s ease-in-out;
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled()">
|
||||||
<span>
|
<span>
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@if (alternativeBtnCaption) {
|
@if (alternativeBtnCaption) {
|
||||||
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
|
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled()">
|
||||||
{{alternativeBtnCaption}}
|
{{alternativeBtnCaption}}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ describe('ConfirmDialogComponent', () => {
|
|||||||
expect(confirmSubjectResult).toBeTruthy()
|
expect(confirmSubjectResult).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should re-render the buttons when they are toggled from outside', async () => {
|
||||||
|
const confirmButton: HTMLButtonElement =
|
||||||
|
fixture.nativeElement.querySelectorAll('.modal-footer button')[1]
|
||||||
|
expect(confirmButton.disabled).toBeFalsy()
|
||||||
|
|
||||||
|
// Deliberately no detectChanges: a request callback toggling this is all
|
||||||
|
// that happens, and nothing else schedules a render for the modal
|
||||||
|
component.buttonsEnabled.set(false)
|
||||||
|
await fixture.whenStable()
|
||||||
|
expect(confirmButton.disabled).toBeTruthy()
|
||||||
|
|
||||||
|
component.buttonsEnabled.set(true)
|
||||||
|
await fixture.whenStable()
|
||||||
|
expect(confirmButton.disabled).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should support cancel & close modal', () => {
|
it('should support cancel & close modal', () => {
|
||||||
let confirmSubjectResult
|
let confirmSubjectResult
|
||||||
const closeModalSpy = jest.spyOn(modal, 'close')
|
const closeModalSpy = jest.spyOn(modal, 'close')
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { DecimalPipe } from '@angular/common'
|
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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
@@ -46,8 +53,7 @@ export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
|||||||
@Input()
|
@Input()
|
||||||
cancelBtnCaption = $localize`Cancel`
|
cancelBtnCaption = $localize`Cancel`
|
||||||
|
|
||||||
@Input()
|
readonly buttonsEnabled = signal(true)
|
||||||
buttonsEnabled = true
|
|
||||||
|
|
||||||
confirmButtonEnabled = true
|
confirmButtonEnabled = true
|
||||||
alternativeButtonEnabled = true
|
alternativeButtonEnabled = true
|
||||||
|
|||||||
+2
-2
@@ -56,10 +56,10 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled()">
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -57,7 +57,7 @@
|
|||||||
class="btn"
|
class="btn"
|
||||||
[class]="cancelBtnClass"
|
[class]="cancelBtnClass"
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
[disabled]="!buttonsEnabled"
|
[disabled]="!buttonsEnabled()"
|
||||||
>
|
>
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">
|
<span class="d-inline-block" style="padding-bottom: 1px;">
|
||||||
{{cancelBtnCaption}}
|
{{cancelBtnCaption}}
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
class="btn"
|
class="btn"
|
||||||
[class]="btnClass"
|
[class]="btnClass"
|
||||||
(click)="confirm()"
|
(click)="confirm()"
|
||||||
[disabled]="!confirmButtonEnabled || !buttonsEnabled"
|
[disabled]="!confirmButtonEnabled || !buttonsEnabled()"
|
||||||
>
|
>
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+2
-2
@@ -34,10 +34,10 @@
|
|||||||
<p class="mb-0 small"><b>{{messageBold}}</b></p>
|
<p class="mb-0 small"><b>{{messageBold}}</b></p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled || degrees === 0">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled() || degrees === 0">
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
@if (!confirmButtonEnabled) {
|
@if (!confirmButtonEnabled) {
|
||||||
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||||
|
|||||||
+24
@@ -1019,4 +1019,28 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
'pass3',
|
'pass3',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse passwords again when retrying a failed save', () => {
|
||||||
|
component.object = {
|
||||||
|
name: 'Workflow with blank Passwords',
|
||||||
|
id: 1,
|
||||||
|
order: null,
|
||||||
|
enabled: true,
|
||||||
|
triggers: [],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: WorkflowActionType.PasswordRemoval,
|
||||||
|
passwords: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
component.ngOnInit()
|
||||||
|
|
||||||
|
component.save()
|
||||||
|
component.objectForm.get('order').setValue(1)
|
||||||
|
|
||||||
|
expect(() => component.save()).not.toThrow()
|
||||||
|
expect(component.objectForm.get('actions').value[0].passwords).toEqual([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-3
@@ -1207,9 +1207,8 @@ export class WorkflowEditDialogComponent
|
|||||||
return passwords.join('\n')
|
return passwords.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
private parsePasswords(value: string = ''): string[] {
|
private parsePasswords(value: string | string[] = ''): string[] {
|
||||||
return value
|
return (Array.isArray(value) ? value : value.split(/[\n,]+/))
|
||||||
.split(/[\n,]+/)
|
|
||||||
.map((entry) => entry.trim())
|
.map((entry) => entry.trim())
|
||||||
.filter((entry) => entry.length > 0)
|
.filter((entry) => entry.length > 0)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -49,7 +49,7 @@
|
|||||||
</cdk-virtual-scroll-viewport>
|
</cdk-virtual-scroll-viewport>
|
||||||
}
|
}
|
||||||
@if (editing) {
|
@if (editing) {
|
||||||
@if (filteredItems.length === 0 && createRef !== undefined) {
|
@if (filteredItems.length === 0 && createRef !== undefined && filterText?.length > 0) {
|
||||||
<button class="list-group-item list-group-item-action bg-light" (click)="createClicked()" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action bg-light" (click)="createClicked()" [disabled]="disabled">
|
||||||
<small class="ms-2"><ng-container i18n>Create</ng-container> "{{filterText}}"</small>
|
<small class="ms-2"><ng-container i18n>Create</ng-container> "{{filterText}}"</small>
|
||||||
<i-bs width="1.5em" height="1em" name="plus"></i-bs>
|
<i-bs width="1.5em" height="1em" name="plus"></i-bs>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (extraButtonTitle) {
|
@if (extraButtonTitle && (showExtraButtonIfEmpty || filteredItems?.length > 0)) {
|
||||||
<button class="list-group-item list-group-item-action bg-light d-flex align-items-center" (click)="extraButtonClicked($event)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action bg-light d-flex align-items-center" (click)="extraButtonClicked($event)" [disabled]="disabled">
|
||||||
<small class="ms-2 fw-bold">{{extraButtonTitle}}</small>
|
<small class="ms-2 fw-bold">{{extraButtonTitle}}</small>
|
||||||
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
||||||
|
|||||||
+33
@@ -911,6 +911,25 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
expect(createSpy).toHaveBeenCalled()
|
expect(createSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should only show create when a non-empty filter has no matches', () => {
|
||||||
|
component.selectionModel.items = []
|
||||||
|
component.icon = 'tag-fill'
|
||||||
|
component.editing = true
|
||||||
|
component.createRef = jest.fn()
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.textContent).not.toContain('Create')
|
||||||
|
component.listFilterEnter()
|
||||||
|
expect(component.createRef).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const filterInput: HTMLInputElement =
|
||||||
|
fixture.nativeElement.querySelector('input[type="text"]')
|
||||||
|
filterInput.value = 'FooBar'
|
||||||
|
filterInput.dispatchEvent(new Event('input'))
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('Create "FooBar"')
|
||||||
|
})
|
||||||
|
|
||||||
it('should exclude item and trigger change event', () => {
|
it('should exclude item and trigger change event', () => {
|
||||||
const id = 1
|
const id = 1
|
||||||
const state = ToggleableItemState.Selected
|
const state = ToggleableItemState.Selected
|
||||||
@@ -970,4 +989,18 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
expect(extraButtonClicked).toBeTruthy()
|
expect(extraButtonClicked).toBeTruthy()
|
||||||
expect(applied).toBeFalsy()
|
expect(applied).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should only show the extra button for an empty result when enabled', () => {
|
||||||
|
component.selectionModel.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
|
component.extraButtonTitle = 'Extra'
|
||||||
|
component.filterText = 'FooBar'
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.textContent).not.toContain('Extra')
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('showExtraButtonIfEmpty', true)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('Extra')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+8
-1
@@ -774,6 +774,9 @@ export class FilterableDropdownComponent
|
|||||||
@Input()
|
@Input()
|
||||||
extraButtonTitle: string
|
extraButtonTitle: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
showExtraButtonIfEmpty: boolean = false
|
||||||
|
|
||||||
creating: boolean = false
|
creating: boolean = false
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
@@ -892,7 +895,11 @@ export class FilterableDropdownComponent
|
|||||||
this.dropdown.close()
|
this.dropdown.close()
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
} else if (filtered.length == 0 && this.createRef) {
|
} else if (
|
||||||
|
filtered.length == 0 &&
|
||||||
|
this.createRef &&
|
||||||
|
this.filterText?.length > 0
|
||||||
|
) {
|
||||||
this.createClicked()
|
this.createClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -52,10 +52,10 @@ describe('CustomFieldsValuesComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should set selectedFields and map values correctly', () => {
|
it('should set selectedFields and map values correctly', () => {
|
||||||
component.value = { 1: 'value1' }
|
component.value = { 1: 'value1', 3: 0, 4: false }
|
||||||
component.selectedFields = [1, 2]
|
component.selectedFields = [1, 2, 3, 4]
|
||||||
expect(component.selectedFields).toEqual([1, 2])
|
expect(component.selectedFields).toEqual([1, 2, 3, 4])
|
||||||
expect(component.value).toEqual({ 1: 'value1', 2: null })
|
expect(component.value).toEqual({ 1: 'value1', 2: null, 3: 0, 4: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return the correct custom field by id', () => {
|
it('should return the correct custom field by id', () => {
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
|
|||||||
this._selectedFields = newFields
|
this._selectedFields = newFields
|
||||||
// map the selected fields to an object with field_id as key and value as value
|
// map the selected fields to an object with field_id as key and value as value
|
||||||
this.value = newFields.reduce((acc, fieldId) => {
|
this.value = newFields.reduce((acc, fieldId) => {
|
||||||
acc[fieldId] = this.value?.[fieldId] || null
|
acc[fieldId] = this.value?.[fieldId] ?? null
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
this.onChange(this.value)
|
this.onChange(this.value)
|
||||||
|
|||||||
@@ -36,7 +36,16 @@
|
|||||||
(focus)="clearLastSearchTerm()"
|
(focus)="clearLastSearchTerm()"
|
||||||
(clear)="clearLastSearchTerm()"
|
(clear)="clearLastSearchTerm()"
|
||||||
(blur)="onBlur()">
|
(blur)="onBlur()">
|
||||||
|
<ng-template ng-label-tmp let-item="item">
|
||||||
|
@if (iconField && item[iconField]) {
|
||||||
|
<i-bs class="me-2" [name]="item[iconField]"></i-bs>
|
||||||
|
}
|
||||||
|
<span [title]="item[bindLabel]">{{item[bindLabel]}}</span>
|
||||||
|
</ng-template>
|
||||||
<ng-template ng-option-tmp let-item="item">
|
<ng-template ng-option-tmp let-item="item">
|
||||||
|
@if (iconField && item[iconField]) {
|
||||||
|
<i-bs class="me-2" [name]="item[iconField]"></i-bs>
|
||||||
|
}
|
||||||
<span [title]="item[bindLabel]">{{item[bindLabel]}}</span>
|
<span [title]="item[bindLabel]">{{item[bindLabel]}}</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-select>
|
</ng-select>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { AbstractInputComponent } from '../abstract-input'
|
|||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SelectComponent extends AbstractInputComponent<number> {
|
export class SelectComponent extends AbstractInputComponent<number | string> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.addItemRef = this.addItem.bind(this)
|
this.addItemRef = this.addItem.bind(this)
|
||||||
@@ -100,6 +100,9 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
|||||||
@Input()
|
@Input()
|
||||||
bindLabel: string = 'name'
|
bindLabel: string = 'name'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
iconField: string
|
||||||
|
|
||||||
public searchFn = (term: string, item: any): boolean =>
|
public searchFn = (term: string, item: any): boolean =>
|
||||||
matchesSearchText(item?.[this.bindLabel], term)
|
matchesSearchText(item?.[this.bindLabel], term)
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="form-group ms-md-auto">
|
<div class="form-group ms-md-auto">
|
||||||
<button type="button" class="btn me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
|
<button type="button" class="btn me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">{{ cancelBtnCaption }}</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,6 +151,13 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
& section {
|
||||||
|
position: absolute;
|
||||||
|
text-align: initial;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
& .annotationTextContent {
|
& .annotationTextContent {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ describe('PngxPdfViewerComponent', () => {
|
|||||||
}
|
}
|
||||||
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
|
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
|
||||||
expect(viewer.options.textLayerMode).toBe(0)
|
expect(viewer.options.textLayerMode).toBe(0)
|
||||||
|
expect(viewer.options.enableSelectionRendering).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies zoom, rotation, and page changes', async () => {
|
it('applies zoom, rotation, and page changes', async () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
|
AnnotationMode,
|
||||||
getDocument,
|
getDocument,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
PDFDocumentLoadingTask,
|
PDFDocumentLoadingTask,
|
||||||
@@ -221,6 +222,8 @@ export class PngxPdfViewerComponent
|
|||||||
linkService: this.linkService,
|
linkService: this.linkService,
|
||||||
findController: this.findController,
|
findController: this.findController,
|
||||||
textLayerMode,
|
textLayerMode,
|
||||||
|
annotationMode: AnnotationMode.ENABLE,
|
||||||
|
enableSelectionRendering: false,
|
||||||
removePageBorders: true,
|
removePageBorders: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="me-1">
|
<div class="me-1">
|
||||||
<small i18n>My documents</small>
|
<small>{{ownerFilterLabel}}</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="me-1">
|
<div class="me-1">
|
||||||
<small i18n>Shared with me</small>
|
<small>{{ownerExclusionFilterLabel}}</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="me-1">
|
<div class="me-1">
|
||||||
<small i18n>Shared by me</small>
|
<small>{{sharedByFilterLabel}}</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
|
||||||
|
|||||||
+52
@@ -94,6 +94,58 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
expect(component.isActive).toBeTruthy()
|
expect(component.isActive).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should describe concrete user filters honestly', () => {
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.SELF
|
||||||
|
component.selectionModel.userID = 1
|
||||||
|
expect(component.ownerFilterLabel).toEqual('Owned by user1')
|
||||||
|
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
||||||
|
component.selectionModel.excludeUsers = [1]
|
||||||
|
expect(component.ownerExclusionFilterLabel).toEqual('Not owned by user1')
|
||||||
|
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.SHARED_BY_ME
|
||||||
|
component.selectionModel.userID = 1
|
||||||
|
expect(component.sharedByFilterLabel).toEqual('Shared by user1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should describe concrete filters when usernames are unavailable', () => {
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.SELF
|
||||||
|
component.selectionModel.userID = 99
|
||||||
|
expect(component.ownerFilterLabel).toEqual('Owned by another user')
|
||||||
|
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
||||||
|
component.selectionModel.excludeUsers = [99]
|
||||||
|
expect(component.ownerExclusionFilterLabel).toEqual(
|
||||||
|
'Not owned by another user'
|
||||||
|
)
|
||||||
|
|
||||||
|
component.selectionModel.excludeUsers = [98, 99]
|
||||||
|
expect(component.ownerExclusionFilterLabel).toEqual(
|
||||||
|
'Not owned by selected users'
|
||||||
|
)
|
||||||
|
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.SHARED_BY_ME
|
||||||
|
component.selectionModel.userID = 99
|
||||||
|
expect(component.sharedByFilterLabel).toEqual('Shared by another user')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should retain relative labels for filters bound to the current user', () => {
|
||||||
|
component.selectionModel.userID = currentUserID
|
||||||
|
expect(component.ownerFilterLabel).toEqual('My documents')
|
||||||
|
expect(component.sharedByFilterLabel).toEqual('Shared by me')
|
||||||
|
|
||||||
|
component.selectionModel.excludeUsers = [currentUserID]
|
||||||
|
expect(component.ownerExclusionFilterLabel).toEqual('Shared with me')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should retain relative labels for inactive filter choices', () => {
|
||||||
|
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||||
|
|
||||||
|
expect(component.ownerFilterLabel).toEqual('My documents')
|
||||||
|
expect(component.ownerExclusionFilterLabel).toEqual('Shared with me')
|
||||||
|
expect(component.sharedByFilterLabel).toEqual('Shared by me')
|
||||||
|
})
|
||||||
|
|
||||||
it('should support reset', () => {
|
it('should support reset', () => {
|
||||||
component.setFilter(OwnerFilterType.OTHERS)
|
component.setFilter(OwnerFilterType.OTHERS)
|
||||||
expect(component.selectionModel.ownerFilter).not.toEqual(
|
expect(component.selectionModel.ownerFilter).not.toEqual(
|
||||||
|
|||||||
+53
@@ -93,6 +93,55 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ownerFilterLabel(): string {
|
||||||
|
if (
|
||||||
|
this.selectionModel?.ownerFilter !== OwnerFilterType.SELF ||
|
||||||
|
this.selectionModel?.userID === this.settingsService.currentUser()?.id
|
||||||
|
) {
|
||||||
|
return $localize`My documents`
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = this.getUsername(this.selectionModel?.userID)
|
||||||
|
return username
|
||||||
|
? $localize`Owned by ${username}`
|
||||||
|
: $localize`Owned by another user`
|
||||||
|
}
|
||||||
|
|
||||||
|
get ownerExclusionFilterLabel(): string {
|
||||||
|
const excludedUsers = this.selectionModel?.excludeUsers ?? []
|
||||||
|
if (
|
||||||
|
this.selectionModel?.ownerFilter !== OwnerFilterType.NOT_SELF ||
|
||||||
|
(excludedUsers.length === 1 &&
|
||||||
|
excludedUsers[0] === this.settingsService.currentUser()?.id)
|
||||||
|
) {
|
||||||
|
return $localize`Shared with me`
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernames = excludedUsers
|
||||||
|
.map((id) => this.getUsername(id))
|
||||||
|
.filter(Boolean)
|
||||||
|
if (usernames.length === excludedUsers.length && usernames.length > 0) {
|
||||||
|
return $localize`Not owned by ${usernames.join(', ')}`
|
||||||
|
}
|
||||||
|
return excludedUsers.length === 1
|
||||||
|
? $localize`Not owned by another user`
|
||||||
|
: $localize`Not owned by selected users`
|
||||||
|
}
|
||||||
|
|
||||||
|
get sharedByFilterLabel(): string {
|
||||||
|
if (
|
||||||
|
this.selectionModel?.ownerFilter !== OwnerFilterType.SHARED_BY_ME ||
|
||||||
|
this.selectionModel?.userID === this.settingsService.currentUser()?.id
|
||||||
|
) {
|
||||||
|
return $localize`Shared by me`
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = this.getUsername(this.selectionModel?.userID)
|
||||||
|
return username
|
||||||
|
? $localize`Shared by ${username}`
|
||||||
|
: $localize`Shared by another user`
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const userService = inject(UserService)
|
const userService = inject(UserService)
|
||||||
|
|
||||||
@@ -164,4 +213,8 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
}
|
}
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUsername(userID: number): string {
|
||||||
|
return this.users().find((user) => user.id === userID)?.username
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -17,6 +17,10 @@ const permissions = [
|
|||||||
'view_document',
|
'view_document',
|
||||||
'change_document',
|
'change_document',
|
||||||
'delete_document',
|
'delete_document',
|
||||||
|
'add_sharelinkbundle',
|
||||||
|
'view_sharelinkbundle',
|
||||||
|
'change_sharelinkbundle',
|
||||||
|
'delete_sharelinkbundle',
|
||||||
'change_tag',
|
'change_tag',
|
||||||
'view_documenttype',
|
'view_documenttype',
|
||||||
]
|
]
|
||||||
@@ -75,6 +79,7 @@ describe('PermissionsSelectComponent', () => {
|
|||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
component.writeValue(permissions)
|
component.writeValue(permissions)
|
||||||
expect(component.typesWithAllActions).toContain('Document')
|
expect(component.typesWithAllActions).toContain('Document')
|
||||||
|
expect(component.typesWithAllActions).toContain('ShareLinkBundle')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update checkboxes on permissions set', () => {
|
it('should update checkboxes on permissions set', () => {
|
||||||
@@ -85,6 +90,10 @@ describe('PermissionsSelectComponent', () => {
|
|||||||
expect(input1.nativeElement.checked).toBeTruthy()
|
expect(input1.nativeElement.checked).toBeTruthy()
|
||||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||||
expect(input2.nativeElement.checked).toBeTruthy()
|
expect(input2.nativeElement.checked).toBeTruthy()
|
||||||
|
const bundleInput = fixture.debugElement.query(
|
||||||
|
By.css('input#ShareLinkBundle_Add')
|
||||||
|
)
|
||||||
|
expect(bundleInput.nativeElement.checked).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disable checkboxes when permissions are inherited', () => {
|
it('disable checkboxes when permissions are inherited', () => {
|
||||||
|
|||||||
+1
-1
@@ -119,7 +119,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap"
|
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap"
|
||||||
(click)="submit()"
|
(click)="submit()"
|
||||||
[disabled]="loading() || !buttonsEnabled">
|
[disabled]="loading() || !buttonsEnabled()">
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ describe('ShareLinkBundleDialogComponent', () => {
|
|||||||
file_version: FileVersion.Original,
|
file_version: FileVersion.Original,
|
||||||
expiration_days: 3,
|
expiration_days: 3,
|
||||||
})
|
})
|
||||||
expect(component.buttonsEnabled).toBe(false)
|
expect(component.buttonsEnabled()).toBe(false)
|
||||||
expect(confirmSpy).toHaveBeenCalled()
|
expect(confirmSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
component.form.setValue({
|
component.form.setValue({
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
|
|||||||
: FileVersion.Original,
|
: FileVersion.Original,
|
||||||
expiration_days: this.form.value.expirationDays,
|
expiration_days: this.form.value.expirationDays,
|
||||||
}
|
}
|
||||||
this.buttonsEnabled = false
|
this.buttonsEnabled.set(false)
|
||||||
super.confirm()
|
super.confirm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -2,10 +2,16 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())">
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
} @else if (noSuggestions) {
|
||||||
|
<i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs>
|
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs>
|
||||||
}
|
}
|
||||||
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
|
@if (noSuggestions) {
|
||||||
|
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
|
||||||
|
} @else {
|
||||||
|
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
|
||||||
|
}
|
||||||
@if (totalSuggestions > 0) {
|
@if (totalSuggestions > 0) {
|
||||||
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
|
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
|
||||||
}
|
}
|
||||||
@@ -19,7 +25,7 @@
|
|||||||
|
|
||||||
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
||||||
<div class="list-group list-group-flush small pb-0">
|
<div class="list-group list-group-flush small pb-0">
|
||||||
@if (!suggestions()?.suggested_tags && !suggestions()?.suggested_document_types && !suggestions()?.suggested_correspondents) {
|
@if (totalSuggestions === 0) {
|
||||||
<div class="list-group-item text-muted fst-italic">
|
<div class="list-group-item text-muted fst-italic">
|
||||||
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
@@ -30,6 +30,34 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
expect(component.totalSuggestions).toBe(4)
|
expect(component.totalSuggestions).toBe(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show when a completed request returned no suggestions', () => {
|
||||||
|
fixture.componentRef.setInput('suggestions', {
|
||||||
|
correspondents: [],
|
||||||
|
tags: [],
|
||||||
|
document_types: [],
|
||||||
|
storage_paths: [],
|
||||||
|
dates: [],
|
||||||
|
})
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.noSuggestions).toBeTruthy()
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('No suggestions')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not show the empty state before a request or with suggestions', () => {
|
||||||
|
expect(component.noSuggestions).toBeFalsy()
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('suggestions', {
|
||||||
|
correspondents: [],
|
||||||
|
tags: [42],
|
||||||
|
document_types: [],
|
||||||
|
storage_paths: [],
|
||||||
|
dates: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(component.noSuggestions).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => {
|
it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => {
|
||||||
jest.spyOn(component.getSuggestions, 'emit')
|
jest.spyOn(component.getSuggestions, 'emit')
|
||||||
fixture.componentRef.setInput('suggestions', null)
|
fixture.componentRef.setInput('suggestions', null)
|
||||||
@@ -59,5 +87,6 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
component.clickSuggest()
|
component.clickSuggest()
|
||||||
expect(component.dropdown.open).toBeTruthy()
|
expect(component.dropdown.open).toBeTruthy()
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('No novel suggestions')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+17
@@ -61,4 +61,21 @@ export class SuggestionsDropdownComponent {
|
|||||||
this.suggestions()?.suggested_document_types?.length || 0
|
this.suggestions()?.suggested_document_types?.length || 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get noSuggestions(): boolean {
|
||||||
|
const suggestions = this.suggestions()
|
||||||
|
return (
|
||||||
|
suggestions != null &&
|
||||||
|
!suggestions.title &&
|
||||||
|
!suggestions.tags?.length &&
|
||||||
|
!suggestions.suggested_tags?.length &&
|
||||||
|
!suggestions.correspondents?.length &&
|
||||||
|
!suggestions.suggested_correspondents?.length &&
|
||||||
|
!suggestions.document_types?.length &&
|
||||||
|
!suggestions.suggested_document_types?.length &&
|
||||||
|
!suggestions.storage_paths?.length &&
|
||||||
|
!suggestions.suggested_storage_paths?.length &&
|
||||||
|
!suggestions.dates?.length
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
<pngx-widget-frame
|
<pngx-widget-frame
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
|
||||||
[title]="savedView.name"
|
[title]="savedView.name"
|
||||||
|
[titleIcon]="savedView.icon || 'funnel'"
|
||||||
[loading]="false"
|
[loading]="false"
|
||||||
[draggable]="savedView"
|
[draggable]="savedView"
|
||||||
>
|
>
|
||||||
|
|||||||
+6
-1
@@ -8,7 +8,12 @@
|
|||||||
<i-bs name="grip-vertical"></i-bs>
|
<i-bs name="grip-vertical"></i-bs>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<h6 class="card-title mb-0">{{title()}}</h6>
|
<h6 class="card-title mb-0">
|
||||||
|
@if (titleIcon()) {
|
||||||
|
<i-bs class="me-2" [name]="titleIcon()"></i-bs>
|
||||||
|
}
|
||||||
|
{{title()}}
|
||||||
|
</h6>
|
||||||
<ng-content select="[title-badge]"></ng-content>
|
<ng-content select="[title-badge]"></ng-content>
|
||||||
@if (badge() !== null && badge() !== undefined) {
|
@if (badge() !== null && badge() !== undefined) {
|
||||||
<span class="badge bg-info text-dark ms-2">{{badge()}}</span>
|
<span class="badge bg-info text-dark ms-2">{{badge()}}</span>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export class WidgetFrameComponent implements AfterViewInit {
|
|||||||
|
|
||||||
title = input<string>()
|
title = input<string>()
|
||||||
|
|
||||||
|
titleIcon = input<string>()
|
||||||
|
|
||||||
draggable = input<any>()
|
draggable = input<any>()
|
||||||
|
|
||||||
cardless = input(false)
|
cardless = input(false)
|
||||||
|
|||||||
@@ -1564,7 +1564,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
dialog.confirmClicked.next()
|
dialog.confirmClicked.next()
|
||||||
await openModal.result
|
await openModal.result
|
||||||
|
|
||||||
expect(dialog.buttonsEnabled).toBe(false)
|
expect(dialog.buttonsEnabled()).toBe(false)
|
||||||
expect(reloadSpy).toHaveBeenCalled()
|
expect(reloadSpy).toHaveBeenCalled()
|
||||||
expect((component as any).incomingUpdateModal).toBeNull()
|
expect((component as any).incomingUpdateModal).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -1789,7 +1789,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
expect(component.networkActive()).toBe(false)
|
expect(component.networkActive()).toBe(false)
|
||||||
expect(dialog.buttonsEnabled).toBe(true)
|
expect(dialog.buttonsEnabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should refresh the document when removing password in update mode', () => {
|
it('should refresh the document when removing password in update mode', () => {
|
||||||
@@ -2161,8 +2161,14 @@ describe('DocumentDetailComponent', () => {
|
|||||||
it('should support open share links and email modals', () => {
|
it('should support open share links and email modals', () => {
|
||||||
const modalSpy = jest.spyOn(modalService, 'open')
|
const modalSpy = jest.spyOn(modalService, 'open')
|
||||||
initNormally()
|
initNormally()
|
||||||
|
component.selectedVersionId.set(10)
|
||||||
component.openShareLinks()
|
component.openShareLinks()
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
modalSpy.mock.results[0].value as NgbModalRef
|
||||||
|
).componentInstance.documentId()
|
||||||
|
).toBe(10)
|
||||||
component.openEmailDocument()
|
component.openEmailDocument()
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -2191,9 +2197,6 @@ describe('DocumentDetailComponent', () => {
|
|||||||
const appendChildSpy = jest
|
const appendChildSpy = jest
|
||||||
.spyOn(document.body, 'appendChild')
|
.spyOn(document.body, 'appendChild')
|
||||||
.mockImplementation((node: Node) => node)
|
.mockImplementation((node: Node) => node)
|
||||||
const removeChildSpy = jest
|
|
||||||
.spyOn(document.body, 'removeChild')
|
|
||||||
.mockImplementation((node: Node) => node)
|
|
||||||
const createObjectURLSpy = jest
|
const createObjectURLSpy = jest
|
||||||
.spyOn(URL, 'createObjectURL')
|
.spyOn(URL, 'createObjectURL')
|
||||||
.mockReturnValue('blob:mock-url')
|
.mockReturnValue('blob:mock-url')
|
||||||
@@ -2212,6 +2215,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
src: '',
|
src: '',
|
||||||
onload: null,
|
onload: null,
|
||||||
contentWindow: mockContentWindow,
|
contentWindow: mockContentWindow,
|
||||||
|
remove: jest.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const createElementSpy = jest
|
const createElementSpy = jest
|
||||||
@@ -2255,12 +2259,11 @@ describe('DocumentDetailComponent', () => {
|
|||||||
mockContentWindow.onafterprint(new Event('afterprint'))
|
mockContentWindow.onafterprint(new Event('afterprint'))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
expect(mockIframe.remove).toHaveBeenCalled()
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
|
||||||
createElementSpy.mockRestore()
|
createElementSpy.mockRestore()
|
||||||
appendChildSpy.mockRestore()
|
appendChildSpy.mockRestore()
|
||||||
removeChildSpy.mockRestore()
|
|
||||||
createObjectURLSpy.mockRestore()
|
createObjectURLSpy.mockRestore()
|
||||||
revokeObjectURLSpy.mockRestore()
|
revokeObjectURLSpy.mockRestore()
|
||||||
})
|
})
|
||||||
@@ -2307,9 +2310,6 @@ describe('DocumentDetailComponent', () => {
|
|||||||
const appendChildSpy = jest
|
const appendChildSpy = jest
|
||||||
.spyOn(document.body, 'appendChild')
|
.spyOn(document.body, 'appendChild')
|
||||||
.mockImplementation((node: Node) => node)
|
.mockImplementation((node: Node) => node)
|
||||||
const removeChildSpy = jest
|
|
||||||
.spyOn(document.body, 'removeChild')
|
|
||||||
.mockImplementation((node: Node) => node)
|
|
||||||
const createObjectURLSpy = jest
|
const createObjectURLSpy = jest
|
||||||
.spyOn(URL, 'createObjectURL')
|
.spyOn(URL, 'createObjectURL')
|
||||||
.mockReturnValue('blob:mock-url')
|
.mockReturnValue('blob:mock-url')
|
||||||
@@ -2332,6 +2332,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
src: '',
|
src: '',
|
||||||
onload: null,
|
onload: null,
|
||||||
contentWindow: mockContentWindow,
|
contentWindow: mockContentWindow,
|
||||||
|
remove: jest.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const createElementSpy = jest
|
const createElementSpy = jest
|
||||||
@@ -2354,15 +2355,21 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
if (expectToast) {
|
if (expectToast) {
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
|
expect(mockIframe.remove).toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
} else {
|
} else {
|
||||||
expect(toastSpy).not.toHaveBeenCalled()
|
expect(toastSpy).not.toHaveBeenCalled()
|
||||||
|
expect(mockIframe.remove).not.toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLSpy).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
component.ngOnDestroy()
|
||||||
|
|
||||||
|
expect(mockIframe.remove).toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
}
|
}
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
|
||||||
|
|
||||||
createElementSpy.mockRestore()
|
createElementSpy.mockRestore()
|
||||||
appendChildSpy.mockRestore()
|
appendChildSpy.mockRestore()
|
||||||
removeChildSpy.mockRestore()
|
|
||||||
createObjectURLSpy.mockRestore()
|
createObjectURLSpy.mockRestore()
|
||||||
revokeObjectURLSpy.mockRestore()
|
revokeObjectURLSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ export class DocumentDetailComponent
|
|||||||
private incomingUpdateModal: NgbModalRef
|
private incomingUpdateModal: NgbModalRef
|
||||||
private pendingIncomingUpdate: IncomingDocumentUpdate
|
private pendingIncomingUpdate: IncomingDocumentUpdate
|
||||||
private lastLocalSaveModified: string | null = null
|
private lastLocalSaveModified: string | null = null
|
||||||
|
private printIframe: HTMLIFrameElement | null = null
|
||||||
|
private printBlobUrl: string | null = null
|
||||||
|
|
||||||
requiresPassword: boolean = false
|
requiresPassword: boolean = false
|
||||||
password: string
|
password: string
|
||||||
@@ -657,7 +659,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
|
modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
|
||||||
|
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.reloadRemoteVersion()
|
this.reloadRemoteVersion()
|
||||||
})
|
})
|
||||||
@@ -868,6 +870,7 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.cleanupPrintDocument()
|
||||||
this.unsubscribeNotifier.next(this)
|
this.unsubscribeNotifier.next(this)
|
||||||
this.unsubscribeNotifier.complete()
|
this.unsubscribeNotifier.complete()
|
||||||
}
|
}
|
||||||
@@ -1371,7 +1374,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
return this.documentsService.delete(this.document())
|
return this.documentsService.delete(this.document())
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1383,7 +1386,7 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.showError($localize`Error deleting document`, error)
|
this.toastService.showError($localize`Error deleting document`, error)
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
this.subscribeModalDelete(modal)
|
this.subscribeModalDelete(modal)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1408,7 +1411,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.reprocessDocuments({ documents: [this.document().id] })
|
.reprocessDocuments({ documents: [this.document().id] })
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@@ -1422,7 +1425,7 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing operation`,
|
$localize`Error executing operation`,
|
||||||
@@ -1795,7 +1798,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.editPdfDocuments([sourceDocumentId], {
|
.editPdfDocuments([sourceDocumentId], {
|
||||||
operations: modal.componentInstance.getOperations(),
|
operations: modal.componentInstance.getOperations(),
|
||||||
@@ -1818,7 +1821,7 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing PDF edit operation`,
|
$localize`Error executing PDF edit operation`,
|
||||||
@@ -1852,7 +1855,7 @@ export class DocumentDetailComponent
|
|||||||
const sourceDocumentId = this.selectedVersionId() ?? this.document().id
|
const sourceDocumentId = this.selectedVersionId() ?? this.document().id
|
||||||
const dialog =
|
const dialog =
|
||||||
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||||
dialog.buttonsEnabled = false
|
dialog.buttonsEnabled.set(false)
|
||||||
this.networkActive.set(true)
|
this.networkActive.set(true)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.removePasswordDocuments([sourceDocumentId], {
|
.removePasswordDocuments([sourceDocumentId], {
|
||||||
@@ -1877,7 +1880,7 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
dialog.buttonsEnabled = true
|
dialog.buttonsEnabled.set(true)
|
||||||
this.networkActive.set(false)
|
this.networkActive.set(false)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing password removal operation`,
|
$localize`Error executing password removal operation`,
|
||||||
@@ -1889,6 +1892,7 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
printDocument() {
|
printDocument() {
|
||||||
|
this.cleanupPrintDocument()
|
||||||
const selectedVersionId = this.getSelectedNonLatestVersionId()
|
const selectedVersionId = this.getSelectedNonLatestVersionId()
|
||||||
const printUrl = this.documentsService.getDownloadUrl(
|
const printUrl = this.documentsService.getDownloadUrl(
|
||||||
this.document().id,
|
this.document().id,
|
||||||
@@ -1902,6 +1906,8 @@ export class DocumentDetailComponent
|
|||||||
next: (blob) => {
|
next: (blob) => {
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe')
|
||||||
|
this.printIframe = iframe
|
||||||
|
this.printBlobUrl = blobUrl
|
||||||
iframe.style.position = 'fixed'
|
iframe.style.position = 'fixed'
|
||||||
iframe.style.right = '0'
|
iframe.style.right = '0'
|
||||||
iframe.style.bottom = '0'
|
iframe.style.bottom = '0'
|
||||||
@@ -1917,22 +1923,19 @@ export class DocumentDetailComponent
|
|||||||
iframe.contentWindow.focus()
|
iframe.contentWindow.focus()
|
||||||
iframe.contentWindow.print()
|
iframe.contentWindow.print()
|
||||||
iframe.contentWindow.onafterprint = () => {
|
iframe.contentWindow.onafterprint = () => {
|
||||||
document.body.removeChild(iframe)
|
this.cleanupPrintDocument()
|
||||||
URL.revokeObjectURL(blobUrl)
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// FF throws cross-origin error on onafterprint
|
// FF throws cross-origin error on onafterprint
|
||||||
const isCrossOriginAfterPrintError =
|
const isCrossOriginAfterPrintError =
|
||||||
err instanceof DOMException &&
|
err instanceof DOMException &&
|
||||||
err.message.includes('onafterprint')
|
err.message.includes('onafterprint')
|
||||||
|
// FF throws here while print preview is still reading the iframe
|
||||||
|
// so keep it alive until the next print or teardown
|
||||||
if (!isCrossOriginAfterPrintError) {
|
if (!isCrossOriginAfterPrintError) {
|
||||||
this.toastService.showError($localize`Print failed.`, err)
|
this.toastService.showError($localize`Print failed.`, err)
|
||||||
|
timer(100).subscribe(() => this.cleanupPrintDocument())
|
||||||
}
|
}
|
||||||
timer(100).subscribe(() => {
|
|
||||||
// delay to avoid FF print failure
|
|
||||||
document.body.removeChild(iframe)
|
|
||||||
URL.revokeObjectURL(blobUrl)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1945,9 +1948,20 @@ export class DocumentDetailComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cleanupPrintDocument() {
|
||||||
|
if (this.printIframe) this.printIframe.remove()
|
||||||
|
this.printIframe = null
|
||||||
|
if (this.printBlobUrl) {
|
||||||
|
URL.revokeObjectURL(this.printBlobUrl)
|
||||||
|
this.printBlobUrl = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public openShareLinks() {
|
public openShareLinks() {
|
||||||
const modal = this.modalService.open(ShareLinksDialogComponent)
|
const modal = this.modalService.open(ShareLinksDialogComponent)
|
||||||
modal.componentInstance.documentId.set(this.document().id)
|
modal.componentInstance.documentId.set(
|
||||||
|
this.selectedVersionId() ?? this.document().id
|
||||||
|
)
|
||||||
modal.componentInstance.hasArchiveVersion.set(
|
modal.componentInstance.hasArchiveVersion.set(
|
||||||
this.metadata()?.has_archive_version ??
|
this.metadata()?.has_archive_version ??
|
||||||
!!this.document()?.archived_file_name
|
!!this.document()?.archived_file_name
|
||||||
|
|||||||
@@ -1683,7 +1683,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expiration_days: 7,
|
expiration_days: 7,
|
||||||
},
|
},
|
||||||
loading: signal(false),
|
loading: signal(false),
|
||||||
buttonsEnabled: true,
|
buttonsEnabled: signal(true),
|
||||||
copied: signal(false),
|
copied: signal(false),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1715,7 +1715,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expiration_days: 7,
|
expiration_days: 7,
|
||||||
})
|
})
|
||||||
expect(dialogInstance.loading()).toBe(false)
|
expect(dialogInstance.loading()).toBe(false)
|
||||||
expect(dialogInstance.buttonsEnabled).toBe(false)
|
expect(dialogInstance.buttonsEnabled()).toBe(false)
|
||||||
expect(dialogInstance.createdBundle).toEqual({ id: 42 })
|
expect(dialogInstance.createdBundle).toEqual({ id: 42 })
|
||||||
expect(typeof dialogInstance.onOpenManage).toBe('function')
|
expect(typeof dialogInstance.onOpenManage).toBe('function')
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||||
@@ -1755,7 +1755,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expiration_days: null,
|
expiration_days: null,
|
||||||
},
|
},
|
||||||
loading: signal(false),
|
loading: signal(false),
|
||||||
buttonsEnabled: true,
|
buttonsEnabled: signal(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1777,7 +1777,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect.any(Error)
|
expect.any(Error)
|
||||||
)
|
)
|
||||||
expect(dialogInstance.loading()).toBe(false)
|
expect(dialogInstance.loading()).toBe(false)
|
||||||
expect(dialogInstance.buttonsEnabled).toBe(true)
|
expect(dialogInstance.buttonsEnabled()).toBe(true)
|
||||||
openSpy.mockRestore()
|
openSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export class BulkEditorComponent
|
|||||||
overrideSelection?: DocumentSelectionQuery
|
overrideSelection?: DocumentSelectionQuery
|
||||||
) {
|
) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
this.setModalButtonsEnabled(modal, false)
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
}
|
}
|
||||||
this.documentService
|
this.documentService
|
||||||
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
|
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
|
||||||
@@ -290,7 +290,7 @@ export class BulkEditorComponent
|
|||||||
options: { deleteOriginals?: boolean } = {}
|
options: { deleteOriginals?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
this.setModalButtonsEnabled(modal, false)
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
}
|
}
|
||||||
request.pipe(first()).subscribe({
|
request.pipe(first()).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -320,7 +320,7 @@ export class BulkEditorComponent
|
|||||||
|
|
||||||
private handleOperationError(modal: NgbModalRef, error: any) {
|
private handleOperationError(modal: NgbModalRef, error: any) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
this.setModalButtonsEnabled(modal, true)
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing bulk operation`,
|
$localize`Error executing bulk operation`,
|
||||||
@@ -328,15 +328,6 @@ export class BulkEditorComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private setModalButtonsEnabled(modal: NgbModalRef, enabled: boolean) {
|
|
||||||
const buttonsEnabled = modal.componentInstance.buttonsEnabled
|
|
||||||
if (typeof buttonsEnabled?.set === 'function') {
|
|
||||||
buttonsEnabled.set(enabled)
|
|
||||||
} else {
|
|
||||||
modal.componentInstance.buttonsEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySelectionData(
|
private applySelectionData(
|
||||||
items: SelectionDataItem[],
|
items: SelectionDataItem[],
|
||||||
selectionModel: FilterableDropdownSelectionModel
|
selectionModel: FilterableDropdownSelectionModel
|
||||||
@@ -872,7 +863,7 @@ export class BulkEditorComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.deleteDocuments(this.getSelectionQuery())
|
this.documentService.deleteDocuments(this.getSelectionQuery())
|
||||||
@@ -920,7 +911,7 @@ export class BulkEditorComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.reprocessDocuments(this.getSelectionQuery())
|
this.documentService.reprocessDocuments(this.getSelectionQuery())
|
||||||
@@ -957,7 +948,7 @@ export class BulkEditorComponent
|
|||||||
rotateDialog.confirmClicked
|
rotateDialog.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
rotateDialog.buttonsEnabled = false
|
rotateDialog.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.rotateDocuments(
|
this.documentService.rotateDocuments(
|
||||||
@@ -990,7 +981,7 @@ export class BulkEditorComponent
|
|||||||
if (mergeDialog.archiveFallback()) {
|
if (mergeDialog.archiveFallback()) {
|
||||||
args.archive_fallback = true
|
args.archive_fallback = true
|
||||||
}
|
}
|
||||||
mergeDialog.buttonsEnabled = false
|
mergeDialog.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args),
|
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args),
|
||||||
@@ -1063,14 +1054,14 @@ export class BulkEditorComponent
|
|||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
dialog.loading.set(true)
|
dialog.loading.set(true)
|
||||||
dialog.buttonsEnabled = false
|
dialog.buttonsEnabled.set(false)
|
||||||
this.shareLinkBundleService
|
this.shareLinkBundleService
|
||||||
.createBundle(dialog.payload)
|
.createBundle(dialog.payload)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
dialog.loading.set(false)
|
dialog.loading.set(false)
|
||||||
dialog.buttonsEnabled = false
|
dialog.buttonsEnabled.set(false)
|
||||||
dialog.createdBundle = result
|
dialog.createdBundle = result
|
||||||
dialog.copied.set(false)
|
dialog.copied.set(false)
|
||||||
dialog.payload = null
|
dialog.payload = null
|
||||||
@@ -1084,7 +1075,7 @@ export class BulkEditorComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
dialog.loading.set(false)
|
dialog.loading.set(false)
|
||||||
dialog.buttonsEnabled = true
|
dialog.buttonsEnabled.set(true)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Share link bundle creation is not available yet.`,
|
$localize`Share link bundle creation is not available yet.`,
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -97,7 +97,9 @@
|
|||||||
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
||||||
@if (!list.activeSavedViewId) {
|
@if (!list.activeSavedViewId) {
|
||||||
@for (view of savedViewService.allViews; track view) {
|
@for (view of savedViewService.allViews; track view) {
|
||||||
<button ngbDropdownItem (click)="loadViewConfig(view.id)">{{view.name}}</button>
|
<button ngbDropdownItem (click)="loadViewConfig(view.id)">
|
||||||
|
<i-bs class="me-2" [name]="view.icon || 'funnel'"></i-bs>{{view.name}}
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
@if (savedViewService.allViews.length > 0) {
|
@if (savedViewService.allViews.length > 0) {
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ $paperless-card-breakpoints: (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Popper may place a dropdown above its toggle when the virtual keyboard
|
||||||
|
// reduces the available viewport, increase the z-index so navbar doesn't
|
||||||
|
// obscure it. See github.com/paperless-ngx/paperless-ngx/pull/13694
|
||||||
|
:host ::ng-deep .sticky-top:has(.dropdown-menu.show) {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 579.98px) {
|
@media (max-width: 579.98px) {
|
||||||
:host-context(main.mobile-search-hidden) .sticky-top {
|
:host-context(main.mobile-search-hidden) .sticky-top {
|
||||||
top: calc(3.5rem - 2px); // height of navbar only when search is hidden
|
top: calc(3.5rem - 2px); // height of navbar only when search is hidden
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ export class DocumentListComponent
|
|||||||
modal.componentInstance.buttonsEnabled.set(false)
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
let savedView: SavedView = {
|
let savedView: SavedView = {
|
||||||
name: formValue.name,
|
name: formValue.name,
|
||||||
|
icon: formValue.icon,
|
||||||
filter_rules: this.list.filterRules,
|
filter_rules: this.list.filterRules,
|
||||||
sort_reverse: this.list.sortReverse,
|
sort_reverse: this.list.sortReverse,
|
||||||
sort_field: this.list.sortField,
|
sort_field: this.list.sortField,
|
||||||
|
|||||||
@@ -1697,6 +1697,24 @@ describe('FilterEditorComponent', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should carry over text filtering once with created and added relative dates', () => {
|
||||||
|
component.textFilter = 'foo'
|
||||||
|
const datesDropdown = fixture.debugElement.query(
|
||||||
|
By.directive(DatesDropdownComponent)
|
||||||
|
)
|
||||||
|
component.dateCreatedRelativeDate = RelativeDate.WITHIN_1_WEEK
|
||||||
|
component.dateAddedRelativeDate = RelativeDate.WITHIN_1_MONTH
|
||||||
|
datesDropdown.triggerEventHandler('datesSet')
|
||||||
|
fixture.detectChanges()
|
||||||
|
tick(400)
|
||||||
|
expect(component.filterRules).toEqual([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
|
value: 'foo,created:[-1 week to now],added:[-1 month to now]',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('should convert legacy title filters into full text query when adding a created relative date', () => {
|
it('should convert legacy title filters into full text query when adding a created relative date', () => {
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
{
|
{
|
||||||
@@ -2195,6 +2213,20 @@ describe('FilterEditorComponent', () => {
|
|||||||
expect(blurSpy).toHaveBeenCalled()
|
expect(blurSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should only dismiss open autocomplete suggestions on Escape, keeping the query', () => {
|
||||||
|
component.textFilter = 'foo bar'
|
||||||
|
component.textFilterInput.nativeElement.value = 'foo bar'
|
||||||
|
jest.spyOn(component.searchTypeahead, 'isPopupOpen').mockReturnValue(true)
|
||||||
|
const dismissSpy = jest
|
||||||
|
.spyOn(component.searchTypeahead, 'dismissPopup')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
component.textFilterInput.nativeElement.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', { key: 'Escape' })
|
||||||
|
)
|
||||||
|
expect(dismissSpy).toHaveBeenCalled()
|
||||||
|
expect(component.textFilter).toEqual('foo bar')
|
||||||
|
})
|
||||||
|
|
||||||
it('should adjust text filter targets if more like search', () => {
|
it('should adjust text filter targets if more like search', () => {
|
||||||
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' // private const
|
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' // private const
|
||||||
component.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
component.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import {
|
import {
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
|
NgbTypeahead,
|
||||||
NgbTypeaheadModule,
|
NgbTypeaheadModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
@@ -351,6 +352,9 @@ export class FilterEditorComponent
|
|||||||
@ViewChild('textFilterInput')
|
@ViewChild('textFilterInput')
|
||||||
textFilterInput: ElementRef
|
textFilterInput: ElementRef
|
||||||
|
|
||||||
|
@ViewChild(NgbTypeahead)
|
||||||
|
searchTypeahead: NgbTypeahead
|
||||||
|
|
||||||
readonly customFields = signal<CustomField[]>([])
|
readonly customFields = signal<CustomField[]>([])
|
||||||
|
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
@@ -1016,7 +1020,6 @@ export class FilterEditorComponent
|
|||||||
this.dateAddedRelativeDate !== null ||
|
this.dateAddedRelativeDate !== null ||
|
||||||
this.dateCreatedRelativeDate !== null
|
this.dateCreatedRelativeDate !== null
|
||||||
) {
|
) {
|
||||||
let queryArgs: Array<string> = []
|
|
||||||
let existingRule = filterRules.find(
|
let existingRule = filterRules.find(
|
||||||
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
|
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
|
||||||
)
|
)
|
||||||
@@ -1038,32 +1041,28 @@ export class FilterEditorComponent
|
|||||||
existingRule.rule_type = FILTER_FULLTEXT_QUERY
|
existingRule.rule_type = FILTER_FULLTEXT_QUERY
|
||||||
}
|
}
|
||||||
|
|
||||||
let existingRuleArgs = existingRule?.value.split(',')
|
let queryArgs = existingRule?.value.split(',') ?? []
|
||||||
if (this.dateCreatedRelativeDate !== null) {
|
if (this.dateCreatedRelativeDate !== null) {
|
||||||
const rd = RELATIVE_DATE_QUERYSTRINGS.find(
|
const rd = RELATIVE_DATE_QUERYSTRINGS.find(
|
||||||
(qS) => qS.relativeDate == this.dateCreatedRelativeDate
|
(qS) => qS.relativeDate == this.dateCreatedRelativeDate
|
||||||
)
|
)
|
||||||
|
queryArgs = queryArgs.filter(
|
||||||
|
(arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED)
|
||||||
|
)
|
||||||
queryArgs.push(
|
queryArgs.push(
|
||||||
`created:${rd.isRange ? `[${rd.dateQuery}]` : `"${rd.dateQuery}"`}`
|
`created:${rd.isRange ? `[${rd.dateQuery}]` : `"${rd.dateQuery}"`}`
|
||||||
)
|
)
|
||||||
if (existingRule) {
|
|
||||||
queryArgs = existingRuleArgs
|
|
||||||
.filter((arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED))
|
|
||||||
.concat(queryArgs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this.dateAddedRelativeDate !== null) {
|
if (this.dateAddedRelativeDate !== null) {
|
||||||
const rd = RELATIVE_DATE_QUERYSTRINGS.find(
|
const rd = RELATIVE_DATE_QUERYSTRINGS.find(
|
||||||
(qS) => qS.relativeDate == this.dateAddedRelativeDate
|
(qS) => qS.relativeDate == this.dateAddedRelativeDate
|
||||||
)
|
)
|
||||||
|
queryArgs = queryArgs.filter(
|
||||||
|
(arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED)
|
||||||
|
)
|
||||||
queryArgs.push(
|
queryArgs.push(
|
||||||
`added:${rd.isRange ? `[${rd.dateQuery}]` : `"${rd.dateQuery}"`}`
|
`added:${rd.isRange ? `[${rd.dateQuery}]` : `"${rd.dateQuery}"`}`
|
||||||
)
|
)
|
||||||
if (existingRule) {
|
|
||||||
queryArgs = existingRuleArgs
|
|
||||||
.filter((arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED))
|
|
||||||
.concat(queryArgs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingRule) {
|
if (existingRule) {
|
||||||
@@ -1155,6 +1154,7 @@ export class FilterEditorComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
set textFilter(value) {
|
set textFilter(value) {
|
||||||
|
this._textFilter = value // set immediately to prevent loss of keystrokes
|
||||||
this.textFilterDebounce.next(value)
|
this.textFilterDebounce.next(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1247,9 +1247,9 @@ export class FilterEditorComponent
|
|||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
filter((query) => !query.length || query.length > 2)
|
filter((query) => !query.length || query.length > 2)
|
||||||
)
|
)
|
||||||
.subscribe((text) =>
|
.subscribe(() =>
|
||||||
this.updateTextFilter(
|
this.updateTextFilter(
|
||||||
text,
|
this._textFilter, // use the current value, not the debounced (possibly stale) one
|
||||||
this.textFilterTarget !== TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
this.textFilterTarget !== TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1325,6 +1325,11 @@ export class FilterEditorComponent
|
|||||||
this.updateTextFilter(filterString)
|
this.updateTextFilter(filterString)
|
||||||
}
|
}
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
|
if (this.searchTypeahead?.isPopupOpen()) {
|
||||||
|
// only dismiss the suggestions, so longer query can use Enter
|
||||||
|
this.searchTypeahead.dismissPopup()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this._textFilter?.length) {
|
if (this._textFilter?.length) {
|
||||||
this.resetTextField()
|
this.resetTextField()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+8
@@ -6,6 +6,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error()?.name" autocomplete="off"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error()?.name" autocomplete="off"></pngx-input-text>
|
||||||
|
<pngx-input-select
|
||||||
|
i18n-title
|
||||||
|
title="Icon"
|
||||||
|
formControlName="icon"
|
||||||
|
[items]="savedViewIcons"
|
||||||
|
iconField="icon"
|
||||||
|
[error]="error()?.icon">
|
||||||
|
</pngx-input-select>
|
||||||
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
|
||||||
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
|
||||||
<pngx-permissions-form accordion="true" formControlName="permissions_form"></pngx-permissions-form>
|
<pngx-permissions-form accordion="true" formControlName="permissions_form"></pngx-permissions-form>
|
||||||
|
|||||||
+5
@@ -9,6 +9,7 @@ import { CheckComponent } from '../../common/input/check/check.component'
|
|||||||
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
||||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { TextComponent } from '../../common/input/text/text.component'
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component'
|
import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component'
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ describe('SaveViewConfigDialogComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
SaveViewConfigDialogComponent,
|
SaveViewConfigDialogComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
|
SelectComponent,
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
PermissionsUserComponent,
|
PermissionsUserComponent,
|
||||||
@@ -63,6 +65,7 @@ describe('SaveViewConfigDialogComponent', () => {
|
|||||||
expect(component.defaultName()).toEqual(name)
|
expect(component.defaultName()).toEqual(name)
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name,
|
name,
|
||||||
|
icon: 'funnel',
|
||||||
showInSideBar: false,
|
showInSideBar: false,
|
||||||
showOnDashboard: false,
|
showOnDashboard: false,
|
||||||
})
|
})
|
||||||
@@ -94,6 +97,7 @@ describe('SaveViewConfigDialogComponent', () => {
|
|||||||
component.save()
|
component.save()
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name,
|
name,
|
||||||
|
icon: 'funnel',
|
||||||
showInSideBar: true,
|
showInSideBar: true,
|
||||||
showOnDashboard: true,
|
showOnDashboard: true,
|
||||||
})
|
})
|
||||||
@@ -113,6 +117,7 @@ describe('SaveViewConfigDialogComponent', () => {
|
|||||||
component.save()
|
component.save()
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name: '',
|
name: '',
|
||||||
|
icon: 'funnel',
|
||||||
showInSideBar: false,
|
showInSideBar: false,
|
||||||
showOnDashboard: false,
|
showOnDashboard: false,
|
||||||
permissions_form: permissions,
|
permissions_form: permissions,
|
||||||
|
|||||||
+9
@@ -13,9 +13,14 @@ import {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import {
|
||||||
|
DEFAULT_SAVED_VIEW_ICON,
|
||||||
|
SAVED_VIEW_ICONS,
|
||||||
|
} from 'src/app/data/saved-view-icons'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { TextComponent } from '../../common/input/text/text.component'
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -24,6 +29,7 @@ import { TextComponent } from '../../common/input/text/text.component'
|
|||||||
styleUrls: ['./save-view-config-dialog.component.scss'],
|
styleUrls: ['./save-view-config-dialog.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
|
SelectComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -41,6 +47,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
public saveClicked = new EventEmitter()
|
public saveClicked = new EventEmitter()
|
||||||
|
|
||||||
users: User[]
|
users: User[]
|
||||||
|
readonly savedViewIcons = SAVED_VIEW_ICONS
|
||||||
|
|
||||||
setDefaultName(value: string) {
|
setDefaultName(value: string) {
|
||||||
this.defaultName.set(value)
|
this.defaultName.set(value)
|
||||||
@@ -49,6 +56,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
|
|
||||||
saveViewConfigForm = new FormGroup({
|
saveViewConfigForm = new FormGroup({
|
||||||
name: new FormControl(''),
|
name: new FormControl(''),
|
||||||
|
icon: new FormControl(DEFAULT_SAVED_VIEW_ICON),
|
||||||
showInSideBar: new FormControl(false),
|
showInSideBar: new FormControl(false),
|
||||||
showOnDashboard: new FormControl(false),
|
showOnDashboard: new FormControl(false),
|
||||||
permissions_form: new FormControl(null),
|
permissions_form: new FormControl(null),
|
||||||
@@ -65,6 +73,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
const formValue = this.saveViewConfigForm.value
|
const formValue = this.saveViewConfigForm.value
|
||||||
const saveViewConfig = {
|
const saveViewConfig = {
|
||||||
name: formValue.name,
|
name: formValue.name,
|
||||||
|
icon: formValue.icon,
|
||||||
showInSideBar: formValue.showInSideBar,
|
showInSideBar: formValue.showInSideBar,
|
||||||
showOnDashboard: formValue.showOnDashboard,
|
showOnDashboard: formValue.showOnDashboard,
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-36
@@ -21,45 +21,47 @@
|
|||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
|
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group d-block d-sm-none">
|
<div class="btn-toolbar gap-2">
|
||||||
<div ngbDropdown container="body" class="d-inline-block">
|
<div class="btn-group d-block d-sm-none">
|
||||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
<div ngbDropdown container="body" class="d-inline-block">
|
||||||
<i-bs name="three-dots-vertical"></i-bs>
|
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||||
</button>
|
<i-bs name="three-dots-vertical"></i-bs>
|
||||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
</button>
|
||||||
<button (click)="editField(field)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" ngbDropdownItem i18n>Edit</button>
|
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||||
<button class="text-danger" (click)="deleteField(field)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" ngbDropdownItem i18n>Delete</button>
|
<button (click)="editField(field)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" ngbDropdownItem i18n>Edit</button>
|
||||||
@if (field.document_count > 0) {
|
<button class="text-danger" (click)="deleteField(field)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" ngbDropdownItem i18n>Delete</button>
|
||||||
<a
|
@if (field.document_count > 0) {
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
|
<a
|
||||||
ngbDropdownItem
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
|
||||||
[routerLink]="getDocumentFilterUrl(field)"
|
ngbDropdownItem
|
||||||
i18n
|
[routerLink]="getDocumentFilterUrl(field)"
|
||||||
>Filter Documents ({{ field.document_count }})</a
|
i18n
|
||||||
>
|
>Filter Documents ({{ field.document_count }})</a
|
||||||
}
|
>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="btn-group d-none d-sm-inline-block">
|
||||||
<div class="btn-group d-none d-sm-inline-block">
|
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)">
|
||||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)">
|
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
|
||||||
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
|
</button>
|
||||||
</button>
|
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)">
|
||||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)">
|
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
||||||
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@if (field.document_count > 0) {
|
|
||||||
<div class="btn-group d-none d-sm-inline-block ms-2">
|
|
||||||
<a
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
[routerLink]="getDocumentFilterUrl(field)"
|
|
||||||
>
|
|
||||||
<i-bs width="1em" height="1em" name="filter" class="me-1"></i-bs><ng-container i18n>Documents</ng-container
|
|
||||||
><span class="badge bg-light text-secondary ms-2">{{ field.document_count }}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
@if (field.document_count > 0) {
|
||||||
|
<div class="btn-group d-none d-sm-inline-block">
|
||||||
|
<a
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
[routerLink]="getDocumentFilterUrl(field)"
|
||||||
|
>
|
||||||
|
<i-bs width="1em" height="1em" name="filter" class="me-1"></i-bs><ng-container i18n>Documents</ng-container
|
||||||
|
><span class="badge bg-light text-secondary ms-2">{{ field.document_count }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ export class CustomFieldsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.customFieldsService.delete(field).subscribe({
|
this.customFieldsService.delete(field).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="my-3 shadow-sm">
|
<div class="my-3">
|
||||||
<ng-container
|
<ng-container
|
||||||
[ngComponentOutlet]="activeSection?.component"
|
[ngComponentOutlet]="activeSection?.component"
|
||||||
#activeOutlet="ngComponentOutlet"
|
#activeOutlet="ngComponentOutlet"
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@
|
|||||||
@if (depth > 0) {
|
@if (depth > 0) {
|
||||||
<div class="indicator"></div>
|
<div class="indicator"></div>
|
||||||
}
|
}
|
||||||
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
<button class="btn btn-link ms-0 ps-0 text-start" style="user-select: text;" [disabled]="!userCanEdit(object)" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
<td class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||||
<td>{{ getDocumentCount(object) }}</td>
|
<td>{{ getDocumentCount(object) }}</td>
|
||||||
|
|||||||
+4
-4
@@ -274,7 +274,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
activeModal.componentInstance.btnClass = 'btn-danger'
|
activeModal.componentInstance.btnClass = 'btn-danger'
|
||||||
activeModal.componentInstance.btnCaption = $localize`Delete`
|
activeModal.componentInstance.btnCaption = $localize`Delete`
|
||||||
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
activeModal.componentInstance.buttonsEnabled = false
|
activeModal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.service
|
this.service
|
||||||
.delete(object)
|
.delete(object)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
@@ -284,7 +284,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.reloadData()
|
this.reloadData()
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
activeModal.componentInstance.buttonsEnabled = true
|
activeModal.componentInstance.buttonsEnabled.set(true)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error while deleting element`,
|
$localize`Error while deleting element`,
|
||||||
error
|
error
|
||||||
@@ -455,7 +455,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.service
|
this.service
|
||||||
.bulk_edit_objects(
|
.bulk_edit_objects(
|
||||||
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
||||||
@@ -472,7 +472,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.reloadData()
|
this.reloadData()
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error deleting objects`,
|
$localize`Error deleting objects`,
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export class MailComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.mailAccountService.delete(account).subscribe({
|
this.mailAccountService.delete(account).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
@@ -298,7 +298,7 @@ export class MailComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.mailRuleService.delete(rule).subscribe({
|
this.mailRuleService.delete(rule).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
@@ -7,23 +7,34 @@
|
|||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
<form [formGroup]="savedViewsForm" (ngSubmit)="save()">
|
<form [formGroup]="savedViewsForm" (ngSubmit)="save()">
|
||||||
<ul class="list-group mb-3" formGroupName="savedViews">
|
<ul class="list-group mb-3" formGroupName="savedViews">
|
||||||
@for (view of savedViews(); track view) {
|
@for (view of pagedSavedViews(); track view) {
|
||||||
<li class="list-group-item py-3">
|
<li class="list-group-item py-3">
|
||||||
<div [formGroupName]="view.id">
|
<div [formGroupName]="view.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-md">
|
||||||
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
|
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-md">
|
||||||
<div class="form-check form-switch mt-3">
|
<pngx-input-select
|
||||||
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
i18n-title
|
||||||
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
title="Icon"
|
||||||
</div>
|
formControlName="icon"
|
||||||
<div class="form-check form-switch">
|
[items]="savedViewIcons"
|
||||||
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
iconField="icon">
|
||||||
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
</pngx-input-select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (canSaveSettings) {
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="form-check form-switch mt-3">
|
||||||
|
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
||||||
|
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
||||||
|
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@if (canDeleteSavedView(view)) {
|
@if (canDeleteSavedView(view)) {
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||||
@@ -79,6 +90,11 @@
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
<div class="d-flex align-items-center mb-3">
|
||||||
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
|
@if (savedViews()?.length > pageSize) {
|
||||||
|
<ngb-pagination class="ms-auto" [pageSize]="pageSize" [collectionSize]="savedViews().length" [page]="page()" [maxSize]="5" (pageChange)="page.set($event)" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|||||||
import { signal } from '@angular/core'
|
import { signal } from '@angular/core'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { Subject, of, throwError } from 'rxjs'
|
import { Subject, of, throwError } from 'rxjs'
|
||||||
@@ -25,8 +26,20 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon
|
|||||||
import { SavedViewsComponent } from './saved-views.component'
|
import { SavedViewsComponent } from './saved-views.component'
|
||||||
|
|
||||||
const savedViews = [
|
const savedViews = [
|
||||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
{
|
||||||
{ id: 2, name: 'view2', show_in_sidebar: false, show_on_dashboard: false },
|
id: 1,
|
||||||
|
name: 'view1',
|
||||||
|
icon: 'archive',
|
||||||
|
show_in_sidebar: true,
|
||||||
|
show_on_dashboard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'view2',
|
||||||
|
icon: 'funnel',
|
||||||
|
show_in_sidebar: false,
|
||||||
|
show_on_dashboard: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('SavedViewsComponent', () => {
|
describe('SavedViewsComponent', () => {
|
||||||
@@ -157,6 +170,24 @@ describe('SavedViewsComponent', () => {
|
|||||||
expect(patchBody.show_in_sidebar).toBeUndefined()
|
expect(patchBody.show_in_sidebar).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should persist a changed icon', () => {
|
||||||
|
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
|
||||||
|
const view = savedViews[0]
|
||||||
|
const iconControl = component.savedViewsForm
|
||||||
|
.get('savedViews')
|
||||||
|
.get(view.id.toString())
|
||||||
|
.get('icon')
|
||||||
|
|
||||||
|
iconControl.setValue('bell')
|
||||||
|
iconControl.markAsDirty()
|
||||||
|
component.save()
|
||||||
|
|
||||||
|
expect(patchSpy.mock.calls[0][0][0]).toMatchObject({
|
||||||
|
id: view.id,
|
||||||
|
icon: 'bell',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should persist visibility changes to user settings', () => {
|
it('should persist visibility changes to user settings', () => {
|
||||||
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
|
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
|
||||||
const updateVisibilitySpy = jest
|
const updateVisibilitySpy = jest
|
||||||
@@ -222,6 +253,44 @@ describe('SavedViewsComponent', () => {
|
|||||||
).toEqual(view.show_on_dashboard)
|
).toEqual(view.show_on_dashboard)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should page saved views, clamp the page if views are removed', () => {
|
||||||
|
const manyViews = Array.from({ length: 30 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: `view${i + 1}`,
|
||||||
|
})) as SavedView[]
|
||||||
|
const listSpy = jest.spyOn(savedViewService, 'list').mockReturnValue(
|
||||||
|
of({
|
||||||
|
all: manyViews.map((v) => v.id),
|
||||||
|
count: manyViews.length,
|
||||||
|
results: manyViews.concat([]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
component.ngOnInit()
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(listSpy).toHaveBeenCalledWith(1, 100000, null, false, {
|
||||||
|
full_perms: true,
|
||||||
|
})
|
||||||
|
expect(component.pagedSavedViews()).toHaveLength(25)
|
||||||
|
expect(fixture.debugElement.query(By.css('ngb-pagination'))).not.toBeNull()
|
||||||
|
// all views have controls, not just the current page
|
||||||
|
expect(
|
||||||
|
Object.keys(component.savedViewsForm.get('savedViews').value)
|
||||||
|
).toHaveLength(30)
|
||||||
|
|
||||||
|
component.page.set(2)
|
||||||
|
expect(component.pagedSavedViews()).toHaveLength(5)
|
||||||
|
|
||||||
|
listSpy.mockReturnValue(
|
||||||
|
of({
|
||||||
|
all: manyViews.slice(0, 25).map((v) => v.id),
|
||||||
|
count: 25,
|
||||||
|
results: manyViews.slice(0, 25),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
component.ngOnInit()
|
||||||
|
expect(component.page()).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should support editing permissions', () => {
|
it('should support editing permissions', () => {
|
||||||
const confirmClicked = new Subject<any>()
|
const confirmClicked = new Subject<any>()
|
||||||
const modalRef = {
|
const modalRef = {
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { AsyncPipe } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
|
import {
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
|
import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
|
||||||
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
|
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
|
||||||
import { DisplayMode } from 'src/app/data/document'
|
import { DisplayMode } from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
|
import {
|
||||||
|
DEFAULT_SAVED_VIEW_ICON,
|
||||||
|
SAVED_VIEW_ICONS,
|
||||||
|
} from 'src/app/data/saved-view-icons'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
@@ -24,6 +36,7 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||||
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
|
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
|
||||||
import { NumberComponent } from '../../common/input/number/number.component'
|
import { NumberComponent } from '../../common/input/number/number.component'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { TextComponent } from '../../common/input/text/text.component'
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
@@ -35,12 +48,14 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
|
SelectComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
DragDropSelectComponent,
|
DragDropSelectComponent,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -55,8 +70,17 @@ export class SavedViewsComponent
|
|||||||
private readonly modalService = inject(NgbModal)
|
private readonly modalService = inject(NgbModal)
|
||||||
|
|
||||||
DisplayMode = DisplayMode
|
DisplayMode = DisplayMode
|
||||||
|
readonly savedViewIcons = SAVED_VIEW_ICONS
|
||||||
|
|
||||||
readonly savedViews = signal<SavedView[]>(undefined)
|
readonly savedViews = signal<SavedView[]>(undefined)
|
||||||
|
readonly page = signal(1)
|
||||||
|
public readonly pageSize = 25
|
||||||
|
// All views are loaded at init, so paging is only for display
|
||||||
|
readonly pagedSavedViews = computed(() => {
|
||||||
|
const start = (this.page() - 1) * this.pageSize
|
||||||
|
return this.savedViews()?.slice(start, start + this.pageSize)
|
||||||
|
})
|
||||||
|
|
||||||
private savedViewsGroup = new FormGroup({})
|
private savedViewsGroup = new FormGroup({})
|
||||||
public savedViewsForm: FormGroup = new FormGroup({
|
public savedViewsForm: FormGroup = new FormGroup({
|
||||||
savedViews: this.savedViewsGroup,
|
savedViews: this.savedViewsGroup,
|
||||||
@@ -71,7 +95,9 @@ export class SavedViewsComponent
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.settings.organizingSidebarSavedViews.set(true)
|
if (this.canSaveSettings) {
|
||||||
|
this.settings.organizingSidebarSavedViews.set(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -81,15 +107,19 @@ export class SavedViewsComponent
|
|||||||
private reloadViews(): void {
|
private reloadViews(): void {
|
||||||
this.loading.set(true)
|
this.loading.set(true)
|
||||||
this.savedViewService
|
this.savedViewService
|
||||||
.list(null, null, null, false, { full_perms: true })
|
.list(1, 100000, null, false, { full_perms: true })
|
||||||
.subscribe((r) => {
|
.subscribe((r) => {
|
||||||
this.savedViews.set(r.results)
|
this.savedViews.set(r.results)
|
||||||
|
const pageCount = Math.ceil(r.results.length / this.pageSize)
|
||||||
|
this.page.update((page) => Math.min(page, Math.max(1, pageCount)))
|
||||||
this.initialize()
|
this.initialize()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.settings.organizingSidebarSavedViews.set(false)
|
if (this.canSaveSettings) {
|
||||||
|
this.settings.organizingSidebarSavedViews.set(false)
|
||||||
|
}
|
||||||
super.ngOnDestroy()
|
super.ngOnDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +135,7 @@ export class SavedViewsComponent
|
|||||||
storeData.savedViews[view.id.toString()] = {
|
storeData.savedViews[view.id.toString()] = {
|
||||||
id: view.id,
|
id: view.id,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
|
icon: view.icon ?? DEFAULT_SAVED_VIEW_ICON,
|
||||||
show_on_dashboard: view.show_on_dashboard,
|
show_on_dashboard: view.show_on_dashboard,
|
||||||
show_in_sidebar: view.show_in_sidebar,
|
show_in_sidebar: view.show_in_sidebar,
|
||||||
page_size: view.page_size,
|
page_size: view.page_size,
|
||||||
@@ -117,6 +148,7 @@ export class SavedViewsComponent
|
|||||||
new FormGroup({
|
new FormGroup({
|
||||||
id: new FormControl({ value: null, disabled: !canEdit }),
|
id: new FormControl({ value: null, disabled: !canEdit }),
|
||||||
name: new FormControl({ value: null, disabled: !canEdit }),
|
name: new FormControl({ value: null, disabled: !canEdit }),
|
||||||
|
icon: new FormControl({ value: null, disabled: !canEdit }),
|
||||||
show_on_dashboard: new FormControl({
|
show_on_dashboard: new FormControl({
|
||||||
value: null,
|
value: null,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -195,6 +227,7 @@ export class SavedViewsComponent
|
|||||||
|
|
||||||
const modelFieldsChanged =
|
const modelFieldsChanged =
|
||||||
group.get('name')?.dirty ||
|
group.get('name')?.dirty ||
|
||||||
|
group.get('icon')?.dirty ||
|
||||||
group.get('page_size')?.dirty ||
|
group.get('page_size')?.dirty ||
|
||||||
group.get('display_mode')?.dirty ||
|
group.get('display_mode')?.dirty ||
|
||||||
group.get('display_fields')?.dirty
|
group.get('display_fields')?.dirty
|
||||||
@@ -216,7 +249,7 @@ export class SavedViewsComponent
|
|||||||
switchMap(() => this.savedViewService.patchMany(changed))
|
switchMap(() => this.savedViewService.patchMany(changed))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (visibilityChanged) {
|
if (visibilityChanged && this.canSaveSettings) {
|
||||||
saveOperation = saveOperation.pipe(
|
saveOperation = saveOperation.pipe(
|
||||||
switchMap(() =>
|
switchMap(() =>
|
||||||
this.settings.updateSavedViewsVisibility(
|
this.settings.updateSavedViewsVisibility(
|
||||||
@@ -250,6 +283,19 @@ export class SavedViewsComponent
|
|||||||
return this.permissionsService.currentUserOwnsObject(view)
|
return this.permissionsService.currentUserOwnsObject(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get canSaveSettings(): boolean {
|
||||||
|
return (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Change,
|
||||||
|
PermissionType.UISettings
|
||||||
|
) &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Add,
|
||||||
|
PermissionType.UISettings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public editPermissions(savedView: SavedView): void {
|
public editPermissions(savedView: SavedView): void {
|
||||||
const modal = this.modalService.open(PermissionsDialogComponent, {
|
const modal = this.modalService.open(PermissionsDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class WorkflowsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.workflowService.delete(workflow).subscribe({
|
this.workflowService.delete(workflow).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
export const DEFAULT_SAVED_VIEW_ICON = 'funnel'
|
||||||
|
|
||||||
|
export const SAVED_VIEW_ICONS = [
|
||||||
|
{ id: 'archive', name: $localize`Archive`, icon: 'archive' },
|
||||||
|
{ id: 'bank', name: $localize`Bank`, icon: 'bank' },
|
||||||
|
{ id: 'basket', name: $localize`Basket`, icon: 'basket' },
|
||||||
|
{ id: 'bell', name: $localize`Bell`, icon: 'bell' },
|
||||||
|
{ id: 'bookmark', name: $localize`Bookmark`, icon: 'bookmark' },
|
||||||
|
{ id: 'boxes', name: $localize`Boxes`, icon: 'boxes' },
|
||||||
|
{ id: 'briefcase', name: $localize`Briefcase`, icon: 'briefcase' },
|
||||||
|
{ id: 'building', name: $localize`Building`, icon: 'building' },
|
||||||
|
{ id: 'calculator', name: $localize`Calculator`, icon: 'calculator' },
|
||||||
|
{ id: 'calendar', name: $localize`Calendar`, icon: 'calendar' },
|
||||||
|
{ id: 'camera', name: $localize`Camera`, icon: 'camera' },
|
||||||
|
{
|
||||||
|
id: 'card-checklist',
|
||||||
|
name: $localize`Checklist`,
|
||||||
|
icon: 'card-checklist',
|
||||||
|
},
|
||||||
|
{ id: 'cash', name: $localize`Cash`, icon: 'cash' },
|
||||||
|
{ id: 'chat-left-text', name: $localize`Chat`, icon: 'chat-left-text' },
|
||||||
|
{ id: 'check-circle', name: $localize`Check`, icon: 'check-circle' },
|
||||||
|
{ id: 'clipboard', name: $localize`Clipboard`, icon: 'clipboard' },
|
||||||
|
{ id: 'clock-history', name: $localize`Clock`, icon: 'clock-history' },
|
||||||
|
{ id: 'credit-card', name: $localize`Credit card`, icon: 'credit-card' },
|
||||||
|
{ id: 'download', name: $localize`Download`, icon: 'download' },
|
||||||
|
{ id: 'envelope', name: $localize`Envelope`, icon: 'envelope' },
|
||||||
|
{
|
||||||
|
id: 'exclamation-triangle',
|
||||||
|
name: $localize`Warning`,
|
||||||
|
icon: 'exclamation-triangle',
|
||||||
|
},
|
||||||
|
{ id: 'file-earmark', name: $localize`File`, icon: 'file-earmark' },
|
||||||
|
{
|
||||||
|
id: 'file-earmark-check',
|
||||||
|
name: $localize`Checked file`,
|
||||||
|
icon: 'file-earmark-check',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-earmark-lock',
|
||||||
|
name: $localize`Locked file`,
|
||||||
|
icon: 'file-earmark-lock',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-earmark-medical',
|
||||||
|
name: $localize`Medical file`,
|
||||||
|
icon: 'file-earmark-medical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-earmark-person',
|
||||||
|
name: $localize`Person file`,
|
||||||
|
icon: 'file-earmark-person',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-earmark-spreadsheet',
|
||||||
|
name: $localize`Spreadsheet`,
|
||||||
|
icon: 'file-earmark-spreadsheet',
|
||||||
|
},
|
||||||
|
{ id: 'file-text', name: $localize`Text file`, icon: 'file-text' },
|
||||||
|
{ id: 'files', name: $localize`Files`, icon: 'files' },
|
||||||
|
{ id: 'folder', name: $localize`Folder`, icon: 'folder' },
|
||||||
|
{ id: 'funnel', name: $localize`Filter`, icon: 'funnel' },
|
||||||
|
{ id: 'gear', name: $localize`Gear`, icon: 'gear' },
|
||||||
|
{ id: 'globe2', name: $localize`Globe`, icon: 'globe2' },
|
||||||
|
{ id: 'hash', name: $localize`Hash`, icon: 'hash' },
|
||||||
|
{ id: 'heart', name: $localize`Heart`, icon: 'heart' },
|
||||||
|
{ id: 'house', name: $localize`House`, icon: 'house' },
|
||||||
|
{ id: 'inbox', name: $localize`Inbox`, icon: 'inbox' },
|
||||||
|
{ id: 'journals', name: $localize`Journals`, icon: 'journals' },
|
||||||
|
{ id: 'list-task', name: $localize`Task list`, icon: 'list-task' },
|
||||||
|
{ id: 'newspaper', name: $localize`Newspaper`, icon: 'newspaper' },
|
||||||
|
{ id: 'paperclip', name: $localize`Attachment`, icon: 'paperclip' },
|
||||||
|
{ id: 'people', name: $localize`People`, icon: 'people' },
|
||||||
|
{ id: 'person', name: $localize`Person`, icon: 'person' },
|
||||||
|
{ id: 'printer', name: $localize`Printer`, icon: 'printer' },
|
||||||
|
{ id: 'receipt', name: $localize`Receipt`, icon: 'receipt' },
|
||||||
|
{ id: 'safe', name: $localize`Safe`, icon: 'safe' },
|
||||||
|
{ id: 'search', name: $localize`Search`, icon: 'search' },
|
||||||
|
{ id: 'send', name: $localize`Send`, icon: 'send' },
|
||||||
|
{ id: 'shop', name: $localize`Shop`, icon: 'shop' },
|
||||||
|
{ id: 'stack', name: $localize`Stack`, icon: 'stack' },
|
||||||
|
{ id: 'stars', name: $localize`Stars`, icon: 'stars' },
|
||||||
|
{ id: 'tag', name: $localize`Tag`, icon: 'tag' },
|
||||||
|
{ id: 'tags', name: $localize`Tags`, icon: 'tags' },
|
||||||
|
{ id: 'telephone', name: $localize`Telephone`, icon: 'telephone' },
|
||||||
|
{ id: 'truck', name: $localize`Truck`, icon: 'truck' },
|
||||||
|
{ id: 'upc-scan', name: $localize`Barcode`, icon: 'upc-scan' },
|
||||||
|
{ id: 'wallet2', name: $localize`Wallet`, icon: 'wallet2' },
|
||||||
|
]
|
||||||
@@ -5,6 +5,8 @@ import { ObjectWithPermissions } from './object-with-permissions'
|
|||||||
export interface SavedView extends ObjectWithPermissions {
|
export interface SavedView extends ObjectWithPermissions {
|
||||||
name?: string
|
name?: string
|
||||||
|
|
||||||
|
icon?: string
|
||||||
|
|
||||||
show_on_dashboard?: boolean
|
show_on_dashboard?: boolean
|
||||||
|
|
||||||
show_in_sidebar?: boolean
|
show_in_sidebar?: boolean
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class DirtyFormGuard extends DirtyCheckGuard {
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Leave page`
|
modal.componentInstance.btnCaption = $localize`Leave page`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
})
|
})
|
||||||
const subject = new Subject<boolean>()
|
const subject = new Subject<boolean>()
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ export class DirtySavedViewGuard {
|
|||||||
modal.componentInstance.alternativeBtnClass = 'btn-primary'
|
modal.componentInstance.alternativeBtnClass = 'btn-primary'
|
||||||
modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
|
modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
|
||||||
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
component.saveViewConfig()
|
component.saveViewConfig()
|
||||||
modal.close()
|
modal.close()
|
||||||
})
|
})
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class OpenDocumentsService {
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Close document`
|
modal.componentInstance.btnCaption = $localize`Close document`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.openDocuments.splice(index, 1)
|
this.openDocuments.splice(index, 1)
|
||||||
this.dirtyDocuments.delete(doc.id)
|
this.dirtyDocuments.delete(doc.id)
|
||||||
@@ -165,7 +165,7 @@ export class OpenDocumentsService {
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Close documents`
|
modal.componentInstance.btnCaption = $localize`Close documents`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.openDocuments.splice(0, this.openDocuments.length)
|
this.openDocuments.splice(0, this.openDocuments.length)
|
||||||
this.dirtyDocuments.clear()
|
this.dirtyDocuments.clear()
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ describe('PermissionsService', () => {
|
|||||||
actionKey: 'View', // PermissionAction.View
|
actionKey: 'View', // PermissionAction.View
|
||||||
typeKey: 'SystemMonitoring', // PermissionType.SystemMonitoring
|
typeKey: 'SystemMonitoring', // PermissionType.SystemMonitoring
|
||||||
})
|
})
|
||||||
|
expect(permissionsService.getPermissionKeys('add_sharelinkbundle')).toEqual(
|
||||||
|
{
|
||||||
|
actionKey: 'Add', // PermissionAction.Add
|
||||||
|
typeKey: 'ShareLinkBundle', // PermissionType.ShareLinkBundle
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('correctly checks explicit global permissions', () => {
|
it('correctly checks explicit global permissions', () => {
|
||||||
@@ -269,6 +275,10 @@ describe('PermissionsService', () => {
|
|||||||
'view_sharelink',
|
'view_sharelink',
|
||||||
'change_sharelink',
|
'change_sharelink',
|
||||||
'delete_sharelink',
|
'delete_sharelink',
|
||||||
|
'add_sharelinkbundle',
|
||||||
|
'view_sharelinkbundle',
|
||||||
|
'change_sharelinkbundle',
|
||||||
|
'delete_sharelinkbundle',
|
||||||
'add_workflow',
|
'add_workflow',
|
||||||
'view_workflow',
|
'view_workflow',
|
||||||
'change_workflow',
|
'change_workflow',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export enum PermissionType {
|
|||||||
User = '%s_user',
|
User = '%s_user',
|
||||||
Group = '%s_group',
|
Group = '%s_group',
|
||||||
ShareLink = '%s_sharelink',
|
ShareLink = '%s_sharelink',
|
||||||
|
ShareLinkBundle = '%s_sharelinkbundle',
|
||||||
CustomField = '%s_customfield',
|
CustomField = '%s_customfield',
|
||||||
Workflow = '%s_workflow',
|
Workflow = '%s_workflow',
|
||||||
ProcessedMail = '%s_processedmail',
|
ProcessedMail = '%s_processedmail',
|
||||||
|
|||||||
@@ -57,6 +57,19 @@ describe('LocalizedDateParserFormatter', () => {
|
|||||||
expect(val).toEqual({ day: 4, month: 5, year: 2023 })
|
expect(val).toEqual({ day: 4, month: 5, year: 2023 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse yyyy-mm-dd input with unpadded month or day by locale', () => {
|
||||||
|
let val = dateParserFormatter.parse('2023-5-4')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 2023 })
|
||||||
|
|
||||||
|
settingsService.setLanguage('de-de') // dd.mm.yyyy
|
||||||
|
val = dateParserFormatter.parse('2023-5-4')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 2023 })
|
||||||
|
|
||||||
|
settingsService.setLanguage('tr-tr') // yyyy-mm-dd
|
||||||
|
val = dateParserFormatter.parse('2023-5-4')
|
||||||
|
expect(val).toEqual({ day: 4, month: 5, year: 2023 })
|
||||||
|
})
|
||||||
|
|
||||||
it('should parse date struct to string by locale', () => {
|
it('should parse date struct to string by locale', () => {
|
||||||
const dateStruct = {
|
const dateStruct = {
|
||||||
day: 4,
|
day: 4,
|
||||||
|
|||||||
@@ -52,15 +52,11 @@ export class LocalizedDateParserFormatter extends NgbDateParserFormatter {
|
|||||||
let segments = value.split(this.separatorRegExp)
|
let segments = value.split(this.separatorRegExp)
|
||||||
|
|
||||||
// always accept strict yyyy*mm*dd format even if that's not the input format since we can be certain its not yyyy*dd*mm
|
// always accept strict yyyy*mm*dd format even if that's not the input format since we can be certain its not yyyy*dd*mm
|
||||||
if (
|
if (segments.length == 3 && segments[0].length == 4) {
|
||||||
value.length == 10 &&
|
|
||||||
segments.length == 3 &&
|
|
||||||
segments[0].length == 4
|
|
||||||
) {
|
|
||||||
return inputFormat
|
return inputFormat
|
||||||
.replace('yyyy', segments[0])
|
.replace('yyyy', segments[0])
|
||||||
.replace('mm', segments[1])
|
.replace('mm', segments[1].padStart(2, '0'))
|
||||||
.replace('dd', segments[2])
|
.replace('dd', segments[2].padStart(2, '0'))
|
||||||
} else {
|
} else {
|
||||||
// otherwise pad & re-join without separator
|
// otherwise pad & re-join without separator
|
||||||
value = segments.map((segment) => segment.padStart(2, '0')).join('')
|
value = segments.map((segment) => segment.padStart(2, '0')).join('')
|
||||||
|
|||||||
+1414
-907
File diff suppressed because it is too large
Load Diff
+1414
-907
File diff suppressed because it is too large
Load Diff
+1414
-907
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user