mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -05:00
Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ce841d4196 | ||
![]() |
c77f8acf41 | ||
![]() |
212674f9df | ||
![]() |
283ced56d1 | ||
![]() |
530e57151d | ||
![]() |
1141c3f361 | ||
![]() |
49416d3372 | ||
![]() |
88ae60a4a0 | ||
![]() |
6651c80fb9 | ||
![]() |
6d6650d5f6 | ||
![]() |
6df252c99b | ||
![]() |
5881f05dbc | ||
![]() |
00eba3b223 | ||
![]() |
f7ab8d23a7 | ||
![]() |
85b596d20d | ||
![]() |
4d43f6b63d | ||
![]() |
ea1eb551a7 | ||
![]() |
5842944d1e | ||
![]() |
5781a0d51f | ||
![]() |
e5f48739a0 | ||
![]() |
d378c861f6 | ||
![]() |
9466bfdb00 | ||
![]() |
f02e8e0dc3 | ||
![]() |
c1ed87a44f | ||
![]() |
16169ca331 | ||
![]() |
26900e0766 | ||
![]() |
aa798604b3 | ||
![]() |
bb98fc5f65 | ||
![]() |
dc1918ad10 | ||
![]() |
ea632d0417 | ||
![]() |
648dc709fd | ||
![]() |
1a84f6a20e | ||
![]() |
96af953e6f | ||
![]() |
6db9e292ba | ||
![]() |
2e2362e2df | ||
![]() |
51dd95be3d | ||
![]() |
e16645b146 | ||
![]() |
0068f091bb | ||
![]() |
ad6efd2898 | ||
![]() |
86e380bb1c | ||
![]() |
58aacd4814 | ||
![]() |
ad07791bac | ||
![]() |
783090c2cd | ||
![]() |
41a3c7c89b | ||
![]() |
16cc7415c1 | ||
![]() |
98c5cf89ef | ||
![]() |
53e04e66cf | ||
![]() |
2a6e79acc8 | ||
![]() |
2da5e46386 | ||
![]() |
4dbf8d7969 | ||
![]() |
4a52fc27d4 | ||
![]() |
05cd34c8af | ||
![]() |
8a622181fc | ||
![]() |
13f38bf3a1 | ||
![]() |
16acc2d6ad | ||
![]() |
c2c9a953d3 | ||
![]() |
530f4a8b28 | ||
![]() |
8eb1dc4f62 | ||
![]() |
a2b87fe012 | ||
![]() |
3dcb973adb | ||
![]() |
8e8810cbaa | ||
![]() |
b0aeec4c43 | ||
![]() |
1ac298f6ff | ||
![]() |
6d5f4e92cc | ||
![]() |
416ad13aaf | ||
![]() |
a7e1299194 | ||
![]() |
a12e1fae72 | ||
![]() |
f525ac0af6 | ||
![]() |
58bf9c552b | ||
![]() |
22d257cd1f | ||
![]() |
f1bf1ddc54 | ||
![]() |
6015cc0e4a | ||
![]() |
f9926d77d5 | ||
![]() |
4f85dcecfc | ||
![]() |
30c31a3d4c | ||
![]() |
c64667d396 | ||
![]() |
9f6613fe05 | ||
![]() |
ea47af7034 | ||
![]() |
d46abeff01 | ||
![]() |
2b39697ffb | ||
![]() |
4b00a72ff5 | ||
![]() |
e590b2482e | ||
![]() |
eb7dd80410 | ||
![]() |
86338465fb | ||
![]() |
a41dbdd12c | ||
![]() |
1e10a438cd | ||
![]() |
ab34ea724d | ||
![]() |
fd8bfe1a80 | ||
![]() |
9043f45350 | ||
![]() |
5921e6d13e | ||
![]() |
ee2bfe2350 |
3
.codespellrc
Normal file
3
.codespellrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[codespell]
|
||||||
|
write-changes = True
|
||||||
|
ignore-words-list = criterias,afterall,valeu,ureue,equest,ure
|
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -20,7 +20,7 @@ body:
|
|||||||
- [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/).
|
- [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/).
|
||||||
- [The installation instructions](https://docs.paperless-ngx.com/setup/#installation).
|
- [The installation instructions](https://docs.paperless-ngx.com/setup/#installation).
|
||||||
- [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues).
|
- [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues).
|
||||||
- Disable any customer container initialization scripts, if using
|
- Disable any custom container initialization scripts, if using
|
||||||
|
|
||||||
If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support).
|
If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,7 +9,7 @@ Please include a summary of the change and which issue is fixed (if any) and any
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
⚠️ Important: Pull requests that implement a new feature *should almost always target an existing feature request*. This is in order to balance the work of implementing and maintaining new features vs. community-interest. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
|
⚠️ Important: Pull requests that implement a new feature or enhancement *should almost always target an existing feature request* with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Closes #(issue or discussion)
|
Closes #(issue or discussion)
|
||||||
@@ -22,7 +22,7 @@ NOTE: Please check only one box!
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] Bug fix: non-breaking change which fixes an issue.
|
- [ ] Bug fix: non-breaking change which fixes an issue.
|
||||||
- [ ] New feature: non-breaking change which adds functionality. _Please read the important note above._
|
- [ ] New feature / Enhancement: non-breaking change which adds functionality. _Please read the important note above._
|
||||||
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
|
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
|
||||||
- [ ] Documentation only.
|
- [ ] Documentation only.
|
||||||
- [ ] Other. Please explain:
|
- [ ] Other. Please explain:
|
||||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -169,7 +169,7 @@ jobs:
|
|||||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
||||||
|
|
||||||
install-frontend-depedendencies:
|
install-frontend-depedendencies:
|
||||||
name: "Install Frontend Dependendencies"
|
name: "Install Frontend Dependencies"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: 'src-ui/package-lock.json'
|
cache-dependency-path: 'src-ui/package-lock.json'
|
||||||
- name: Cache frontend depdendencies
|
- name: Cache frontend dependencies
|
||||||
id: cache-frontend-deps
|
id: cache-frontend-deps
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: 'src-ui/package-lock.json'
|
cache-dependency-path: 'src-ui/package-lock.json'
|
||||||
- name: Cache frontend depdendencies
|
- name: Cache frontend dependencies
|
||||||
id: cache-frontend-deps
|
id: cache-frontend-deps
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -310,7 +310,7 @@ jobs:
|
|||||||
build-docker-image:
|
build-docker-image:
|
||||||
name: Build Docker image for ${{ github.ref_name }}
|
name: Build Docker image for ${{ github.ref_name }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
2
.github/workflows/cleanup-tags.yml
vendored
2
.github/workflows/cleanup-tags.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
package_name: "${{ matrix.primary-name }}"
|
package_name: "${{ matrix.primary-name }}"
|
||||||
scheme: "branch"
|
scheme: "branch"
|
||||||
repo_name: "paperless-ngx"
|
repo_name: "paperless-ngx"
|
||||||
match_regex: "feature-"
|
match_regex: "(feature|fix)"
|
||||||
do_delete: "true"
|
do_delete: "true"
|
||||||
|
|
||||||
cleanup-untagged-images:
|
cleanup-untagged-images:
|
||||||
|
1
.github/workflows/crowdin.yml
vendored
1
.github/workflows/crowdin.yml
vendored
@@ -7,6 +7,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
paths: [
|
paths: [
|
||||||
'src/locale/**',
|
'src/locale/**',
|
||||||
|
'src-ui/messages.xlf',
|
||||||
'src-ui/src/locale/**'
|
'src-ui/src/locale/**'
|
||||||
]
|
]
|
||||||
branches: [ dev ]
|
branches: [ dev ]
|
||||||
|
93
.github/workflows/repo-maintenance.yml
vendored
93
.github/workflows/repo-maintenance.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
|||||||
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
|
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
|
||||||
|
|
||||||
for (const discussion of result.repository.discussions.nodes) {
|
for (const discussion of result.repository.discussions.nodes) {
|
||||||
console.log(`Closing dicussion #${discussion.number} (${discussion.id})`)
|
console.log(`Closing discussion #${discussion.number} (${discussion.id})`)
|
||||||
|
|
||||||
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
||||||
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||||
@@ -107,3 +107,94 @@ jobs:
|
|||||||
|
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
}
|
}
|
||||||
|
close-outdated-discussions:
|
||||||
|
name: 'Close Outdated Discussions'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const CUTOFF_DAYS = 180;
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - CUTOFF_DAYS);
|
||||||
|
|
||||||
|
const query = `query(
|
||||||
|
$owner:String!,
|
||||||
|
$name:String!,
|
||||||
|
$supportCategory:ID!,
|
||||||
|
$generalCategory:ID!,
|
||||||
|
) {
|
||||||
|
supportDiscussions: repository(owner:$owner, name:$name){
|
||||||
|
discussions(
|
||||||
|
categoryId:$supportCategory,
|
||||||
|
last:50,
|
||||||
|
answered:false,
|
||||||
|
states:[OPEN],
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
id,
|
||||||
|
number,
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
generalDiscussions: repository(owner:$owner, name:$name){
|
||||||
|
discussions(
|
||||||
|
categoryId:$generalCategory,
|
||||||
|
last:50,
|
||||||
|
states:[OPEN],
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
id,
|
||||||
|
number,
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const variables = {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
name: context.repo.repo,
|
||||||
|
supportCategory: "DIC_kwDOG1Zs184CBKWK",
|
||||||
|
generalCategory: "DIC_kwDOG1Zs184CBKWJ"
|
||||||
|
}
|
||||||
|
const result = await github.graphql(query, variables);
|
||||||
|
const combinedDiscussions = [
|
||||||
|
...result.supportDiscussions.discussions.nodes,
|
||||||
|
...result.generalDiscussions.discussions.nodes,
|
||||||
|
]
|
||||||
|
|
||||||
|
console.log(`Checking ${combinedDiscussions.length} open discussions`);
|
||||||
|
|
||||||
|
for (const discussion of combinedDiscussions) {
|
||||||
|
if (new Date(discussion.updatedAt) < cutoff) {
|
||||||
|
console.log(`Closing outdated discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt}`);
|
||||||
|
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
||||||
|
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const commentVariables = {
|
||||||
|
discussion: discussion.id,
|
||||||
|
body: 'This discussion has been automatically closed due to inactivity.',
|
||||||
|
}
|
||||||
|
await github.graphql(addCommentMutation, commentVariables);
|
||||||
|
|
||||||
|
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
|
||||||
|
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const closeVariables = {
|
||||||
|
discussion: discussion.id,
|
||||||
|
reason: "OUTDATED",
|
||||||
|
}
|
||||||
|
await github.graphql(closeDiscussionMutation, closeVariables);
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -28,6 +28,14 @@ repos:
|
|||||||
- svg
|
- svg
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.2.6
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||||
|
exclude_types:
|
||||||
|
- pofile
|
||||||
|
- json
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: 'v3.1.0'
|
rev: 'v3.1.0'
|
||||||
hooks:
|
hooks:
|
||||||
@@ -39,11 +47,11 @@ repos:
|
|||||||
exclude: "(^Pipfile\\.lock$)"
|
exclude: "(^Pipfile\\.lock$)"
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 'v0.1.5'
|
rev: 'v0.1.11'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.11.0
|
rev: 23.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
# Dockerfile hooks
|
# Dockerfile hooks
|
||||||
|
@@ -189,7 +189,7 @@ RUN set -eux \
|
|||||||
&& chmod 755 /usr/local/bin/paperless_cmd.sh \
|
&& chmod 755 /usr/local/bin/paperless_cmd.sh \
|
||||||
&& mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
|
&& mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
|
||||||
&& chmod 755 /usr/local/bin/flower-conditional.sh \
|
&& chmod 755 /usr/local/bin/flower-conditional.sh \
|
||||||
&& echo "Installing managment commands" \
|
&& echo "Installing management commands" \
|
||||||
&& chmod +x install_management_commands.sh \
|
&& chmod +x install_management_commands.sh \
|
||||||
&& ./install_management_commands.sh
|
&& ./install_management_commands.sh
|
||||||
|
|
||||||
|
4
Pipfile
4
Pipfile
@@ -7,7 +7,7 @@ name = "pypi"
|
|||||||
dateparser = "~=1.2"
|
dateparser = "~=1.2"
|
||||||
# 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 = "~=4.2.8"
|
django = "~=4.2.9"
|
||||||
django-auditlog = "*"
|
django-auditlog = "*"
|
||||||
django-celery-results = "*"
|
django-celery-results = "*"
|
||||||
django-compression-middleware = "*"
|
django-compression-middleware = "*"
|
||||||
@@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
|||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
# Linting
|
# Linting
|
||||||
black = "==23.11.0"
|
black = "*"
|
||||||
pre-commit = "*"
|
pre-commit = "*"
|
||||||
ruff = "*"
|
ruff = "*"
|
||||||
# Testing
|
# Testing
|
||||||
|
1620
Pipfile.lock
generated
1620
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
# Docker Compose file for running paperless testing with actual gotenberg
|
# Docker Compose file for running paperless testing with actual gotenberg
|
||||||
# and Tika containers for a more end to end test of the Tika related functionality
|
# and Tika containers for a more end to end test of the Tika related functionality
|
||||||
# Can be used locally or by the CI to start the nessecary containers with the
|
# Can be used locally or by the CI to start the necessary containers with the
|
||||||
# correct networking for the tests
|
# correct networking for the tests
|
||||||
|
|
||||||
version: "3.7"
|
version: "3.7"
|
||||||
|
@@ -60,11 +60,6 @@ services:
|
|||||||
- tika
|
- tika
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
|
@@ -54,11 +54,6 @@ services:
|
|||||||
- broker
|
- broker
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
@@ -73,7 +68,6 @@ services:
|
|||||||
PAPERLESS_DBPASS: paperless # only needed if non-default password
|
PAPERLESS_DBPASS: paperless # only needed if non-default password
|
||||||
PAPERLESS_DBPORT: 3306
|
PAPERLESS_DBPORT: 3306
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
# To install and update paperless with this file, do the following:
|
# To install and update paperless with this file, do the following:
|
||||||
#
|
#
|
||||||
# - Open portainer Stacks list and click 'Add stack'
|
# - Open portainer Stacks list and click 'Add stack'
|
||||||
# - Paste the contents of this file and assign a name, e.g. 'Paperless'
|
# - Paste the contents of this file and assign a name, e.g. 'paperless'
|
||||||
# - Click 'Deploy the stack' and wait for it to be deployed
|
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||||
# - Open the list of containers, select paperless_webserver_1
|
# - Open the list of containers, select paperless_webserver_1
|
||||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||||
@@ -54,11 +54,6 @@ services:
|
|||||||
- broker
|
- broker
|
||||||
ports:
|
ports:
|
||||||
- "8010:8000"
|
- "8010:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
|
@@ -58,11 +58,6 @@ services:
|
|||||||
- tika
|
- tika
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
|
@@ -52,11 +52,6 @@ services:
|
|||||||
- broker
|
- broker
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
@@ -67,7 +62,6 @@ services:
|
|||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
@@ -47,11 +47,6 @@ services:
|
|||||||
- tika
|
- tika
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
|
@@ -38,11 +38,6 @@ services:
|
|||||||
- broker
|
- broker
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/usr/src/paperless/data
|
- data:/usr/src/paperless/data
|
||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
@@ -52,7 +47,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
@@ -434,8 +434,10 @@ to view more detailed information about the health of the celery workers
|
|||||||
used for asynchronous tasks. This includes details on currently running,
|
used for asynchronous tasks. This includes details on currently running,
|
||||||
queued and completed tasks, timing and more. Flower can also be used
|
queued and completed tasks, timing and more. Flower can also be used
|
||||||
with Prometheus, as it exports metrics. For details on its capabilities,
|
with Prometheus, as it exports metrics. For details on its capabilities,
|
||||||
refer to the Flower documentation.
|
refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration/#PAPERLESS_ENABLE_FLOWER).
|
||||||
To configure Flower further, create a `flowerconfig.py` and
|
To configure Flower further, create a `flowerconfig.py` and
|
||||||
place it into the `src/paperless` directory. For a Docker
|
place it into the `src/paperless` directory. For a Docker
|
||||||
installation, you can use volumes to accomplish this:
|
installation, you can use volumes to accomplish this:
|
||||||
@@ -444,6 +446,8 @@ installation, you can use volumes to accomplish this:
|
|||||||
services:
|
services:
|
||||||
# ...
|
# ...
|
||||||
webserver:
|
webserver:
|
||||||
|
environment:
|
||||||
|
- PAPERLESS_ENABLE_FLOWER
|
||||||
ports:
|
ports:
|
||||||
- 5555:5555 # (2)!
|
- 5555:5555 # (2)!
|
||||||
# ...
|
# ...
|
||||||
@@ -452,7 +456,7 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
1. Note the `:ro` tag means the file will be mounted as read only.
|
1. Note the `:ro` tag means the file will be mounted as read only.
|
||||||
2. `flower` runs by default on port 5555, but this can be configured
|
2. By default, Flower runs on port 5555, but this can be configured.
|
||||||
|
|
||||||
## Custom Container Initialization
|
## Custom Container Initialization
|
||||||
|
|
||||||
@@ -613,7 +617,7 @@ scan a completely new "odd numbered pages" one. The old staging file will get di
|
|||||||
|
|
||||||
The collation feature can be used together with the [subdirs as tags](configuration.md#consume_config)
|
The collation feature can be used together with the [subdirs as tags](configuration.md#consume_config)
|
||||||
feature (but this is not a requirement). Just create a correctly named double-sided subdir
|
feature (but this is not a requirement). Just create a correctly named double-sided subdir
|
||||||
in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as
|
in the hierarchy and upload your scans there. For example, both `double-sided/foo/bar` as
|
||||||
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
|
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
|
||||||
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
||||||
|
|
||||||
|
@@ -1,5 +1,203 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 2.4.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Enhancement: support remote user auth directly against API (DRF) [@shamoon](https://github.com/shamoon) ([#5386](https://github.com/paperless-ngx/paperless-ngx/pull/5386))
|
||||||
|
- Feature: Add additional caching support to suggestions and metadata [@stumpylog](https://github.com/stumpylog) ([#5414](https://github.com/paperless-ngx/paperless-ngx/pull/5414))
|
||||||
|
- Feature: help tooltips [@shamoon](https://github.com/shamoon) ([#5383](https://github.com/paperless-ngx/paperless-ngx/pull/5383))
|
||||||
|
- Enhancement: warn when outdated doc detected [@shamoon](https://github.com/shamoon) ([#5372](https://github.com/paperless-ngx/paperless-ngx/pull/5372))
|
||||||
|
- Feature: app branding [@shamoon](https://github.com/shamoon) ([#5357](https://github.com/paperless-ngx/paperless-ngx/pull/5357))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: doc link removal when has never been assigned [@shamoon](https://github.com/shamoon) ([#5451](https://github.com/paperless-ngx/paperless-ngx/pull/5451))
|
||||||
|
- Fix: dont lose permissions ui if owner changed from [@shamoon](https://github.com/shamoon) ([#5433](https://github.com/paperless-ngx/paperless-ngx/pull/5433))
|
||||||
|
- Fix: Getting next ASN when no documents have an ASN [@stumpylog](https://github.com/stumpylog) ([#5431](https://github.com/paperless-ngx/paperless-ngx/pull/5431))
|
||||||
|
- Fix: signin username floating label [@shamoon](https://github.com/shamoon) ([#5424](https://github.com/paperless-ngx/paperless-ngx/pull/5424))
|
||||||
|
- Fix: shared by me filter with multiple users / groups in postgres [@shamoon](https://github.com/shamoon) ([#5396](https://github.com/paperless-ngx/paperless-ngx/pull/5396))
|
||||||
|
- Fix: Catch new warning when loading the classifier [@stumpylog](https://github.com/stumpylog) ([#5395](https://github.com/paperless-ngx/paperless-ngx/pull/5395))
|
||||||
|
- Fix: doc detail component fixes [@shamoon](https://github.com/shamoon) ([#5373](https://github.com/paperless-ngx/paperless-ngx/pull/5373))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore: better bootstrap icons [@shamoon](https://github.com/shamoon) ([#5403](https://github.com/paperless-ngx/paperless-ngx/pull/5403))
|
||||||
|
- Chore: Close outdated support / general discussions [@shamoon](https://github.com/shamoon) ([#5443](https://github.com/paperless-ngx/paperless-ngx/pull/5443))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#5413](https://github.com/paperless-ngx/paperless-ngx/pull/5413))
|
||||||
|
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5412](https://github.com/paperless-ngx/paperless-ngx/pull/5412))
|
||||||
|
- Chore(deps-dev): Bump jinja2 from 3.1.2 to 3.1.3 [@dependabot](https://github.com/dependabot) ([#5352](https://github.com/paperless-ngx/paperless-ngx/pull/5352))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>16 changes</summary>
|
||||||
|
|
||||||
|
- Fix: doc link removal when has never been assigned [@shamoon](https://github.com/shamoon) ([#5451](https://github.com/paperless-ngx/paperless-ngx/pull/5451))
|
||||||
|
- Chore: better bootstrap icons [@shamoon](https://github.com/shamoon) ([#5403](https://github.com/paperless-ngx/paperless-ngx/pull/5403))
|
||||||
|
- Fix: dont lose permissions ui if owner changed from [@shamoon](https://github.com/shamoon) ([#5433](https://github.com/paperless-ngx/paperless-ngx/pull/5433))
|
||||||
|
- Enhancement: support remote user auth directly against API (DRF) [@shamoon](https://github.com/shamoon) ([#5386](https://github.com/paperless-ngx/paperless-ngx/pull/5386))
|
||||||
|
- Fix: Getting next ASN when no documents have an ASN [@stumpylog](https://github.com/stumpylog) ([#5431](https://github.com/paperless-ngx/paperless-ngx/pull/5431))
|
||||||
|
- Feature: Add additional caching support to suggestions and metadata [@stumpylog](https://github.com/stumpylog) ([#5414](https://github.com/paperless-ngx/paperless-ngx/pull/5414))
|
||||||
|
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#5413](https://github.com/paperless-ngx/paperless-ngx/pull/5413))
|
||||||
|
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5412](https://github.com/paperless-ngx/paperless-ngx/pull/5412))
|
||||||
|
- Fix: signin username floating label [@shamoon](https://github.com/shamoon) ([#5424](https://github.com/paperless-ngx/paperless-ngx/pull/5424))
|
||||||
|
- Feature: help tooltips [@shamoon](https://github.com/shamoon) ([#5383](https://github.com/paperless-ngx/paperless-ngx/pull/5383))
|
||||||
|
- Enhancement / QoL: show selected tasks count [@shamoon](https://github.com/shamoon) ([#5379](https://github.com/paperless-ngx/paperless-ngx/pull/5379))
|
||||||
|
- Fix: shared by me filter with multiple users / groups in postgres [@shamoon](https://github.com/shamoon) ([#5396](https://github.com/paperless-ngx/paperless-ngx/pull/5396))
|
||||||
|
- Fix: doc detail component fixes [@shamoon](https://github.com/shamoon) ([#5373](https://github.com/paperless-ngx/paperless-ngx/pull/5373))
|
||||||
|
- Enhancement: warn when outdated doc detected [@shamoon](https://github.com/shamoon) ([#5372](https://github.com/paperless-ngx/paperless-ngx/pull/5372))
|
||||||
|
- Feature: app branding [@shamoon](https://github.com/shamoon) ([#5357](https://github.com/paperless-ngx/paperless-ngx/pull/5357))
|
||||||
|
- Chore: Initial refactor of consume task [@stumpylog](https://github.com/stumpylog) ([#5367](https://github.com/paperless-ngx/paperless-ngx/pull/5367))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.3.3
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- Enhancement: Explain behavior of unset app config boolean to user [@shamoon](https://github.com/shamoon) ([#5345](https://github.com/paperless-ngx/paperless-ngx/pull/5345))
|
||||||
|
- Enhancement: title assignment placeholder error handling, fallback [@shamoon](https://github.com/shamoon) ([#5282](https://github.com/paperless-ngx/paperless-ngx/pull/5282))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: Don't require the JSON user arguments field, interpret empty string as [@stumpylog](https://github.com/stumpylog) ([#5320](https://github.com/paperless-ngx/paperless-ngx/pull/5320))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5336](https://github.com/paperless-ngx/paperless-ngx/pull/5336))
|
||||||
|
- Chore: add pre-commit hook for codespell [@shamoon](https://github.com/shamoon) ([#5324](https://github.com/paperless-ngx/paperless-ngx/pull/5324))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>5 changes</summary>
|
||||||
|
|
||||||
|
- Enhancement: Explain behavior of unset app config boolean to user [@shamoon](https://github.com/shamoon) ([#5345](https://github.com/paperless-ngx/paperless-ngx/pull/5345))
|
||||||
|
- Enhancement: title assignment placeholder error handling, fallback [@shamoon](https://github.com/shamoon) ([#5282](https://github.com/paperless-ngx/paperless-ngx/pull/5282))
|
||||||
|
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5336](https://github.com/paperless-ngx/paperless-ngx/pull/5336))
|
||||||
|
- Fix: Don't require the JSON user arguments field, interpret empty string as [@stumpylog](https://github.com/stumpylog) ([#5320](https://github.com/paperless-ngx/paperless-ngx/pull/5320))
|
||||||
|
- Chore: add pre-commit hook for codespell [@shamoon](https://github.com/shamoon) ([#5324](https://github.com/paperless-ngx/paperless-ngx/pull/5324))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.3.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
|
||||||
|
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
|
||||||
|
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
|
||||||
|
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
|
||||||
|
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>5 changes</summary>
|
||||||
|
|
||||||
|
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
|
||||||
|
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
|
||||||
|
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
|
||||||
|
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
|
||||||
|
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.3.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
|
||||||
|
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>2 changes</summary>
|
||||||
|
|
||||||
|
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
|
||||||
|
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.3.0
|
||||||
|
|
||||||
|
### Notable Changes
|
||||||
|
|
||||||
|
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
|
||||||
|
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
|
||||||
|
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
|
||||||
|
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
|
||||||
|
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
|
||||||
|
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
|
||||||
|
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
|
||||||
|
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
|
||||||
|
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
|
||||||
|
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
|
||||||
|
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
|
||||||
|
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
|
||||||
|
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
|
||||||
|
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
|
||||||
|
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
|
||||||
|
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
|
||||||
|
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
|
||||||
|
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>4 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>21 changes</summary>
|
||||||
|
|
||||||
|
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
|
||||||
|
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
|
||||||
|
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
|
||||||
|
- Enhancement: add basic filters for listing of custom fields [@shamoon](https://github.com/shamoon) ([#5257](https://github.com/paperless-ngx/paperless-ngx/pull/5257))
|
||||||
|
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
|
||||||
|
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
|
||||||
|
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
|
||||||
|
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
|
||||||
|
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
|
||||||
|
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
|
||||||
|
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
|
||||||
|
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
|
||||||
|
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
|
||||||
|
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
|
||||||
|
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
|
||||||
|
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
|
||||||
|
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
|
||||||
|
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 2.2.1
|
## paperless-ngx 2.2.1
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@@ -311,7 +509,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
|||||||
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
|
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
|
||||||
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
|
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
|
||||||
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
||||||
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
- Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||||
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
|
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
|
||||||
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
|
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
|
||||||
@@ -521,7 +719,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
|||||||
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
||||||
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
|
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
|
||||||
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
|
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
|
||||||
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
- Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||||
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
|
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
|
||||||
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
|
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
|
||||||
@@ -553,11 +751,11 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
- Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
||||||
|
|
||||||
### All App Changes
|
### All App Changes
|
||||||
|
|
||||||
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
- Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
||||||
|
|
||||||
## paperless-ngx 1.17.3
|
## paperless-ngx 1.17.3
|
||||||
|
|
||||||
@@ -1224,7 +1422,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
|||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- Whitespace changes, making sure the example is correcly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
|
- Whitespace changes, making sure the example is correctly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
|
||||||
- Docs: Include additional information about barcodes [@stumpylog](https://github.com/stumpylog) ([#2889](https://github.com/paperless-ngx/paperless-ngx/pull/2889))
|
- Docs: Include additional information about barcodes [@stumpylog](https://github.com/stumpylog) ([#2889](https://github.com/paperless-ngx/paperless-ngx/pull/2889))
|
||||||
- Fix formatting in Setup documentation page [@igrybkov](https://github.com/igrybkov) ([#2880](https://github.com/paperless-ngx/paperless-ngx/pull/2880))
|
- Fix formatting in Setup documentation page [@igrybkov](https://github.com/igrybkov) ([#2880](https://github.com/paperless-ngx/paperless-ngx/pull/2880))
|
||||||
- [Documentation] Update docker-compose steps to support podman [@white-gecko](https://github.com/white-gecko) ([#2855](https://github.com/paperless-ngx/paperless-ngx/pull/2855))
|
- [Documentation] Update docker-compose steps to support podman [@white-gecko](https://github.com/white-gecko) ([#2855](https://github.com/paperless-ngx/paperless-ngx/pull/2855))
|
||||||
@@ -1279,7 +1477,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
|||||||
- Fix: update PaperlessTask on hard failures [@shamoon](https://github.com/shamoon) ([#3062](https://github.com/paperless-ngx/paperless-ngx/pull/3062))
|
- Fix: update PaperlessTask on hard failures [@shamoon](https://github.com/shamoon) ([#3062](https://github.com/paperless-ngx/paperless-ngx/pull/3062))
|
||||||
- Bump typescript from 4.8.4 to 4.9.5 in /src-ui [@dependabot](https://github.com/dependabot) ([#3071](https://github.com/paperless-ngx/paperless-ngx/pull/3071))
|
- Bump typescript from 4.8.4 to 4.9.5 in /src-ui [@dependabot](https://github.com/dependabot) ([#3071](https://github.com/paperless-ngx/paperless-ngx/pull/3071))
|
||||||
- Bulk Bump npm packages 04.23 [@dependabot](https://github.com/dependabot) ([#3068](https://github.com/paperless-ngx/paperless-ngx/pull/3068))
|
- Bulk Bump npm packages 04.23 [@dependabot](https://github.com/dependabot) ([#3068](https://github.com/paperless-ngx/paperless-ngx/pull/3068))
|
||||||
- Fix: Hide UI tour steps if user doesnt have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
|
- Fix: Hide UI tour steps if user doesn't have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
|
||||||
- Fix: Hide Permissions tab if user cannot view users [@shamoon](https://github.com/shamoon) ([#3061](https://github.com/paperless-ngx/paperless-ngx/pull/3061))
|
- Fix: Hide Permissions tab if user cannot view users [@shamoon](https://github.com/shamoon) ([#3061](https://github.com/paperless-ngx/paperless-ngx/pull/3061))
|
||||||
- v1.14.0 delete document fixes [@shamoon](https://github.com/shamoon) ([#3020](https://github.com/paperless-ngx/paperless-ngx/pull/3020))
|
- v1.14.0 delete document fixes [@shamoon](https://github.com/shamoon) ([#3020](https://github.com/paperless-ngx/paperless-ngx/pull/3020))
|
||||||
- Bump wait-on from 6.0.1 to 7.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#2990](https://github.com/paperless-ngx/paperless-ngx/pull/2990))
|
- Bump wait-on from 6.0.1 to 7.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#2990](https://github.com/paperless-ngx/paperless-ngx/pull/2990))
|
||||||
@@ -1484,7 +1682,7 @@ older comments. The Docker image will automatically perform this reindex, bare m
|
|||||||
- [Docs] Add Paperless Mobile app to docs [@astubenbord](https://github.com/astubenbord) ([#2378](https://github.com/paperless-ngx/paperless-ngx/pull/2378))
|
- [Docs] Add Paperless Mobile app to docs [@astubenbord](https://github.com/astubenbord) ([#2378](https://github.com/paperless-ngx/paperless-ngx/pull/2378))
|
||||||
- Tiny spelling change [@veverkap](https://github.com/veverkap) ([#2369](https://github.com/paperless-ngx/paperless-ngx/pull/2369))
|
- Tiny spelling change [@veverkap](https://github.com/veverkap) ([#2369](https://github.com/paperless-ngx/paperless-ngx/pull/2369))
|
||||||
- Documentation: update build instructions to remove deprecated [@shamoon](https://github.com/shamoon) ([#2334](https://github.com/paperless-ngx/paperless-ngx/pull/2334))
|
- Documentation: update build instructions to remove deprecated [@shamoon](https://github.com/shamoon) ([#2334](https://github.com/paperless-ngx/paperless-ngx/pull/2334))
|
||||||
- [Documentation] Add note that PAPERLESS_URL cant contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
|
- [Documentation] Add note that PAPERLESS_URL can't contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
|
||||||
- [Documentation] Add v1.11.3 changelog [@github-actions](https://github.com/github-actions) ([#2311](https://github.com/paperless-ngx/paperless-ngx/pull/2311))
|
- [Documentation] Add v1.11.3 changelog [@github-actions](https://github.com/github-actions) ([#2311](https://github.com/paperless-ngx/paperless-ngx/pull/2311))
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
@@ -1815,7 +2013,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
|||||||
|
|
||||||
### All App Changes
|
### All App Changes
|
||||||
|
|
||||||
- Add info that re-do OCR doesnt automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
|
- Add info that re-do OCR doesn't automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
|
||||||
- Bugfix: Fix created_date being a string [@stumpylog](https://github.com/stumpylog) ([#2023](https://github.com/paperless-ngx/paperless-ngx/pull/2023))
|
- Bugfix: Fix created_date being a string [@stumpylog](https://github.com/stumpylog) ([#2023](https://github.com/paperless-ngx/paperless-ngx/pull/2023))
|
||||||
- Bugfix: Fixes an issue with mixed text and images when redoing OCR [@stumpylog](https://github.com/stumpylog) ([#2017](https://github.com/paperless-ngx/paperless-ngx/pull/2017))
|
- Bugfix: Fixes an issue with mixed text and images when redoing OCR [@stumpylog](https://github.com/stumpylog) ([#2017](https://github.com/paperless-ngx/paperless-ngx/pull/2017))
|
||||||
- Bugfix: Don't allow exceptions during date parsing to fail consume [@stumpylog](https://github.com/stumpylog) ([#1998](https://github.com/paperless-ngx/paperless-ngx/pull/1998))
|
- Bugfix: Don't allow exceptions during date parsing to fail consume [@stumpylog](https://github.com/stumpylog) ([#1998](https://github.com/paperless-ngx/paperless-ngx/pull/1998))
|
||||||
@@ -2226,7 +2424,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
|||||||
- Fix local Docker image building [\@stumpylog](https://github.com/stumpylog) ([\#849](https://github.com/paperless-ngx/paperless-ngx/pull/849))
|
- Fix local Docker image building [\@stumpylog](https://github.com/stumpylog) ([\#849](https://github.com/paperless-ngx/paperless-ngx/pull/849))
|
||||||
- Fix: show errors on invalid date input [\@shamoon](https://github.com/shamoon) ([\#862](https://github.com/paperless-ngx/paperless-ngx/pull/862))
|
- Fix: show errors on invalid date input [\@shamoon](https://github.com/shamoon) ([\#862](https://github.com/paperless-ngx/paperless-ngx/pull/862))
|
||||||
- Fix: Older dates do not display on frontend [\@shamoon](https://github.com/shamoon) ([\#852](https://github.com/paperless-ngx/paperless-ngx/pull/852))
|
- Fix: Older dates do not display on frontend [\@shamoon](https://github.com/shamoon) ([\#852](https://github.com/paperless-ngx/paperless-ngx/pull/852))
|
||||||
- Fixes IMAP UTF8 Authenication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
|
- Fixes IMAP UTF8 Authentication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
|
||||||
- Fix password field remains visible [\@shamoon](https://github.com/shamoon) ([\#840](https://github.com/paperless-ngx/paperless-ngx/pull/840))
|
- Fix password field remains visible [\@shamoon](https://github.com/shamoon) ([\#840](https://github.com/paperless-ngx/paperless-ngx/pull/840))
|
||||||
- Fixes Pillow build for armv7 [\@stumpylog](https://github.com/stumpylog) ([\#815](https://github.com/paperless-ngx/paperless-ngx/pull/815))
|
- Fixes Pillow build for armv7 [\@stumpylog](https://github.com/stumpylog) ([\#815](https://github.com/paperless-ngx/paperless-ngx/pull/815))
|
||||||
- Update frontend localization source file [\@shamoon](https://github.com/shamoon) ([\#814](https://github.com/paperless-ngx/paperless-ngx/pull/814))
|
- Update frontend localization source file [\@shamoon](https://github.com/shamoon) ([\#814](https://github.com/paperless-ngx/paperless-ngx/pull/814))
|
||||||
@@ -2347,7 +2545,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
|||||||
[\@shamoon](https://github.com/shamoon) ([\#313](https://github.com/paperless-ngx/paperless-ngx/pull/313))
|
[\@shamoon](https://github.com/shamoon) ([\#313](https://github.com/paperless-ngx/paperless-ngx/pull/313))
|
||||||
- Fix imap tools bug [\@stumpylog](https://github.com/stumpylog)
|
- Fix imap tools bug [\@stumpylog](https://github.com/stumpylog)
|
||||||
([\#393](https://github.com/paperless-ngx/paperless-ngx/pull/393))
|
([\#393](https://github.com/paperless-ngx/paperless-ngx/pull/393))
|
||||||
- Fix filterable dropdown buttons arent translated
|
- Fix filterable dropdown buttons aren't translated
|
||||||
[\@shamoon](https://github.com/shamoon) ([\#366](https://github.com/paperless-ngx/paperless-ngx/pull/366))
|
[\@shamoon](https://github.com/shamoon) ([\#366](https://github.com/paperless-ngx/paperless-ngx/pull/366))
|
||||||
- Fix 224: "Auto-detected date is day before receipt date"
|
- Fix 224: "Auto-detected date is day before receipt date"
|
||||||
[\@a17t](https://github.com/a17t) ([\#246](https://github.com/paperless-ngx/paperless-ngx/pull/246))
|
[\@a17t](https://github.com/a17t) ([\#246](https://github.com/paperless-ngx/paperless-ngx/pull/246))
|
||||||
@@ -3183,7 +3381,7 @@ primarily.
|
|||||||
[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR
|
[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR
|
||||||
on documents. It still uses tesseract under the hood, but the
|
on documents. It still uses tesseract under the hood, but the
|
||||||
PDF parser of Paperless has changed considerably and will behave
|
PDF parser of Paperless has changed considerably and will behave
|
||||||
different for some douments.
|
different for some documents.
|
||||||
- OCRmyPDF creates archived PDF/A documents with embedded text
|
- OCRmyPDF creates archived PDF/A documents with embedded text
|
||||||
that can be selected in the front end.
|
that can be selected in the front end.
|
||||||
- Paperless stores archived versions of documents alongside with
|
- Paperless stores archived versions of documents alongside with
|
||||||
@@ -3234,7 +3432,7 @@ primarily.
|
|||||||
crash.
|
crash.
|
||||||
- Mail handling no longer exits entirely when encountering errors.
|
- Mail handling no longer exits entirely when encountering errors.
|
||||||
It will skip the account/rule/message on which the error
|
It will skip the account/rule/message on which the error
|
||||||
occured.
|
occurred.
|
||||||
- Assigning correspondents from mail sender names failed for very
|
- Assigning correspondents from mail sender names failed for very
|
||||||
long names. Paperless no longer assigns correspondents in these
|
long names. Paperless no longer assigns correspondents in these
|
||||||
cases.
|
cases.
|
||||||
|
@@ -4,9 +4,9 @@ Paperless provides a wide range of customizations. Depending on how you
|
|||||||
run paperless, these settings have to be defined in different places.
|
run paperless, these settings have to be defined in different places.
|
||||||
|
|
||||||
Certain configuration options may be set via the UI. This currently includes
|
Certain configuration options may be set via the UI. This currently includes
|
||||||
common [OCR](#ocr) related settings. If set, these will take preference over the
|
common [OCR](#ocr) related settings and some frontend settings. If set, these will take
|
||||||
settings via environment variables. If not set, the environment setting or applicable
|
preference over the settings via environment variables. If not set, the environment setting
|
||||||
default will be utilized instead.
|
or applicable default will be utilized instead.
|
||||||
|
|
||||||
- If you run paperless on docker, `paperless.conf` is not used.
|
- If you run paperless on docker, `paperless.conf` is not used.
|
||||||
Rather, configure paperless by copying necessary options to
|
Rather, configure paperless by copying necessary options to
|
||||||
@@ -665,11 +665,13 @@ completely.
|
|||||||
|
|
||||||
Specifying 1 here will only use the first page.
|
Specifying 1 here will only use the first page.
|
||||||
|
|
||||||
|
The value must be greater than or equal to 1 to be used.
|
||||||
|
|
||||||
When combined with `PAPERLESS_OCR_MODE=redo` or
|
When combined with `PAPERLESS_OCR_MODE=redo` or
|
||||||
`PAPERLESS_OCR_MODE=force`, paperless will not modify any text it
|
`PAPERLESS_OCR_MODE=force`, paperless will not modify any text it
|
||||||
finds on excluded pages and copy it verbatim.
|
finds on excluded pages and copy it verbatim.
|
||||||
|
|
||||||
Defaults to 0, which disables this feature and always uses all
|
Defaults to unset, which disables this feature and always uses all
|
||||||
pages.
|
pages.
|
||||||
|
|
||||||
#### [`PAPERLESS_OCR_IMAGE_DPI=<num>`](#PAPERLESS_OCR_IMAGE_DPI) {#PAPERLESS_OCR_IMAGE_DPI}
|
#### [`PAPERLESS_OCR_IMAGE_DPI=<num>`](#PAPERLESS_OCR_IMAGE_DPI) {#PAPERLESS_OCR_IMAGE_DPI}
|
||||||
@@ -683,7 +685,7 @@ fails, it uses this value as a fallback.
|
|||||||
|
|
||||||
Set this to the DPI your scanner produces images at.
|
Set this to the DPI your scanner produces images at.
|
||||||
|
|
||||||
Default is none, which will automatically calculate image DPI so
|
Defaults to unset, which will automatically calculate image DPI so
|
||||||
that the produced PDF documents are A4 sized.
|
that the produced PDF documents are A4 sized.
|
||||||
|
|
||||||
#### [`PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>`](#PAPERLESS_OCR_MAX_IMAGE_PIXELS) {#PAPERLESS_OCR_MAX_IMAGE_PIXELS}
|
#### [`PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>`](#PAPERLESS_OCR_MAX_IMAGE_PIXELS) {#PAPERLESS_OCR_MAX_IMAGE_PIXELS}
|
||||||
@@ -1327,7 +1329,15 @@ started by the container.
|
|||||||
|
|
||||||
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
|
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
|
||||||
|
|
||||||
## Update Checking {#update-checking}
|
## Frontend Settings
|
||||||
|
|
||||||
|
#### [`PAPERLESS_APP_TITLE=<bool>`](#PAPERLESS_APP_TITLE) {#PAPERLESS_APP_TITLE}
|
||||||
|
|
||||||
|
: If set, overrides the default name "Paperless-ngx"
|
||||||
|
|
||||||
|
#### [`PAPERLESS_APP_LOGO=<path>`](#PAPERLESS_APP_LOGO) {#PAPERLESS_APP_LOGO}
|
||||||
|
|
||||||
|
: Path to an image file in the /media/logo directory, must include 'logo', e.g. `/logo/Atari_logo.svg`
|
||||||
|
|
||||||
#### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK}
|
#### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK}
|
||||||
|
|
||||||
|
@@ -96,7 +96,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
Don't change the part after the colon or paperless wont find your
|
Don't change the part after the colon or paperless won't find your
|
||||||
documents.
|
documents.
|
||||||
|
|
||||||
You may also need to change the default port that the webserver will
|
You may also need to change the default port that the webserver will
|
||||||
|
@@ -138,7 +138,7 @@ command:
|
|||||||
You might encounter errors such as:
|
You might encounter errors such as:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
The following error occured while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
|
The following error occurred while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
|
||||||
```
|
```
|
||||||
|
|
||||||
This happens when paperless does not have permission to delete files
|
This happens when paperless does not have permission to delete files
|
||||||
|
@@ -149,7 +149,7 @@ different means. These are as follows:
|
|||||||
- **Flag:** Sets the 'important' flag on mails with consumed
|
- **Flag:** Sets the 'important' flag on mails with consumed
|
||||||
documents. Paperless will not consume flagged mails.
|
documents. Paperless will not consume flagged mails.
|
||||||
- **Move to folder:** Moves consumed mails out of the way so that
|
- **Move to folder:** Moves consumed mails out of the way so that
|
||||||
paperless wont consume them again.
|
paperless won't consume them again.
|
||||||
- **Add custom Tag:** Adds a custom tag to mails with consumed
|
- **Add custom Tag:** Adds a custom tag to mails with consumed
|
||||||
documents (the IMAP standard calls these "keywords"). Paperless
|
documents (the IMAP standard calls these "keywords"). Paperless
|
||||||
will not consume mails already tagged. Not all mail servers support
|
will not consume mails already tagged. Not all mail servers support
|
||||||
@@ -411,7 +411,7 @@ The following custom field types are supported:
|
|||||||
|
|
||||||
## Share Links
|
## Share Links
|
||||||
|
|
||||||
Paperless-ngx added the abiltiy to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
||||||
|
|
||||||
- Share links do not require a user to login and thus link directly to a file.
|
- Share links do not require a user to login and thus link directly to a file.
|
||||||
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
||||||
|
@@ -315,7 +315,7 @@ fi
|
|||||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
||||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
|
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
|
||||||
|
|
||||||
SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' < /dev/urandom | head --bytes 64)
|
SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' < /dev/urandom | dd bs=1 count=64 2>/dev/null)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
||||||
|
@@ -131,7 +131,7 @@ test('sorting', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Notes' }).click()
|
await page.getByRole('button', { name: 'Notes' }).click()
|
||||||
await expect(page).toHaveURL(/sort=num_notes/)
|
await expect(page).toHaveURL(/sort=num_notes/)
|
||||||
await page.getByRole('button', { name: 'Sort' }).click()
|
await page.getByRole('button', { name: 'Sort' }).click()
|
||||||
await page.locator('.w-100 > label > .toolbaricon').first().click()
|
await page.locator('.w-100 > label > i-bs').first().click()
|
||||||
await expect(page).not.toHaveURL(/reverse=1/)
|
await expect(page).not.toHaveURL(/reverse=1/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
1495
src-ui/messages.xlf
1495
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
17
src-ui/package-lock.json
generated
17
src-ui/package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^9.0.0",
|
"ngx-color": "^9.0.0",
|
||||||
"ngx-cookie-service": "^17.0.1",
|
"ngx-cookie-service": "^17.0.1",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
@@ -13850,6 +13851,22 @@
|
|||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ngx-bootstrap-icons": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngx-bootstrap-icons/-/ngx-bootstrap-icons-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.18.1",
|
||||||
|
"npm": ">= 8.11.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">= 13.3.8",
|
||||||
|
"@angular/core": ">= 13.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ngx-color": {
|
"node_modules/ngx-color": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",
|
||||||
|
@@ -27,6 +27,7 @@
|
|||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^9.0.0",
|
"ngx-color": "^9.0.0",
|
||||||
"ngx-cookie-service": "^17.0.1",
|
"ngx-cookie-service": "^17.0.1",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
|
@@ -186,8 +186,8 @@ export const routes: Routes = [
|
|||||||
canActivate: [PermissionsGuard],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
requiredPermission: {
|
requiredPermission: {
|
||||||
action: PermissionAction.View,
|
action: PermissionAction.Change,
|
||||||
type: PermissionType.Admin,
|
type: PermissionType.AppConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -110,6 +110,175 @@ import { DocumentLinkComponent } from './components/common/input/document-link/d
|
|||||||
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
||||||
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
||||||
import { ConfigComponent } from './components/admin/config/config.component'
|
import { ConfigComponent } from './components/admin/config/config.component'
|
||||||
|
import { FileComponent } from './components/common/input/file/file.component'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import {
|
||||||
|
archive,
|
||||||
|
arrowCounterclockwise,
|
||||||
|
arrowDown,
|
||||||
|
arrowLeft,
|
||||||
|
arrowRepeat,
|
||||||
|
arrowRight,
|
||||||
|
arrowRightShort,
|
||||||
|
arrowUpRight,
|
||||||
|
asterisk,
|
||||||
|
boxArrowUp,
|
||||||
|
boxArrowUpRight,
|
||||||
|
boxes,
|
||||||
|
calendar,
|
||||||
|
calendarEvent,
|
||||||
|
caretDown,
|
||||||
|
caretUp,
|
||||||
|
chatLeftText,
|
||||||
|
check,
|
||||||
|
check2All,
|
||||||
|
checkAll,
|
||||||
|
checkLg,
|
||||||
|
chevronDoubleLeft,
|
||||||
|
chevronDoubleRight,
|
||||||
|
clipboard,
|
||||||
|
clipboardCheckFill,
|
||||||
|
clipboardFill,
|
||||||
|
dash,
|
||||||
|
diagram3,
|
||||||
|
dice5,
|
||||||
|
doorOpen,
|
||||||
|
download,
|
||||||
|
envelope,
|
||||||
|
exclamationTriangle,
|
||||||
|
eye,
|
||||||
|
fileEarmark,
|
||||||
|
fileEarmarkCheck,
|
||||||
|
fileEarmarkFill,
|
||||||
|
fileEarmarkLock,
|
||||||
|
files,
|
||||||
|
fileText,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
folderFill,
|
||||||
|
funnel,
|
||||||
|
gear,
|
||||||
|
grid,
|
||||||
|
gripVertical,
|
||||||
|
hash,
|
||||||
|
hddStack,
|
||||||
|
house,
|
||||||
|
infoCircle,
|
||||||
|
link,
|
||||||
|
listTask,
|
||||||
|
listUl,
|
||||||
|
pencil,
|
||||||
|
people,
|
||||||
|
peopleFill,
|
||||||
|
person,
|
||||||
|
personCircle,
|
||||||
|
personFill,
|
||||||
|
personFillLock,
|
||||||
|
personLock,
|
||||||
|
plus,
|
||||||
|
plusCircle,
|
||||||
|
questionCircle,
|
||||||
|
search,
|
||||||
|
slashCircle,
|
||||||
|
sliders2Vertical,
|
||||||
|
sortAlphaDown,
|
||||||
|
sortAlphaUpAlt,
|
||||||
|
tagFill,
|
||||||
|
tags,
|
||||||
|
textIndentLeft,
|
||||||
|
textLeft,
|
||||||
|
threeDots,
|
||||||
|
threeDotsVertical,
|
||||||
|
trash,
|
||||||
|
uiRadios,
|
||||||
|
upcScan,
|
||||||
|
x,
|
||||||
|
xLg,
|
||||||
|
} from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
archive,
|
||||||
|
arrowCounterclockwise,
|
||||||
|
arrowDown,
|
||||||
|
arrowLeft,
|
||||||
|
arrowRepeat,
|
||||||
|
arrowRight,
|
||||||
|
arrowRightShort,
|
||||||
|
arrowUpRight,
|
||||||
|
asterisk,
|
||||||
|
boxArrowUp,
|
||||||
|
boxArrowUpRight,
|
||||||
|
boxes,
|
||||||
|
calendar,
|
||||||
|
calendarEvent,
|
||||||
|
caretDown,
|
||||||
|
caretUp,
|
||||||
|
chatLeftText,
|
||||||
|
check,
|
||||||
|
check2All,
|
||||||
|
checkAll,
|
||||||
|
checkLg,
|
||||||
|
chevronDoubleLeft,
|
||||||
|
chevronDoubleRight,
|
||||||
|
clipboard,
|
||||||
|
clipboardCheckFill,
|
||||||
|
clipboardFill,
|
||||||
|
dash,
|
||||||
|
diagram3,
|
||||||
|
dice5,
|
||||||
|
doorOpen,
|
||||||
|
download,
|
||||||
|
envelope,
|
||||||
|
exclamationTriangle,
|
||||||
|
eye,
|
||||||
|
fileEarmark,
|
||||||
|
fileEarmarkCheck,
|
||||||
|
fileEarmarkFill,
|
||||||
|
fileEarmarkLock,
|
||||||
|
files,
|
||||||
|
fileText,
|
||||||
|
filter,
|
||||||
|
folder,
|
||||||
|
folderFill,
|
||||||
|
funnel,
|
||||||
|
gear,
|
||||||
|
grid,
|
||||||
|
gripVertical,
|
||||||
|
hash,
|
||||||
|
hddStack,
|
||||||
|
house,
|
||||||
|
infoCircle,
|
||||||
|
link,
|
||||||
|
listTask,
|
||||||
|
listUl,
|
||||||
|
pencil,
|
||||||
|
people,
|
||||||
|
peopleFill,
|
||||||
|
person,
|
||||||
|
personCircle,
|
||||||
|
personFill,
|
||||||
|
personFillLock,
|
||||||
|
personLock,
|
||||||
|
plus,
|
||||||
|
plusCircle,
|
||||||
|
questionCircle,
|
||||||
|
search,
|
||||||
|
slashCircle,
|
||||||
|
sliders2Vertical,
|
||||||
|
sortAlphaDown,
|
||||||
|
sortAlphaUpAlt,
|
||||||
|
tagFill,
|
||||||
|
tags,
|
||||||
|
textIndentLeft,
|
||||||
|
textLeft,
|
||||||
|
threeDots,
|
||||||
|
threeDotsVertical,
|
||||||
|
trash,
|
||||||
|
uiRadios,
|
||||||
|
upcScan,
|
||||||
|
x,
|
||||||
|
xLg,
|
||||||
|
}
|
||||||
|
|
||||||
import localeAf from '@angular/common/locales/af'
|
import localeAf from '@angular/common/locales/af'
|
||||||
import localeAr from '@angular/common/locales/ar'
|
import localeAr from '@angular/common/locales/ar'
|
||||||
@@ -267,6 +436,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
PreviewPopupComponent,
|
PreviewPopupComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
ConfigComponent,
|
ConfigComponent,
|
||||||
|
FileComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -280,6 +450,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
ColorSliderModule,
|
ColorSliderModule,
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrapModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
|
NgxBootstrapIconsModule.pick(icons),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@@ -1,4 +1,10 @@
|
|||||||
<pngx-page-header title="Configuration" i18n-title></pngx-page-header>
|
<pngx-page-header
|
||||||
|
title="Application Configuration"
|
||||||
|
i18n-title
|
||||||
|
info="Global app configuration options which apply to <strong>every</strong> user of this install of Paperless-ngx. Options can also be set using environment variables or the configuration file but the value here will always take precedence."
|
||||||
|
i18n-info
|
||||||
|
infoLink="configuration">
|
||||||
|
</pngx-page-header>
|
||||||
|
|
||||||
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">
|
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">
|
||||||
|
|
||||||
@@ -17,9 +23,7 @@
|
|||||||
<h6>
|
<h6>
|
||||||
{{option.title}}
|
{{option.title}}
|
||||||
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="info-circle"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,9 +31,10 @@
|
|||||||
@switch (option.type) {
|
@switch (option.type) {
|
||||||
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
|
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
|
||||||
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
|
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
|
||||||
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
|
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
|
||||||
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||||
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||||
|
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -15,12 +15,16 @@ import { SwitchComponent } from '../../common/input/switch/switch.component'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
|
import { FileComponent } from '../../common/input/file/file.component'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('ConfigComponent', () => {
|
describe('ConfigComponent', () => {
|
||||||
let component: ConfigComponent
|
let component: ConfigComponent
|
||||||
let fixture: ComponentFixture<ConfigComponent>
|
let fixture: ComponentFixture<ConfigComponent>
|
||||||
let configService: ConfigService
|
let configService: ConfigService
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
|
let settingService: SettingsService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -30,6 +34,7 @@ describe('ConfigComponent', () => {
|
|||||||
SelectComponent,
|
SelectComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
|
FileComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@@ -39,11 +44,13 @@ describe('ConfigComponent', () => {
|
|||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
configService = TestBed.inject(ConfigService)
|
configService = TestBed.inject(ConfigService)
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
|
settingService = TestBed.inject(SettingsService)
|
||||||
fixture = TestBed.createComponent(ConfigComponent)
|
fixture = TestBed.createComponent(ConfigComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@@ -100,4 +107,39 @@ describe('ConfigComponent', () => {
|
|||||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||||
expect(component.errors).toEqual({ user_args: null })
|
expect(component.errors).toEqual({ user_args: null })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should upload file, show error if necessary', () => {
|
||||||
|
const uploadSpy = jest.spyOn(configService, 'uploadFile')
|
||||||
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
uploadSpy.mockReturnValueOnce(
|
||||||
|
throwError(() => new Error('Error uploading file'))
|
||||||
|
)
|
||||||
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
|
expect(uploadSpy).toHaveBeenCalled()
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
uploadSpy.mockReturnValueOnce(
|
||||||
|
of({ app_logo: 'https://example.com/logo/test.png' } as any)
|
||||||
|
)
|
||||||
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
|
expect(component.initialConfig).toEqual({
|
||||||
|
app_logo: 'https://example.com/logo/test.png',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should refresh ui settings after save or upload', () => {
|
||||||
|
const saveSpy = jest.spyOn(configService, 'saveConfig')
|
||||||
|
const initSpy = jest.spyOn(settingService, 'initializeSettings')
|
||||||
|
saveSpy.mockReturnValueOnce(
|
||||||
|
of({ output_type: OutputTypeConfig.PDF_A } as any)
|
||||||
|
)
|
||||||
|
component.saveConfig()
|
||||||
|
expect(initSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
const uploadSpy = jest.spyOn(configService, 'uploadFile')
|
||||||
|
uploadSpy.mockReturnValueOnce(
|
||||||
|
of({ app_logo: 'https://example.com/logo/test.png' } as any)
|
||||||
|
)
|
||||||
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
|
expect(initSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -19,6 +19,7 @@ import { ConfigService } from 'src/app/services/config.service'
|
|||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-config',
|
selector: 'pngx-config',
|
||||||
@@ -55,7 +56,8 @@ export class ConfigComponent
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private toastService: ToastService
|
private toastService: ToastService,
|
||||||
|
private settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.configForm.addControl('id', new FormControl())
|
this.configForm.addControl('id', new FormControl())
|
||||||
@@ -145,6 +147,7 @@ export class ConfigComponent
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
this.initialize(config)
|
this.initialize(config)
|
||||||
this.store.next(config)
|
this.store.next(config)
|
||||||
|
this.settingsService.initializeSettings().subscribe()
|
||||||
this.toastService.showInfo($localize`Configuration updated`)
|
this.toastService.showInfo($localize`Configuration updated`)
|
||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
@@ -160,4 +163,27 @@ export class ConfigComponent
|
|||||||
public discardChanges() {
|
public discardChanges() {
|
||||||
this.configForm.reset(this.initialConfig)
|
this.configForm.reset(this.initialConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public uploadFile(file: File, key: string) {
|
||||||
|
this.loading = true
|
||||||
|
this.configService
|
||||||
|
.uploadFile(file, this.configForm.value['id'], key)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier), first())
|
||||||
|
.subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
this.loading = false
|
||||||
|
this.initialize(config)
|
||||||
|
this.store.next(config)
|
||||||
|
this.settingsService.initializeSettings().subscribe()
|
||||||
|
this.toastService.showInfo($localize`File successfully updated`)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.loading = false
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred uploading file`,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
<pngx-page-header title="Logs" i18n-title>
|
<pngx-page-header
|
||||||
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
|
title="Logs"
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
i18n-title
|
||||||
|
info="Review the log files for the application and for email checking."
|
||||||
|
i18n-info>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
|
||||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
@@ -11,6 +11,7 @@ import { of, throwError } from 'rxjs'
|
|||||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { BrowserModule, By } from '@angular/platform-browser'
|
import { BrowserModule, By } from '@angular/platform-browser'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const paperless_logs = [
|
const paperless_logs = [
|
||||||
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
||||||
@@ -37,7 +38,12 @@ describe('LogsComponent', () => {
|
|||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [LogsComponent, PageHeaderComponent],
|
declarations: [LogsComponent, PageHeaderComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
imports: [HttpClientTestingModule, BrowserModule, NgbModule],
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
BrowserModule,
|
||||||
|
NgbModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
logService = TestBed.inject(LogService)
|
logService = TestBed.inject(LogService)
|
||||||
|
@@ -2,9 +2,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
OnInit,
|
OnInit,
|
||||||
AfterViewChecked,
|
|
||||||
ViewChild,
|
ViewChild,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
ChangeDetectorRef,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
import { LogService } from 'src/app/services/rest/log.service'
|
import { LogService } from 'src/app/services/rest/log.service'
|
||||||
@@ -14,8 +14,11 @@ import { LogService } from 'src/app/services/rest/log.service'
|
|||||||
templateUrl: './logs.component.html',
|
templateUrl: './logs.component.html',
|
||||||
styleUrls: ['./logs.component.scss'],
|
styleUrls: ['./logs.component.scss'],
|
||||||
})
|
})
|
||||||
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
export class LogsComponent implements OnInit, OnDestroy {
|
||||||
constructor(private logService: LogService) {}
|
constructor(
|
||||||
|
private logService: LogService,
|
||||||
|
private changedetectorRef: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
public logs: string[] = []
|
public logs: string[] = []
|
||||||
|
|
||||||
@@ -47,10 +50,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewChecked() {
|
|
||||||
this.scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.unsubscribeNotifier.next(true)
|
this.unsubscribeNotifier.next(true)
|
||||||
this.unsubscribeNotifier.complete()
|
this.unsubscribeNotifier.complete()
|
||||||
@@ -66,6 +65,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||||||
next: (result) => {
|
next: (result) => {
|
||||||
this.logs = result
|
this.logs = result
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.logs = []
|
this.logs = []
|
||||||
@@ -89,6 +89,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom(): void {
|
scrollToBottom(): void {
|
||||||
|
this.changedetectorRef.detectChanges()
|
||||||
this.logContainer?.nativeElement.scroll({
|
this.logContainer?.nativeElement.scroll({
|
||||||
top: this.logContainer.nativeElement.scrollHeight,
|
top: this.logContainer.nativeElement.scrollHeight,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
<pngx-page-header title="Settings" i18n-title>
|
<pngx-page-header
|
||||||
|
title="Settings"
|
||||||
|
i18n-title
|
||||||
|
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
|
||||||
|
i18n-info
|
||||||
|
>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||||
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||||
<ng-container i18n>Open Django Admin</ng-container>
|
<ng-container i18n>Open Django Admin</ng-container>
|
||||||
<svg class="sidebaricon ms-1" fill="currentColor">
|
<i-bs name="arrow-up-right"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@@ -135,204 +138,202 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
||||||
<svg fill="currentColor" class="buttonicon-sm me-1">
|
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
</button>
|
||||||
</svg><ng-container i18n>Reset</ng-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
|
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="offset-md-3 col">
|
||||||
<p i18n>
|
<p i18n>
|
||||||
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
|
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
|
||||||
Actual updating of the app must still be performed manually.
|
Actual updating of the app must still be performed manually.
|
||||||
</p>
|
</p>
|
||||||
<p i18n>
|
<p i18n>
|
||||||
<em>No tracking data is collected by the app in any way.</em>
|
<em>No tracking data is collected by the app in any way.</em>
|
||||||
</p>
|
</p>
|
||||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="offset-md-3 col">
|
||||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
|
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
|
||||||
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Notes</h4>
|
<h4 class="mt-4" i18n>Notes</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="offset-md-3 col">
|
||||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="SettingsNavIDs.Permissions">
|
<li [ngbNavItem]="SettingsNavIDs.Permissions">
|
||||||
<a ngbNavLink i18n>Permissions</a>
|
<a ngbNavLink i18n>Permissions</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<h4 i18n>Default Permissions</h4>
|
<h4 i18n>Default Permissions</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="offset-md-3 col">
|
||||||
<p i18n>
|
<p i18n>
|
||||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
</div>
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
<div class="row mb-3">
|
||||||
<span i18n>Default Owner</span>
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
</div>
|
<span i18n>Default Owner</span>
|
||||||
<div class="col-md-5">
|
|
||||||
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
|
|
||||||
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="col-md-5">
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
|
||||||
<span i18n>Default View Permissions</span>
|
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
</div>
|
||||||
<div class="row">
|
<div class="row mb-3">
|
||||||
<div class="col-3">
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
<span class="d-block pt-1" i18n>Users:</span>
|
<span i18n>Default View Permissions</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-md-5">
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
<div class="row">
|
||||||
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
|
<div class="col-3">
|
||||||
</ng-container>
|
<span class="d-block pt-1" i18n>Users:</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="col">
|
||||||
<div class="col-3">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||||
<span class="d-block pt-1" i18n>Groups:</span>
|
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
|
||||||
</div>
|
</ng-container>
|
||||||
<div class="col">
|
</div>
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
|
</div>
|
||||||
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
|
<div class="row">
|
||||||
</ng-container>
|
<div class="col-3">
|
||||||
</div>
|
<span class="d-block pt-1" i18n>Groups:</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
|
||||||
|
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
</div>
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
<div class="row mb-3">
|
||||||
<span i18n>Default Edit Permissions</span>
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
</div>
|
<span i18n>Default Edit Permissions</span>
|
||||||
<div class="col-md-5">
|
</div>
|
||||||
<div class="row">
|
<div class="col-md-5">
|
||||||
<div class="col-3">
|
<div class="row">
|
||||||
<span class="d-block pt-1" i18n>Users:</span>
|
<div class="col-3">
|
||||||
</div>
|
<span class="d-block pt-1" i18n>Users:</span>
|
||||||
<div class="col">
|
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
|
||||||
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="col">
|
||||||
<div class="col-3">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||||
<span class="d-block pt-1" i18n>Groups:</span>
|
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
|
||||||
</div>
|
</ng-container>
|
||||||
<div class="col">
|
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
|
|
||||||
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
</ng-template>
|
<div class="col-3">
|
||||||
</li>
|
<span class="d-block pt-1" i18n>Groups:</span>
|
||||||
|
</div>
|
||||||
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
<div class="col">
|
||||||
<a ngbNavLink i18n>Notifications</a>
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
|
||||||
<ng-template ngbNavContent>
|
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
|
||||||
|
</ng-container>
|
||||||
<h4 i18n>Document processing</h4>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row">
|
||||||
<div class="offset-md-3 col">
|
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||||
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
|
|
||||||
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
|
|
||||||
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
|
|
||||||
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ng-template>
|
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
||||||
</li>
|
<a ngbNavLink i18n>Notifications</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
|
<h4 i18n>Document processing</h4>
|
||||||
<a ngbNavLink i18n>Saved views</a>
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
|
|
||||||
<h4 i18n>Settings</h4>
|
<div class="row mb-3">
|
||||||
<div class="row mb-3">
|
<div class="offset-md-3 col">
|
||||||
<div class="offset-md-3 col">
|
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
|
||||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
|
||||||
|
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
|
||||||
|
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
|
||||||
|
<a ngbNavLink i18n>Saved views</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
|
<h4 i18n>Settings</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="offset-md-3 col">
|
||||||
|
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 i18n>Views</h4>
|
||||||
|
<div formGroupName="savedViews">
|
||||||
|
|
||||||
|
@for (view of savedViews; track view) {
|
||||||
|
<div [formGroupName]="view.id" class="row">
|
||||||
|
<div class="mb-3 col">
|
||||||
|
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
|
||||||
|
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col">
|
||||||
|
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n> <span class="visually-hidden">Appears on</span></label>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<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="mb-2 col-auto">
|
||||||
|
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<h4 i18n>Views</h4>
|
@if (savedViews && savedViews.length === 0) {
|
||||||
<div formGroupName="savedViews">
|
<div i18n>No saved views defined.</div>
|
||||||
|
}
|
||||||
|
|
||||||
@for (view of savedViews; track view) {
|
@if (!savedViews) {
|
||||||
<div [formGroupName]="view.id" class="row">
|
<div>
|
||||||
<div class="mb-3 col">
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
|
</div>
|
||||||
</div>
|
}
|
||||||
<div class="mb-2 col">
|
|
||||||
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n> <span class="visually-hidden">Appears on</span></label>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<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="mb-2 col-auto">
|
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (savedViews && savedViews.length === 0) {
|
</div>
|
||||||
<div i18n>No saved views defined.</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!savedViews) {
|
</ng-template>
|
||||||
<div>
|
</li>
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
</ul>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
|
|
||||||
</ng-template>
|
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
</li>
|
</form>
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
|
||||||
</form>
|
|
||||||
|
@@ -37,6 +37,7 @@ 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 { SettingsComponent } from './settings.component'
|
import { SettingsComponent } from './settings.component'
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const savedViews = [
|
const savedViews = [
|
||||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
||||||
@@ -92,6 +93,7 @@ describe('SettingsComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@@ -363,7 +363,7 @@ export class SettingsComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didnt save
|
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
|
||||||
this.storeSub && this.storeSub.unsubscribe()
|
this.storeSub && this.storeSub.unsubscribe()
|
||||||
this.settings.organizingSidebarSavedViews = false
|
this.settings.organizingSidebarSavedViews = false
|
||||||
}
|
}
|
||||||
|
@@ -1,155 +1,155 @@
|
|||||||
<pngx-page-header title="File Tasks" i18n-title>
|
<pngx-page-header
|
||||||
|
title="File Tasks"
|
||||||
|
i18n-title
|
||||||
|
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
|
||||||
|
i18n-info
|
||||||
|
>
|
||||||
<div class="btn-toolbar col col-md-auto align-items-center">
|
<div class="btn-toolbar col col-md-auto align-items-center">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
</button>
|
||||||
</svg> <ng-container i18n>Clear selection</ng-container>
|
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||||
</button>
|
<i-bs name="check2-all"></i-bs> {{dismissButtonText}}
|
||||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
</button>
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<div class="form-check form-switch mb-0">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
|
||||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||||
</button>
|
</div>
|
||||||
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
|
</div>
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
</pngx-page-header>
|
||||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</pngx-page-header>
|
|
||||||
|
|
||||||
@if (!tasksService.completedFileTasks && tasksService.loading) {
|
@if (!tasksService.completedFileTasks && tasksService.loading) {
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<ng-template let-tasks="tasks" #tasksTemplate>
|
<ng-template let-tasks="tasks" #tasksTemplate>
|
||||||
<table class="table table-striped align-middle border shadow-sm">
|
<table class="table table-striped align-middle border shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||||
<label class="form-check-label" for="all-tasks"></label>
|
<label class="form-check-label" for="all-tasks"></label>
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th scope="col" i18n>Name</th>
|
|
||||||
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
|
|
||||||
@if (activeTab !== 'started' && activeTab !== 'queued') {
|
|
||||||
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
|
|
||||||
}
|
|
||||||
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
|
|
||||||
<th scope="col" i18n>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
|
|
||||||
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
|
||||||
<td>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
|
||||||
<label class="form-check-label" for="task{{task.id}}"></label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
|
|
||||||
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
|
|
||||||
@if (activeTab !== 'started' && activeTab !== 'queued') {
|
|
||||||
<td class="d-none d-lg-table-cell">
|
|
||||||
@if (task.result?.length > 50) {
|
|
||||||
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
|
|
||||||
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
|
|
||||||
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (task.result?.length <= 50) {
|
|
||||||
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
|
|
||||||
}
|
|
||||||
<ng-template #resultPopover>
|
|
||||||
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
|
|
||||||
…
|
|
||||||
}</pre>
|
|
||||||
@if (task.result?.length > 300) {
|
|
||||||
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
|
|
||||||
}
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
<td class="d-lg-none">
|
|
||||||
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
|
|
||||||
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td scope="row">
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
|
||||||
</svg> <ng-container i18n>Dismiss</ng-container>
|
|
||||||
</button>
|
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
|
||||||
@if (task.related_document) {
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
|
||||||
</svg> <ng-container i18n>Open Document</ng-container>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
|
|
||||||
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
|
|
||||||
@if (tasks.length > 0) {
|
|
||||||
<div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
|
|
||||||
}
|
|
||||||
@if (tasks.length > pageSize) {
|
|
||||||
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</th>
|
||||||
|
<th scope="col" i18n>Name</th>
|
||||||
|
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
|
||||||
|
@if (activeTab !== 'started' && activeTab !== 'queued') {
|
||||||
|
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
|
||||||
|
}
|
||||||
|
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
|
||||||
|
<th scope="col" i18n>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
|
||||||
|
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
||||||
|
<label class="form-check-label" for="task{{task.id}}"></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
|
||||||
|
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
|
||||||
|
@if (activeTab !== 'started' && activeTab !== 'queued') {
|
||||||
|
<td class="d-none d-lg-table-cell">
|
||||||
|
@if (task.result?.length > 50) {
|
||||||
|
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
|
||||||
|
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
|
||||||
|
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (task.result?.length <= 50) {
|
||||||
|
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
|
||||||
|
}
|
||||||
|
<ng-template #resultPopover>
|
||||||
|
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
|
||||||
|
…
|
||||||
|
}</pre>
|
||||||
|
@if (task.result?.length > 300) {
|
||||||
|
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="d-lg-none">
|
||||||
|
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
|
||||||
|
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td scope="row">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
||||||
|
<i-bs name="check"></i-bs> <ng-container i18n>Dismiss</ng-container>
|
||||||
|
</button>
|
||||||
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
|
@if (task.related_document) {
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
||||||
|
<i-bs name="file-text"></i-bs> <ng-container i18n>Open Document</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
|
||||||
|
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
|
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
|
||||||
<li ngbNavItem="failed">
|
@if (tasks.length > 0) {
|
||||||
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
<div class="pb-2 pb-sm-0">
|
||||||
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
|
||||||
}</a>
|
@if (selectedTasks.size > 0) {
|
||||||
<ng-template ngbNavContent>
|
<ng-container i18n> ({{selectedTasks.size}} selected)</ng-container>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
}
|
||||||
</ng-template>
|
</div>
|
||||||
</li>
|
}
|
||||||
<li ngbNavItem="completed">
|
@if (tasks.length > pageSize) {
|
||||||
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
|
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
|
||||||
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
}
|
||||||
}</a>
|
</div>
|
||||||
<ng-template ngbNavContent>
|
</ng-template>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
|
||||||
</ng-template>
|
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
|
||||||
</li>
|
<li ngbNavItem="failed">
|
||||||
<li ngbNavItem="started">
|
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
||||||
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
|
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
||||||
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
|
}</a>
|
||||||
}</a>
|
<ng-template ngbNavContent>
|
||||||
<ng-template ngbNavContent>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
|
</ng-template>
|
||||||
</ng-template>
|
</li>
|
||||||
</li>
|
<li ngbNavItem="completed">
|
||||||
<li ngbNavItem="queued">
|
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
|
||||||
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
|
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
||||||
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
|
}</a>
|
||||||
}</a>
|
<ng-template ngbNavContent>
|
||||||
<ng-template ngbNavContent>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
|
</ng-template>
|
||||||
</ng-template>
|
</li>
|
||||||
</li>
|
<li ngbNavItem="started">
|
||||||
</ul>
|
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
|
||||||
<div [ngbNavOutlet]="nav"></div>
|
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
|
||||||
|
}</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
<li ngbNavItem="queued">
|
||||||
|
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
|
||||||
|
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
|
||||||
|
}</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div [ngbNavOutlet]="nav"></div>
|
||||||
|
@@ -28,6 +28,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
|
|||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { TasksComponent } from './tasks.component'
|
import { TasksComponent } from './tasks.component'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const tasks: PaperlessTask[] = [
|
const tasks: PaperlessTask[] = [
|
||||||
{
|
{
|
||||||
@@ -138,6 +139,7 @@ describe('TasksComponent', () => {
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
RouterTestingModule.withRoutes(routes),
|
RouterTestingModule.withRoutes(routes),
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
<pngx-page-header title="Users & Groups" i18n-title>
|
<pngx-page-header
|
||||||
|
title="Users & Groups"
|
||||||
|
i18n-title
|
||||||
|
info="Create, delete and edit users and groups."
|
||||||
|
i18n-info
|
||||||
|
infoLink="usage/#users-and-groups"
|
||||||
|
>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@if (users) {
|
@if (users) {
|
||||||
<h4 class="d-flex">
|
<h4 class="d-flex">
|
||||||
<ng-container i18n>Users</ng-container>
|
<ng-container i18n>Users</ng-container>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add User</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
|
||||||
</svg>
|
|
||||||
<ng-container i18n>Add User</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@@ -29,14 +32,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
||||||
<svg class="buttonicon-sm" fill="currentColor">
|
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
|
||||||
</svg> <ng-container i18n>Edit</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
||||||
<svg class="buttonicon-sm" fill="currentColor">
|
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
|
||||||
</svg> <ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,10 +49,7 @@
|
|||||||
<h4 class="mt-4 d-flex">
|
<h4 class="mt-4 d-flex">
|
||||||
<ng-container i18n>Groups</ng-container>
|
<ng-container i18n>Groups</ng-container>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Group</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
|
||||||
</svg>
|
|
||||||
<ng-container i18n>Add Group</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
@if (groups.length > 0) {
|
@if (groups.length > 0) {
|
||||||
@@ -75,14 +71,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||||
<svg class="buttonicon-sm" fill="currentColor">
|
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
|
||||||
</svg> <ng-container i18n>Edit</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||||
<svg class="buttonicon-sm" fill="currentColor">
|
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
|
||||||
</svg> <ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -43,6 +43,7 @@ import { SettingsComponent } from '../settings/settings.component'
|
|||||||
import { UsersAndGroupsComponent } from './users-groups.component'
|
import { UsersAndGroupsComponent } from './users-groups.component'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const users = [
|
const users = [
|
||||||
{ id: 1, username: 'user1', is_superuser: false },
|
{ id: 1, username: 'user1', is_superuser: false },
|
||||||
@@ -92,6 +93,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
fixture = TestBed.createComponent(UsersAndGroupsComponent)
|
fixture = TestBed.createComponent(UsersAndGroupsComponent)
|
||||||
|
@@ -4,30 +4,36 @@
|
|||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
|
<a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
|
||||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard"
|
[ngClass]="{ 'slim': slimSidebarEnabled, 'd-flex col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
|
||||||
|
routerLink="/dashboard"
|
||||||
tourAnchor="tour.intro">
|
tourAnchor="tour.intro">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
|
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
|
||||||
transform="translate(0 0)" />
|
transform="translate(0 0)" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
<div class="ms-2 d-inline-block" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
|
@if (customAppTitle?.length) {
|
||||||
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<span class="title">{{customAppTitle}}</span>
|
||||||
|
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
Paperless-ngx
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
|
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||||
<svg width="1em" height="1em" fill="currentColor">
|
<i-bs style="top: .25em;" width="1em" height="1em" name="search"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#search" />
|
|
||||||
</svg>
|
|
||||||
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
||||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
|
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
|
||||||
(selectItem)="itemSelected($event)" i18n-placeholder>
|
(selectItem)="itemSelected($event)" i18n-placeholder>
|
||||||
@if (!searchFieldEmpty) {
|
@if (!searchFieldEmpty) {
|
||||||
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
|
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
|
||||||
<svg fill="currentColor" class="buttonicon-sm me-1">
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@@ -38,9 +44,7 @@
|
|||||||
<span class="small me-2 d-none d-sm-inline">
|
<span class="small me-2 d-none d-sm-inline">
|
||||||
{{this.settingsService.displayName}}
|
{{this.settingsService.displayName}}
|
||||||
</span>
|
</span>
|
||||||
<svg width="1.3em" height="1.3em" fill="currentColor">
|
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#person-circle" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown">
|
<div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown">
|
||||||
<div class="d-sm-none">
|
<div class="d-sm-none">
|
||||||
@@ -48,27 +52,19 @@
|
|||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
</div>
|
</div>
|
||||||
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
||||||
<svg class="sidebaricon me-2" fill="currentColor">
|
<i-bs class="me-2" name="person"></i-bs> <ng-container i18n>My Profile</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#person" />
|
|
||||||
</svg><ng-container i18n>My Profile</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
|
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||||
<svg class="sidebaricon me-2" fill="currentColor">
|
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear" />
|
|
||||||
</svg><ng-container i18n>Settings</ng-container>
|
|
||||||
</a>
|
</a>
|
||||||
<a ngbDropdownItem class="nav-link" href="accounts/logout/" (click)="onLogout()">
|
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
|
||||||
<svg class="sidebaricon me-2" fill="currentColor">
|
<i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#door-open" />
|
|
||||||
</svg><ng-container i18n>Logout</ng-container>
|
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
|
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://docs.paperless-ngx.com">
|
href="https://docs.paperless-ngx.com">
|
||||||
<svg class="sidebaricon me-2" fill="currentColor">
|
<i-bs class="me-2" name="question-circle"></i-bs><ng-container i18n>Documentation</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
|
|
||||||
</svg><ng-container i18n>Documentation</ng-container>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -81,13 +77,11 @@
|
|||||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
||||||
[ngbCollapse]="isMenuCollapsed">
|
[ngbCollapse]="isMenuCollapsed">
|
||||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||||
<svg class="sidebaricon-sm" fill="currentColor">
|
@if (slimSidebarEnabled) {
|
||||||
@if (slimSidebarEnabled) {
|
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-right" />
|
} @else {
|
||||||
} @else {
|
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left" />
|
}
|
||||||
}
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
@@ -95,18 +89,14 @@
|
|||||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="house"></i-bs><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#house" />
|
|
||||||
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="files"></i-bs><span> <ng-container i18n>Documents</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#files" />
|
|
||||||
</svg><span> <ng-container i18n>Documents</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -128,15 +118,11 @@
|
|||||||
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">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}}</span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
|
||||||
</svg><span> {{view.name}}</span>
|
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarSavedViews) {
|
@if (settingsService.organizingSidebarSavedViews) {
|
||||||
<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>
|
||||||
<svg class="sidebaricon text-muted" fill="currentColor">
|
<i-bs name="grip-vertical"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
@@ -157,13 +143,9 @@
|
|||||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
||||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="file-text"></i-bs><span> {{d.title | documentTitle}}</span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#file-text" />
|
|
||||||
</svg><span> {{d.title | documentTitle}}</span>
|
|
||||||
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
||||||
<svg fill="currentColor" class="toolbaricon">
|
<i-bs name="x"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -173,9 +155,7 @@
|
|||||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
||||||
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="x"></i-bs><span> <ng-container i18n>Close all</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
|
||||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -191,9 +171,7 @@
|
|||||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#person" />
|
|
||||||
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
||||||
@@ -201,9 +179,7 @@
|
|||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#tags" />
|
|
||||||
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"
|
<li class="nav-item"
|
||||||
@@ -211,27 +187,21 @@
|
|||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#hash" />
|
|
||||||
</svg><span> <ng-container i18n>Document Types</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#folder" />
|
|
||||||
</svg><span> <ng-container i18n>Storage Paths</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
|
|
||||||
</svg><span> <ng-container i18n>Custom Fields</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"
|
<li class="nav-item"
|
||||||
@@ -240,9 +210,7 @@
|
|||||||
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="boxes"></i-bs><span> <ng-container i18n>Workflows</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#boxes" />
|
|
||||||
</svg><span> <ng-container i18n>Workflows</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
|
||||||
@@ -250,9 +218,7 @@
|
|||||||
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="envelope"></i-bs><span> <ng-container i18n>Mail</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#envelope" />
|
|
||||||
</svg><span> <ng-container i18n>Mail</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -266,27 +232,21 @@
|
|||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="gear"></i-bs><span> <ng-container i18n>Settings</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear" />
|
|
||||||
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
||||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="sliders2-vertical"></i-bs><span> <ng-container i18n>Configuration</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#sliders2-vertical" />
|
|
||||||
</svg><span> <ng-container i18n>Configuration</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="people"></i-bs><span> <ng-container i18n>Users & Groups</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#people" />
|
|
||||||
</svg><span> <ng-container i18n>Users & Groups</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"
|
<li class="nav-item"
|
||||||
@@ -295,23 +255,19 @@
|
|||||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<i-bs class="me-1" name="list-task"></i-bs><span> <ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
|
||||||
|
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
|
||||||
|
}</span>
|
||||||
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
|
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
|
||||||
<span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
<span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
||||||
}
|
}
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#list-task" />
|
|
||||||
</svg><span> <ng-container i18n>File Tasks@if (tasksService.failedFileTasks.length > 0) {
|
|
||||||
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
|
|
||||||
}</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="me-1" name="text-left"></i-bs><span> <ng-container i18n>Logs</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#text-left" />
|
|
||||||
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item mt-2" tourAnchor="tour.outro">
|
<li class="nav-item mt-2" tourAnchor="tour.outro">
|
||||||
@@ -319,9 +275,7 @@
|
|||||||
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1"> <ng-container i18n>Documentation</ng-container></span>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
|
|
||||||
</svg><span class="ms-1"> <ng-container i18n>Documentation</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
@@ -360,10 +314,7 @@
|
|||||||
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
||||||
container="body">
|
container="body">
|
||||||
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;"
|
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||||
viewBox="0 0 16 16">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
|
||||||
</svg>
|
|
||||||
@if (appRemoteVersion?.update_available) {
|
@if (appRemoteVersion?.update_available) {
|
||||||
<ng-container i18n>Update available</ng-container>
|
<ng-container i18n>Update available</ng-container>
|
||||||
}
|
}
|
||||||
@@ -373,10 +324,7 @@
|
|||||||
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
||||||
container="body">
|
container="body">
|
||||||
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;"
|
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||||
viewBox="0 0 16 16">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -152,9 +152,9 @@ main {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebaricon {
|
i-bs {
|
||||||
margin-right: 4px;
|
position: relative;
|
||||||
color: inherit;
|
top: -1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,11 +186,11 @@ main {
|
|||||||
width: 1.8rem;
|
width: 1.8rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
svg {
|
i-bs {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover svg {
|
&:hover i-bs {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ main {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
i-bs {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,9 +217,16 @@ main {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
padding-top: 0.75rem;
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.byline {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
@@ -241,7 +248,7 @@ main {
|
|||||||
.navbar .dropdown-menu {
|
.navbar .dropdown-menu {
|
||||||
font-size: 0.875rem; // body size
|
font-size: 0.875rem; // body size
|
||||||
|
|
||||||
a svg {
|
a i-bs {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +259,7 @@ main {
|
|||||||
form {
|
form {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> svg {
|
> i-bs {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.6rem;
|
left: 0.6rem;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
@@ -262,7 +269,7 @@ main {
|
|||||||
|
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
form > svg {
|
form > i-bs {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +323,6 @@ main {
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .navItemDrag .position-absolute svg {
|
::ng-deep .navItemDrag .position-absolute i-bs {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@@ -33,6 +33,7 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
|||||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const saved_views = [
|
const saved_views = [
|
||||||
{
|
{
|
||||||
@@ -101,6 +102,7 @@ describe('AppFrameComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
@@ -248,7 +250,7 @@ describe('AppFrameComponent', () => {
|
|||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support collapsable menu', () => {
|
it('should support collapsible menu', () => {
|
||||||
const button: HTMLButtonElement = (
|
const button: HTMLButtonElement = (
|
||||||
fixture.nativeElement as HTMLDivElement
|
fixture.nativeElement as HTMLDivElement
|
||||||
).querySelector('button[data-toggle=collapse]')
|
).querySelector('button[data-toggle=collapse]')
|
||||||
|
@@ -102,6 +102,10 @@ export class AppFrameComponent
|
|||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get customAppTitle(): string {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,11 @@
|
|||||||
@if (active) {
|
@if (active) {
|
||||||
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
||||||
@if (!isNumbered && selected) {
|
@if (!isNumbered && selected) {
|
||||||
<svg width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
|
|
||||||
</svg>
|
|
||||||
}
|
}
|
||||||
@if (isNumbered) {
|
@if (isNumbered) {
|
||||||
<div class="number">{{number}}<span class="visually-hidden">selected</span></div>
|
<div class="number">{{number}}<span class="visually-hidden">selected</span></div>
|
||||||
}
|
}
|
||||||
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@@ -22,7 +22,7 @@ button:hover {
|
|||||||
.x {
|
.x {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: .4em;
|
||||||
left: calc(50% - 4px);
|
left: calc(50% - .4em);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('ClearableBadgeComponent', () => {
|
describe('ClearableBadgeComponent', () => {
|
||||||
let component: ClearableBadgeComponent
|
let component: ClearableBadgeComponent
|
||||||
@@ -8,6 +9,7 @@ describe('ClearableBadgeComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ClearableBadgeComponent],
|
declarations: [ClearableBadgeComponent],
|
||||||
|
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
||||||
|
@@ -12,8 +12,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</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>
|
||||||
|
@@ -37,6 +37,12 @@ export class ConfirmDialogComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
alternativeBtnCaption
|
alternativeBtnCaption
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
cancelBtnClass = 'btn-outline-secondary'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
cancelBtnCaption = $localize`Cancel`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
buttonsEnabled = true
|
buttonsEnabled = true
|
||||||
|
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()">
|
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<i-bs name="ui-radios"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
|
|
||||||
</svg>
|
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
||||||
@@ -20,14 +18,10 @@
|
|||||||
</pngx-input-select>
|
</pngx-input-select>
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<div class="btn-toolbar" role="toolbar">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields">
|
<button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields">
|
||||||
<svg fill="currentColor" class="buttonicon-sm me-1 mb-1">
|
<i-bs width="1em" height="1em" name="asterisk"></i-bs> <ng-container i18n>Create New Field</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#asterisk"/>
|
|
||||||
</svg><ng-container i18n>Create New Field</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary me-1" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
|
<button class="btn btn-sm btn-outline-primary me-1" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
|
||||||
<svg fill="currentColor" class="buttonicon me-1">
|
<i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs> <ng-container i18n>Add</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle"/>
|
|
||||||
</svg><ng-container i18n>Add</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const fields: CustomField[] = [
|
const fields: CustomField[] = [
|
||||||
{
|
{
|
||||||
@@ -40,7 +41,6 @@ describe('CustomFieldsDropdownComponent', () => {
|
|||||||
let customFieldService: CustomFieldsService
|
let customFieldService: CustomFieldsService
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
let modalService: NgbModal
|
let modalService: NgbModal
|
||||||
let httpController: HttpTestingController
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -52,10 +52,10 @@ describe('CustomFieldsDropdownComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
customFieldService = TestBed.inject(CustomFieldsService)
|
customFieldService = TestBed.inject(CustomFieldsService)
|
||||||
httpController = TestBed.inject(HttpTestingController)
|
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
modalService = TestBed.inject(NgbModal)
|
modalService = TestBed.inject(NgbModal)
|
||||||
jest.spyOn(customFieldService, 'listAll').mockReturnValue(
|
jest.spyOn(customFieldService, 'listAll').mockReturnValue(
|
||||||
|
@@ -9,9 +9,7 @@
|
|||||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
|
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
|
||||||
<div class="selected-icon">
|
<div class="selected-icon">
|
||||||
@if (relativeDate === rd.id) {
|
@if (relativeDate === rd.id) {
|
||||||
<svg fill="currentColor" class="buttonicon-sm">
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
|
||||||
</svg>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||||
@@ -32,9 +30,7 @@
|
|||||||
<div i18n>After</div>
|
<div i18n>After</div>
|
||||||
@if (dateAfter) {
|
@if (dateAfter) {
|
||||||
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
||||||
<svg fill="currentColor" class="buttonicon-sm">
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg>
|
|
||||||
<small i18n>Clear</small>
|
<small i18n>Clear</small>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -44,9 +40,7 @@
|
|||||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||||
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
||||||
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
||||||
<svg fill="currentColor" class="buttonicon-sm">
|
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,9 +51,7 @@
|
|||||||
<div i18n>Before</div>
|
<div i18n>Before</div>
|
||||||
@if (dateBefore) {
|
@if (dateBefore) {
|
||||||
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
||||||
<svg fill="currentColor" class="buttonicon-sm">
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg>
|
|
||||||
<small i18n>Clear</small>
|
<small i18n>Clear</small>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -69,9 +61,7 @@
|
|||||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||||
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
||||||
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
||||||
<svg fill="currentColor" class="buttonicon-sm">
|
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -10,20 +10,17 @@ import {
|
|||||||
DateSelection,
|
DateSelection,
|
||||||
RelativeDate,
|
RelativeDate,
|
||||||
} from './date-dropdown.component'
|
} from './date-dropdown.component'
|
||||||
import {
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
HttpClientTestingModule,
|
|
||||||
HttpTestingController,
|
|
||||||
} from '@angular/common/http/testing'
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('DateDropdownComponent', () => {
|
describe('DateDropdownComponent', () => {
|
||||||
let component: DateDropdownComponent
|
let component: DateDropdownComponent
|
||||||
let httpTestingController: HttpTestingController
|
|
||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
let settingsSpy
|
let settingsSpy
|
||||||
|
|
||||||
@@ -40,10 +37,10 @@ describe('DateDropdownComponent', () => {
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
httpTestingController = TestBed.inject(HttpTestingController)
|
|
||||||
settingsService = TestBed.inject(SettingsService)
|
settingsService = TestBed.inject(SettingsService)
|
||||||
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
|
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></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="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||||
@if (patternRequired) {
|
@if (patternRequired) {
|
||||||
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></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="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
|
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
|
||||||
@if (typeFieldDisabled) {
|
@if (typeFieldDisabled) {
|
||||||
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
|
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
|
||||||
|
@@ -97,7 +97,7 @@ export abstract class EditDialogComponent<
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
|
// wait to enable close button so it doesn't steal focus from input since its the first clickable element in the DOM
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.closeEnabled = true
|
this.closeEnabled = true
|
||||||
})
|
})
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
<pngx-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></pngx-permissions-select>
|
<pngx-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></pngx-permissions-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
<pngx-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></pngx-input-text>
|
<pngx-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></pngx-input-text>
|
||||||
<pngx-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></pngx-input-text>
|
<pngx-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></pngx-input-text>
|
||||||
<pngx-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></pngx-input-select>
|
<pngx-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></pngx-input-select>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></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="Account" [items]="accounts" formControlName="account"></pngx-input-select>
|
<pngx-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select>
|
||||||
<pngx-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></pngx-input-text>
|
<pngx-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></pngx-input-text>
|
||||||
<pngx-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></pngx-input-number>
|
<pngx-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></pngx-input-number>
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
<pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
|
<pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
|
||||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||||
@if (patternRequired) {
|
@if (patternRequired) {
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
|
|
||||||
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
|
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
|
||||||
|
|
||||||
|
@@ -13,6 +13,7 @@ import { SelectComponent } from '../../input/select/select.component'
|
|||||||
import { TextComponent } from '../../input/text/text.component'
|
import { TextComponent } from '../../input/text/text.component'
|
||||||
import { EditDialogMode } from '../edit-dialog.component'
|
import { EditDialogMode } from '../edit-dialog.component'
|
||||||
import { TagEditDialogComponent } from './tag-edit-dialog.component'
|
import { TagEditDialogComponent } from './tag-edit-dialog.component'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('TagEditDialogComponent', () => {
|
describe('TagEditDialogComponent', () => {
|
||||||
let component: TagEditDialogComponent
|
let component: TagEditDialogComponent
|
||||||
@@ -38,6 +39,7 @@ describe('TagEditDialogComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||||
@@ -27,10 +27,7 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<p class="p-2" i18n>Trigger Workflow On:</p>
|
<p class="p-2" i18n>Trigger Workflow On:</p>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Trigger</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
|
||||||
</svg>
|
|
||||||
<ng-container i18n>Add Trigger</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ngbAccordion [closeOthers]="true">
|
<div ngbAccordion [closeOthers]="true">
|
||||||
@@ -42,10 +39,7 @@
|
|||||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
|
||||||
}
|
}
|
||||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)">
|
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
|
||||||
</svg>
|
|
||||||
<ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,10 +65,7 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<p class="p-2" i18n>Apply Actions:</p>
|
<p class="p-2" i18n>Apply Actions:</p>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Action</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
|
||||||
</svg>
|
|
||||||
<ng-container i18n>Add Action</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
||||||
@@ -86,10 +77,7 @@
|
|||||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
||||||
}
|
}
|
||||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
|
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
|
||||||
</svg>
|
|
||||||
<ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +87,7 @@
|
|||||||
<input type="hidden" formControlName="id" />
|
<input type="hidden" formControlName="id" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.assign_title"></pngx-input-text>
|
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
|
||||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
|
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
|
||||||
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
||||||
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
|
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
|
||||||
@@ -182,7 +170,7 @@
|
|||||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
||||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
|
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||||
}
|
}
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||||
|
@@ -66,7 +66,7 @@ const workflow: Workflow = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ConsumptionTemplateEditDialogComponent', () => {
|
describe('WorkflowEditDialogComponent', () => {
|
||||||
let component: WorkflowEditDialogComponent
|
let component: WorkflowEditDialogComponent
|
||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
let fixture: ComponentFixture<WorkflowEditDialogComponent>
|
let fixture: ComponentFixture<WorkflowEditDialogComponent>
|
||||||
@@ -219,6 +219,7 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
|
|||||||
const action1 = workflow.actions[0]
|
const action1 = workflow.actions[0]
|
||||||
const action2 = workflow.actions[1]
|
const action2 = workflow.actions[1]
|
||||||
component.object = workflow
|
component.object = workflow
|
||||||
|
component.ngOnInit()
|
||||||
component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
|
component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
|
||||||
WorkflowAction[]
|
WorkflowAction[]
|
||||||
>)
|
>)
|
||||||
|
@@ -19,6 +19,7 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
|
|||||||
import { CustomField } from 'src/app/data/custom-field'
|
import { CustomField } from 'src/app/data/custom-field'
|
||||||
import {
|
import {
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
|
WorkflowTrigger,
|
||||||
WorkflowTriggerType,
|
WorkflowTriggerType,
|
||||||
} from 'src/app/data/workflow-trigger'
|
} from 'src/app/data/workflow-trigger'
|
||||||
import {
|
import {
|
||||||
@@ -157,7 +158,7 @@ export class WorkflowEditDialogComponent
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
this.updateTriggerActionFields()
|
this.updateAllTriggerActionFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
get triggerFields(): FormArray {
|
get triggerFields(): FormArray {
|
||||||
@@ -168,52 +169,66 @@ export class WorkflowEditDialogComponent
|
|||||||
return this.objectForm.get('actions') as FormArray
|
return this.objectForm.get('actions') as FormArray
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTriggerActionFields(emitEvent: boolean = false) {
|
private createTriggerField(
|
||||||
|
trigger: WorkflowTrigger,
|
||||||
|
emitEvent: boolean = false
|
||||||
|
) {
|
||||||
|
this.triggerFields.push(
|
||||||
|
new FormGroup({
|
||||||
|
id: new FormControl(trigger.id),
|
||||||
|
type: new FormControl(trigger.type),
|
||||||
|
sources: new FormControl(trigger.sources),
|
||||||
|
filter_filename: new FormControl(trigger.filter_filename),
|
||||||
|
filter_path: new FormControl(trigger.filter_path),
|
||||||
|
filter_mailrule: new FormControl(trigger.filter_mailrule),
|
||||||
|
matching_algorithm: new FormControl(trigger.matching_algorithm),
|
||||||
|
match: new FormControl(trigger.match),
|
||||||
|
is_insensitive: new FormControl(trigger.is_insensitive),
|
||||||
|
filter_has_tags: new FormControl(trigger.filter_has_tags),
|
||||||
|
filter_has_correspondent: new FormControl(
|
||||||
|
trigger.filter_has_correspondent
|
||||||
|
),
|
||||||
|
filter_has_document_type: new FormControl(
|
||||||
|
trigger.filter_has_document_type
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{ emitEvent }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private createActionField(
|
||||||
|
action: WorkflowAction,
|
||||||
|
emitEvent: boolean = false
|
||||||
|
) {
|
||||||
|
this.actionFields.push(
|
||||||
|
new FormGroup({
|
||||||
|
id: new FormControl(action.id),
|
||||||
|
type: new FormControl(action.type),
|
||||||
|
assign_title: new FormControl(action.assign_title),
|
||||||
|
assign_tags: new FormControl(action.assign_tags),
|
||||||
|
assign_owner: new FormControl(action.assign_owner),
|
||||||
|
assign_document_type: new FormControl(action.assign_document_type),
|
||||||
|
assign_correspondent: new FormControl(action.assign_correspondent),
|
||||||
|
assign_storage_path: new FormControl(action.assign_storage_path),
|
||||||
|
assign_view_users: new FormControl(action.assign_view_users),
|
||||||
|
assign_view_groups: new FormControl(action.assign_view_groups),
|
||||||
|
assign_change_users: new FormControl(action.assign_change_users),
|
||||||
|
assign_change_groups: new FormControl(action.assign_change_groups),
|
||||||
|
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
||||||
|
}),
|
||||||
|
{ emitEvent }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAllTriggerActionFields(emitEvent: boolean = false) {
|
||||||
this.triggerFields.clear({ emitEvent: false })
|
this.triggerFields.clear({ emitEvent: false })
|
||||||
this.object?.triggers.forEach((trigger) => {
|
this.object?.triggers.forEach((trigger) => {
|
||||||
this.triggerFields.push(
|
this.createTriggerField(trigger, emitEvent)
|
||||||
new FormGroup({
|
|
||||||
id: new FormControl(trigger.id),
|
|
||||||
type: new FormControl(trigger.type),
|
|
||||||
sources: new FormControl(trigger.sources),
|
|
||||||
filter_filename: new FormControl(trigger.filter_filename),
|
|
||||||
filter_path: new FormControl(trigger.filter_path),
|
|
||||||
filter_mailrule: new FormControl(trigger.filter_mailrule),
|
|
||||||
matching_algorithm: new FormControl(MATCH_NONE),
|
|
||||||
match: new FormControl(''),
|
|
||||||
is_insensitive: new FormControl(true),
|
|
||||||
filter_has_tags: new FormControl(trigger.filter_has_tags),
|
|
||||||
filter_has_correspondent: new FormControl(
|
|
||||||
trigger.filter_has_correspondent
|
|
||||||
),
|
|
||||||
filter_has_document_type: new FormControl(
|
|
||||||
trigger.filter_has_document_type
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
{ emitEvent }
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.actionFields.clear({ emitEvent: false })
|
this.actionFields.clear({ emitEvent: false })
|
||||||
this.object?.actions.forEach((action) => {
|
this.object?.actions.forEach((action) => {
|
||||||
this.actionFields.push(
|
this.createActionField(action, emitEvent)
|
||||||
new FormGroup({
|
|
||||||
id: new FormControl(action.id),
|
|
||||||
type: new FormControl(action.type),
|
|
||||||
assign_title: new FormControl(action.assign_title),
|
|
||||||
assign_tags: new FormControl(action.assign_tags),
|
|
||||||
assign_owner: new FormControl(action.assign_owner),
|
|
||||||
assign_document_type: new FormControl(action.assign_document_type),
|
|
||||||
assign_correspondent: new FormControl(action.assign_correspondent),
|
|
||||||
assign_storage_path: new FormControl(action.assign_storage_path),
|
|
||||||
assign_view_users: new FormControl(action.assign_view_users),
|
|
||||||
assign_view_groups: new FormControl(action.assign_view_groups),
|
|
||||||
assign_change_users: new FormControl(action.assign_change_users),
|
|
||||||
assign_change_groups: new FormControl(action.assign_change_groups),
|
|
||||||
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
|
||||||
}),
|
|
||||||
{ emitEvent }
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +248,7 @@ export class WorkflowEditDialogComponent
|
|||||||
if (!this.object) {
|
if (!this.object) {
|
||||||
this.object = Object.assign({}, this.objectForm.value)
|
this.object = Object.assign({}, this.objectForm.value)
|
||||||
}
|
}
|
||||||
this.object.triggers.push({
|
const trigger: WorkflowTrigger = {
|
||||||
type: WorkflowTriggerType.Consumption,
|
type: WorkflowTriggerType.Consumption,
|
||||||
sources: [],
|
sources: [],
|
||||||
filter_filename: null,
|
filter_filename: null,
|
||||||
@@ -242,9 +257,12 @@ export class WorkflowEditDialogComponent
|
|||||||
filter_has_tags: [],
|
filter_has_tags: [],
|
||||||
filter_has_correspondent: null,
|
filter_has_correspondent: null,
|
||||||
filter_has_document_type: null,
|
filter_has_document_type: null,
|
||||||
})
|
matching_algorithm: MATCH_NONE,
|
||||||
|
match: '',
|
||||||
this.updateTriggerActionFields()
|
is_insensitive: true,
|
||||||
|
}
|
||||||
|
this.object.triggers.push(trigger)
|
||||||
|
this.createTriggerField(trigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
get actionTypeOptions() {
|
get actionTypeOptions() {
|
||||||
@@ -259,7 +277,7 @@ export class WorkflowEditDialogComponent
|
|||||||
if (!this.object) {
|
if (!this.object) {
|
||||||
this.object = Object.assign({}, this.objectForm.value)
|
this.object = Object.assign({}, this.objectForm.value)
|
||||||
}
|
}
|
||||||
this.object.actions.push({
|
const action: WorkflowAction = {
|
||||||
type: WorkflowActionType.Assignment,
|
type: WorkflowActionType.Assignment,
|
||||||
assign_title: null,
|
assign_title: null,
|
||||||
assign_tags: [],
|
assign_tags: [],
|
||||||
@@ -272,19 +290,19 @@ export class WorkflowEditDialogComponent
|
|||||||
assign_change_users: [],
|
assign_change_users: [],
|
||||||
assign_change_groups: [],
|
assign_change_groups: [],
|
||||||
assign_custom_fields: [],
|
assign_custom_fields: [],
|
||||||
})
|
}
|
||||||
|
this.object.actions.push(action)
|
||||||
this.updateTriggerActionFields()
|
this.createActionField(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTrigger(index: number) {
|
removeTrigger(index: number) {
|
||||||
this.object.triggers.splice(index, 1)
|
this.object.triggers.splice(index, 1).pop()
|
||||||
this.updateTriggerActionFields()
|
this.triggerFields.removeAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAction(index: number) {
|
removeAction(index: number) {
|
||||||
this.object.actions.splice(index, 1)
|
this.object.actions.splice(index, 1)
|
||||||
this.updateTriggerActionFields()
|
this.actionFields.removeAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
onActionDrop(event: CdkDragDrop<WorkflowAction[]>) {
|
onActionDrop(event: CdkDragDrop<WorkflowAction[]>) {
|
||||||
@@ -293,8 +311,10 @@ export class WorkflowEditDialogComponent
|
|||||||
event.previousIndex,
|
event.previousIndex,
|
||||||
event.currentIndex
|
event.currentIndex
|
||||||
)
|
)
|
||||||
|
const actionField = this.actionFields.at(event.previousIndex)
|
||||||
|
this.actionFields.removeAt(event.previousIndex)
|
||||||
|
this.actionFields.insert(event.currentIndex, actionField)
|
||||||
// removing id will effectively re-create the actions in this order
|
// removing id will effectively re-create the actions in this order
|
||||||
this.object.actions.forEach((a) => (a.id = null))
|
this.object.actions.forEach((a) => (a.id = null))
|
||||||
this.updateTriggerActionFields()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)">
|
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)">
|
||||||
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<i-bs name="{{icon}}"></i-bs>
|
||||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
|
||||||
</svg>
|
|
||||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||||
@if (!editing && selectionModel.totalCount > 0) {
|
@if (!editing && selectionModel.totalCount > 0) {
|
||||||
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
|
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
|
||||||
@@ -49,9 +47,7 @@
|
|||||||
@if (editing) {
|
@if (editing) {
|
||||||
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (!editing && manyToOne) {
|
@if (!editing && manyToOne) {
|
||||||
|
@@ -25,10 +25,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
small > svg {
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-note {
|
.list-group-item-note {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
|
@@ -25,6 +25,7 @@ import {
|
|||||||
import { TagComponent } from '../tag/tag.component'
|
import { TagComponent } from '../tag/tag.component'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const items: Tag[] = [
|
const items: Tag[] = [
|
||||||
{
|
{
|
||||||
@@ -63,7 +64,12 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
ClearableBadgeComponent,
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
providers: [FilterPipe],
|
providers: [FilterPipe],
|
||||||
imports: [NgbModule, FormsModule, ReactiveFormsModule],
|
imports: [
|
||||||
|
NgbModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||||
@@ -215,6 +221,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should apply changes and close when apply button clicked', () => {
|
it('should apply changes and close when apply button clicked', () => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
component.editing = true
|
component.editing = true
|
||||||
component.selectionModel = selectionModel
|
component.selectionModel = selectionModel
|
||||||
fixture.nativeElement
|
fixture.nativeElement
|
||||||
@@ -236,6 +243,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should apply on close if enabled', () => {
|
it('should apply on close if enabled', () => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
component.editing = true
|
component.editing = true
|
||||||
component.applyOnClose = true
|
component.applyOnClose = true
|
||||||
component.selectionModel = selectionModel
|
component.selectionModel = selectionModel
|
||||||
@@ -253,6 +261,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
fixture.nativeElement
|
fixture.nativeElement
|
||||||
.querySelector('button')
|
.querySelector('button')
|
||||||
.dispatchEvent(new MouseEvent('click')) // open
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
@@ -279,6 +288,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||||
fixture.nativeElement
|
fixture.nativeElement
|
||||||
.querySelector('button')
|
.querySelector('button')
|
||||||
@@ -298,6 +308,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
component.editing = true
|
component.editing = true
|
||||||
let applyResult: ChangedItems
|
let applyResult: ChangedItems
|
||||||
component.apply.subscribe((result) => (applyResult = result))
|
component.apply.subscribe((result) => (applyResult = result))
|
||||||
@@ -319,6 +330,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
fixture.nativeElement
|
fixture.nativeElement
|
||||||
.querySelector('button')
|
.querySelector('button')
|
||||||
.dispatchEvent(new MouseEvent('click')) // open
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
@@ -363,6 +375,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
fixture.nativeElement
|
fixture.nativeElement
|
||||||
.querySelector('button')
|
.querySelector('button')
|
||||||
.dispatchEvent(new MouseEvent('click')) // open
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
@@ -398,6 +411,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
fixture.nativeElement
|
fixture.nativeElement
|
||||||
.querySelector('button')
|
.querySelector('button')
|
||||||
.dispatchEvent(new MouseEvent('click')) // open
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
@@ -422,6 +436,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should toggle logical operator', fakeAsync(() => {
|
it('should toggle logical operator', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
component.manyToOne = true
|
component.manyToOne = true
|
||||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||||
@@ -450,6 +465,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
|
|
||||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||||
component.items = items
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||||
component.selectionModel = selectionModel
|
component.selectionModel = selectionModel
|
||||||
|
@@ -1,19 +1,13 @@
|
|||||||
<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)="toggleItem($event)" [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)="toggleItem($event)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (isChecked()) {
|
@if (isChecked()) {
|
||||||
<svg fill="currentColor" class="buttonicon-sm bi-check">
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
|
||||||
</svg>
|
|
||||||
}
|
}
|
||||||
@if (isPartiallyChecked()) {
|
@if (isPartiallyChecked()) {
|
||||||
<svg fill="currentColor" class="buttonicon-sm bi-dash">
|
<i-bs width="1em" height="1em" name="dash"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#dash"/>
|
|
||||||
</svg>
|
|
||||||
}
|
}
|
||||||
@if (isExcluded()) {
|
@if (isExcluded()) {
|
||||||
<svg fill="currentColor" class="buttonicon-sm bi-x">
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="me-1">
|
<div class="me-1">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
import { MatchingModel } from 'src/app/data/matching-model'
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
|
|
||||||
export enum ToggleableItemState {
|
export enum ToggleableItemState {
|
||||||
|
@@ -5,9 +5,7 @@
|
|||||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,10 +16,7 @@
|
|||||||
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||||
|
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
|
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">
|
<i-bs name="dice5"></i-bs>
|
||||||
<path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/>
|
|
||||||
<path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
import { ColorComponent } from './color.component'
|
import { ColorComponent } from './color.component'
|
||||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ColorSliderModule } from 'ngx-color/slider'
|
import { ColorSliderModule } from 'ngx-color/slider'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('ColorComponent', () => {
|
describe('ColorComponent', () => {
|
||||||
let component: ColorComponent
|
let component: ColorComponent
|
||||||
@@ -22,6 +23,7 @@ describe('ColorComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
ColorSliderModule,
|
ColorSliderModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@@ -4,9 +4,7 @@
|
|||||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -16,15 +14,11 @@
|
|||||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
|
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
|
||||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
|
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon">
|
<i-bs width="1.2em" height="1.2em" name="calendar"></i-bs>
|
||||||
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
@if (showFilter) {
|
@if (showFilter) {
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
|
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
|
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('DateComponent', () => {
|
describe('DateComponent', () => {
|
||||||
let component: DateComponent
|
let component: DateComponent
|
||||||
@@ -33,6 +34,7 @@ describe('DateComponent', () => {
|
|||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
NgbDatepickerModule,
|
NgbDatepickerModule,
|
||||||
RouterTestingModule,
|
RouterTestingModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@@ -6,51 +6,45 @@
|
|||||||
}
|
}
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
</button>
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
}
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div [class.col-md-9]="horizontal">
|
|
||||||
<div>
|
|
||||||
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
|
|
||||||
[disabled]="disabled"
|
|
||||||
[items]="foundDocuments$ | async"
|
|
||||||
placeholder="Search for documents"
|
|
||||||
[notFoundText]="notFoundText"
|
|
||||||
[multiple]="true"
|
|
||||||
bindValue="id"
|
|
||||||
[compareWith]="compareDocuments"
|
|
||||||
[trackByFn]="trackByFn"
|
|
||||||
[minTermLength]="2"
|
|
||||||
[loading]="loading"
|
|
||||||
[typeahead]="documentsInput$"
|
|
||||||
(change)="onChange(selectedDocuments)">
|
|
||||||
<ng-template ng-label-tmp let-document="item">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg>
|
|
||||||
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
|
|
||||||
<svg class="sidebaricon-sm me-1" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
|
||||||
</svg><span>{{document.title}}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template ng-loadingspinner-tmp>
|
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
|
|
||||||
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
|
|
||||||
</ng-template>
|
|
||||||
</ng-select>
|
|
||||||
</div>
|
|
||||||
@if (hint) {
|
|
||||||
<small class="form-text text-muted">{{hint}}</small>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div [class.col-md-9]="horizontal">
|
||||||
|
<div>
|
||||||
|
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
|
||||||
|
[disabled]="disabled"
|
||||||
|
[items]="foundDocuments$ | async"
|
||||||
|
placeholder="Search for documents"
|
||||||
|
[notFoundText]="notFoundText"
|
||||||
|
[multiple]="true"
|
||||||
|
bindValue="id"
|
||||||
|
[compareWith]="compareDocuments"
|
||||||
|
[trackByFn]="trackByFn"
|
||||||
|
[minTermLength]="2"
|
||||||
|
[loading]="loading"
|
||||||
|
[typeahead]="documentsInput$"
|
||||||
|
(change)="onChange(selectedDocuments)">
|
||||||
|
<ng-template ng-label-tmp let-document="item">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i-bs (click)="unselect(document)" name="x"></i-bs>
|
||||||
|
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
|
||||||
|
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{document.title}}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ng-loadingspinner-tmp>
|
||||||
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
|
||||||
|
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-select>
|
||||||
|
</div>
|
||||||
|
@if (hint) {
|
||||||
|
<small class="form-text text-muted">{{hint}}</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebaricon {
|
i-bs {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
<div class="mb-3" [class.pb-3]="error">
|
||||||
|
<div class="row">
|
||||||
|
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||||
|
@if (title) {
|
||||||
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
|
}
|
||||||
|
@if (removable) {
|
||||||
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="input-group" [class.col-md-9]="horizontal" [class.is-invalid]="error">
|
||||||
|
<input #fileInput type="file" class="form-control" [id]="inputId" (change)="onFile($event)" [disabled]="disabled">
|
||||||
|
<button class="btn btn-outline-primary py-0" type="button" (click)="uploadClicked()" [disabled]="disabled || !file" i18n>Upload</button>
|
||||||
|
</div>
|
||||||
|
@if (filename) {
|
||||||
|
<div class="form-text d-flex align-items-center">
|
||||||
|
<span class="text-muted">{{filename}}</span>
|
||||||
|
<button type="button" class="btn btn-link btn-sm text-danger ms-2" (click)="clear()">
|
||||||
|
<i-bs name="x"></i-bs><small i18n>Remove</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
|
||||||
|
@if (hint) {
|
||||||
|
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||||
|
}
|
||||||
|
<div class="invalid-feedback position-absolute top-100">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { FileComponent } from './file.component'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
|
||||||
|
describe('FileComponent', () => {
|
||||||
|
let component: FileComponent
|
||||||
|
let fixture: ComponentFixture<FileComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [FileComponent],
|
||||||
|
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FileComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update file on change', () => {
|
||||||
|
const event = { target: { files: [new File([], 'test.png')] } }
|
||||||
|
component.onFile(event as any)
|
||||||
|
expect(component.file.name).toEqual('test.png')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get filename', () => {
|
||||||
|
component.value = 'https://example.com:8000/logo/filename.svg'
|
||||||
|
expect(component.filename).toEqual('filename.svg')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fire upload event', () => {
|
||||||
|
let firedFile
|
||||||
|
component.file = new File([], 'test.png')
|
||||||
|
component.upload.subscribe((file) => (firedFile = file))
|
||||||
|
component.uploadClicked()
|
||||||
|
expect(firedFile.name).toEqual('test.png')
|
||||||
|
expect(component.file).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
forwardRef,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => FileComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'pngx-input-file',
|
||||||
|
templateUrl: './file.component.html',
|
||||||
|
styleUrl: './file.component.scss',
|
||||||
|
})
|
||||||
|
export class FileComponent extends AbstractInputComponent<string> {
|
||||||
|
@Output()
|
||||||
|
upload = new EventEmitter<File>()
|
||||||
|
|
||||||
|
public file: File
|
||||||
|
|
||||||
|
@ViewChild('fileInput') fileInput: ElementRef
|
||||||
|
|
||||||
|
get filename(): string {
|
||||||
|
return this.value
|
||||||
|
? this.value.substring(this.value.lastIndexOf('/') + 1)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
onFile(event: Event) {
|
||||||
|
this.file = (event.target as HTMLInputElement).files[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadClicked() {
|
||||||
|
this.upload.emit(this.file)
|
||||||
|
this.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.file = undefined
|
||||||
|
this.fileInput.nativeElement.value = null
|
||||||
|
this.writeValue(null)
|
||||||
|
this.onChange(null)
|
||||||
|
}
|
||||||
|
}
|
@@ -6,9 +6,7 @@
|
|||||||
}
|
}
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -56,8 +56,13 @@ describe('NumberComponent', () => {
|
|||||||
component.step = 0.1
|
component.step = 0.1
|
||||||
component.writeValue(12.3456)
|
component.writeValue(12.3456)
|
||||||
expect(component.value).toEqual(12.3456)
|
expect(component.value).toEqual(12.3456)
|
||||||
// float (step = .1) doesnt force 2 decimals
|
// float (step = .1) doesn't force 2 decimals
|
||||||
component.writeValue(11.1)
|
component.writeValue(11.1)
|
||||||
expect(component.value).toEqual(11.1)
|
expect(component.value).toEqual(11.1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support scientific notation', () => {
|
||||||
|
component.writeValue(1.23456789e8)
|
||||||
|
expect(component.value).toEqual(123456789)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -37,7 +37,8 @@ export class NumberComponent extends AbstractInputComponent<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeValue(newValue: any): void {
|
writeValue(newValue: any): void {
|
||||||
if (this.step === 1) newValue = parseInt(newValue, 10)
|
if (this.step === 1 && newValue?.toString().indexOf('e') === -1)
|
||||||
|
newValue = parseInt(newValue, 10)
|
||||||
if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2)
|
if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2)
|
||||||
super.writeValue(newValue)
|
super.writeValue(newValue)
|
||||||
}
|
}
|
||||||
|
@@ -4,9 +4,7 @@
|
|||||||
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
||||||
@if (showReveal) {
|
@if (showReveal) {
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
|
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
<i-bs name="eye"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#eye" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
import { PasswordComponent } from './password.component'
|
import { PasswordComponent } from './password.component'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
describe('PasswordComponent', () => {
|
describe('PasswordComponent', () => {
|
||||||
let component: PasswordComponent
|
let component: PasswordComponent
|
||||||
@@ -16,7 +17,11 @@ describe('PasswordComponent', () => {
|
|||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [PasswordComponent],
|
declarations: [PasswordComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
imports: [FormsModule, ReactiveFormsModule],
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(PasswordComponent)
|
fixture = TestBed.createComponent(PasswordComponent)
|
||||||
@@ -28,7 +33,7 @@ describe('PasswordComponent', () => {
|
|||||||
|
|
||||||
it('should support use of input field', () => {
|
it('should support use of input field', () => {
|
||||||
expect(component.value).toBeUndefined()
|
expect(component.value).toBeUndefined()
|
||||||
// TODO: why doesnt this work?
|
// TODO: why doesn't this work?
|
||||||
// input.value = 'foo'
|
// input.value = 'foo'
|
||||||
// input.dispatchEvent(new Event('change'))
|
// input.dispatchEvent(new Event('change'))
|
||||||
// fixture.detectChanges()
|
// fixture.detectChanges()
|
||||||
|
@@ -6,9 +6,7 @@
|
|||||||
}
|
}
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -40,16 +38,12 @@
|
|||||||
</ng-select>
|
</ng-select>
|
||||||
@if (allowCreateNew) {
|
@if (allowCreateNew) {
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
|
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (showFilter) {
|
@if (showFilter) {
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
|
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,12 +2,15 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
@if (!horizontal) {
|
@if (!horizontal) {
|
||||||
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
|
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
|
||||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
<label class="form-label" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
|
||||||
|
{{title}}
|
||||||
|
@if (showUnsetNote && isUnset) {
|
||||||
|
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +19,12 @@
|
|||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||||
@if (horizontal) {
|
@if (horizontal) {
|
||||||
<label class="form-check-label" [for]="inputId">{{title}}</label>
|
<label class="form-check-label" [class.text-muted]="showUnsetNote && isUnset" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
|
||||||
|
{{title}}
|
||||||
|
@if (showUnsetNote && isUnset) {
|
||||||
|
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
}
|
}
|
||||||
@if (hint) {
|
@if (hint) {
|
||||||
<div class="form-text text-muted">{{hint}}</div>
|
<div class="form-text text-muted">{{hint}}</div>
|
||||||
@@ -25,3 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #tipContent>
|
||||||
|
<span class="text-light fst-italic" i18n>Note: value has not yet been set and will not apply until explicitly changed</span>
|
||||||
|
</ng-template>
|
||||||
|
@@ -5,6 +5,7 @@ import {
|
|||||||
NG_VALUE_ACCESSOR,
|
NG_VALUE_ACCESSOR,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
describe('SwitchComponent', () => {
|
describe('SwitchComponent', () => {
|
||||||
let component: SwitchComponent
|
let component: SwitchComponent
|
||||||
@@ -15,7 +16,7 @@ describe('SwitchComponent', () => {
|
|||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [SwitchComponent],
|
declarations: [SwitchComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
imports: [FormsModule, ReactiveFormsModule],
|
imports: [FormsModule, ReactiveFormsModule, NgbTooltipModule],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SwitchComponent)
|
fixture = TestBed.createComponent(SwitchComponent)
|
||||||
@@ -36,4 +37,9 @@ describe('SwitchComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.value).toBeFalsy()
|
expect(component.value).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show note if unset', () => {
|
||||||
|
component.value = null
|
||||||
|
expect(component.isUnset).toBeTruthy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, forwardRef } from '@angular/core'
|
import { Component, Input, forwardRef } from '@angular/core'
|
||||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { AbstractInputComponent } from '../abstract-input'
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
|
||||||
@@ -15,7 +15,14 @@ import { AbstractInputComponent } from '../abstract-input'
|
|||||||
styleUrls: ['./switch.component.scss'],
|
styleUrls: ['./switch.component.scss'],
|
||||||
})
|
})
|
||||||
export class SwitchComponent extends AbstractInputComponent<boolean> {
|
export class SwitchComponent extends AbstractInputComponent<boolean> {
|
||||||
|
@Input()
|
||||||
|
showUnsetNote: boolean = false
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isUnset(): boolean {
|
||||||
|
return this.value === null || this.value === undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,9 +18,7 @@
|
|||||||
|
|
||||||
<ng-template ng-label-tmp let-item="item">
|
<ng-template ng-label-tmp let-item="item">
|
||||||
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
|
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
|
||||||
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<i-bs name="x"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg>
|
|
||||||
@if (item.id && tags) {
|
@if (item.id && tags) {
|
||||||
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
|
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
|
||||||
}
|
}
|
||||||
@@ -36,16 +34,12 @@
|
|||||||
</ng-select>
|
</ng-select>
|
||||||
@if (allowCreate) {
|
@if (allowCreate) {
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
|
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (showFilter) {
|
@if (showFilter) {
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
|
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -30,6 +30,7 @@ import { ColorComponent } from '../color/color.component'
|
|||||||
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
|
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
|
||||||
import { SelectComponent } from '../select/select.component'
|
import { SelectComponent } from '../select/select.component'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const tags: Tag[] = [
|
const tags: Tag[] = [
|
||||||
{
|
{
|
||||||
@@ -99,6 +100,7 @@ describe('TagsComponent', () => {
|
|||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbAccordionModule,
|
NgbAccordionModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@@ -6,9 +6,7 @@
|
|||||||
}
|
}
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -27,7 +27,7 @@ describe('TextComponent', () => {
|
|||||||
|
|
||||||
it('should support use of input field', () => {
|
it('should support use of input field', () => {
|
||||||
expect(component.value).toBeUndefined()
|
expect(component.value).toBeUndefined()
|
||||||
// TODO: why doesnt this work?
|
// TODO: why doesn't this work?
|
||||||
// input.value = 'foo'
|
// input.value = 'foo'
|
||||||
// input.dispatchEvent(new Event('change'))
|
// input.dispatchEvent(new Event('change'))
|
||||||
// fixture.detectChanges()
|
// fixture.detectChanges()
|
||||||
|
@@ -4,27 +4,23 @@
|
|||||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
</button>
|
||||||
</svg> <ng-container i18n>Remove</ng-container>
|
}
|
||||||
</button>
|
</div>
|
||||||
}
|
<div [class.col-md-9]="horizontal">
|
||||||
</div>
|
<div class="input-group" [class.is-invalid]="error">
|
||||||
<div [class.col-md-9]="horizontal">
|
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||||
<div class="input-group" [class.is-invalid]="error">
|
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
|
||||||
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
<i-bs width="1.2em" height="1.2em" name="box-arrow-up-right"></i-bs>
|
||||||
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
|
</a>
|
||||||
<svg class="buttonicon mb-1" fill="currentColor">
|
<div class="invalid-feedback position-absolute top-100">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up-right" />
|
{{error}}
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div class="invalid-feedback position-absolute top-100">
|
|
||||||
{{error}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@if (hint) {
|
|
||||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (hint) {
|
||||||
|
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user