mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-18 09:26:25 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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 installation instructions](https://docs.paperless-ngx.com/setup/#installation).
|
||||
- [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).
|
||||
- type: textarea
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -169,7 +169,7 @@ jobs:
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
||||
|
||||
install-frontend-depedendencies:
|
||||
name: "Install Frontend Dependendencies"
|
||||
name: "Install Frontend Dependencies"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend depdendencies
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend depdendencies
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
||||
1
.github/workflows/crowdin.yml
vendored
1
.github/workflows/crowdin.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
paths: [
|
||||
'src/locale/**',
|
||||
'src-ui/messages.xlf',
|
||||
'src-ui/src/locale/**'
|
||||
]
|
||||
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`)
|
||||
|
||||
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!) {
|
||||
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||
@@ -107,3 +107,94 @@ jobs:
|
||||
|
||||
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
|
||||
- id: check-case-conflict
|
||||
- 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
|
||||
rev: 'v3.1.0'
|
||||
hooks:
|
||||
@@ -39,11 +47,11 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.1.5'
|
||||
rev: 'v0.1.11'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.11.0
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
# Dockerfile hooks
|
||||
|
||||
@@ -189,7 +189,7 @@ RUN set -eux \
|
||||
&& chmod 755 /usr/local/bin/paperless_cmd.sh \
|
||||
&& mv flower-conditional.sh /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 \
|
||||
&& ./install_management_commands.sh
|
||||
|
||||
|
||||
4
Pipfile
4
Pipfile
@@ -7,7 +7,7 @@ name = "pypi"
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=4.2.8"
|
||||
django = "~=4.2.9"
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
@@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
|
||||
[dev-packages]
|
||||
# Linting
|
||||
black = "==23.11.0"
|
||||
black = "*"
|
||||
pre-commit = "*"
|
||||
ruff = "*"
|
||||
# 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
|
||||
# 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
|
||||
|
||||
version: "3.7"
|
||||
|
||||
@@ -613,7 +613,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)
|
||||
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
|
||||
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ document. Paperless only reports PDF metadata at this point.
|
||||
|
||||
## Authorization
|
||||
|
||||
The REST api provides three different forms of authentication.
|
||||
The REST api provides four different forms of authentication.
|
||||
|
||||
1. Basic authentication
|
||||
|
||||
@@ -177,6 +177,12 @@ The REST api provides three different forms of authentication.
|
||||
|
||||
Tokens can also be managed in the Django admin.
|
||||
|
||||
4. Remote User authentication
|
||||
|
||||
If already setup (see
|
||||
[configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER)),
|
||||
you can authenticate against the API using Remote User auth.
|
||||
|
||||
## Searching for documents
|
||||
|
||||
Full text searching is available on the `/api/documents/` endpoint. Two
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
@@ -405,7 +455,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))
|
||||
- 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: 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))
|
||||
- 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))
|
||||
@@ -615,7 +665,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))
|
||||
- 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))
|
||||
- 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))
|
||||
- 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))
|
||||
@@ -647,11 +697,11 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
|
||||
### 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
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1318,7 +1368,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
|
||||
### 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))
|
||||
- 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))
|
||||
@@ -1373,7 +1423,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))
|
||||
- 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))
|
||||
- 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))
|
||||
- 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))
|
||||
@@ -1578,7 +1628,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))
|
||||
- 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] 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))
|
||||
|
||||
### Maintenance
|
||||
@@ -1909,7 +1959,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
||||
|
||||
### 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: 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))
|
||||
@@ -2320,7 +2370,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: 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))
|
||||
- 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))
|
||||
- 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))
|
||||
@@ -2441,7 +2491,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))
|
||||
- Fix imap tools bug [\@stumpylog](https://github.com/stumpylog)
|
||||
([\#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))
|
||||
- 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))
|
||||
@@ -3277,7 +3327,7 @@ primarily.
|
||||
[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR
|
||||
on documents. It still uses tesseract under the hood, but the
|
||||
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
|
||||
that can be selected in the front end.
|
||||
- Paperless stores archived versions of documents alongside with
|
||||
@@ -3328,7 +3378,7 @@ primarily.
|
||||
crash.
|
||||
- Mail handling no longer exits entirely when encountering errors.
|
||||
It will skip the account/rule/message on which the error
|
||||
occured.
|
||||
occurred.
|
||||
- Assigning correspondents from mail sender names failed for very
|
||||
long names. Paperless no longer assigns correspondents in these
|
||||
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.
|
||||
|
||||
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
|
||||
settings via environment variables. If not set, the environment setting or applicable
|
||||
default will be utilized instead.
|
||||
common [OCR](#ocr) related settings and some frontend settings. If set, these will take
|
||||
preference over the settings via environment variables. If not set, the environment setting
|
||||
or applicable default will be utilized instead.
|
||||
|
||||
- If you run paperless on docker, `paperless.conf` is not used.
|
||||
Rather, configure paperless by copying necessary options to
|
||||
@@ -1329,7 +1329,15 @@ started by the container.
|
||||
|
||||
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}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
- /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.
|
||||
|
||||
You may also need to change the default port that the webserver will
|
||||
|
||||
@@ -138,7 +138,7 @@ command:
|
||||
You might encounter errors such as:
|
||||
|
||||
```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
|
||||
|
||||
@@ -149,7 +149,7 @@ different means. These are as follows:
|
||||
- **Flag:** Sets the 'important' flag on mails with consumed
|
||||
documents. Paperless will not consume flagged mails.
|
||||
- **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
|
||||
documents (the IMAP standard calls these "keywords"). Paperless
|
||||
will not consume mails already tagged. Not all mail servers support
|
||||
@@ -411,7 +411,7 @@ The following custom field types are supported:
|
||||
|
||||
## 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.
|
||||
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
||||
|
||||
@@ -131,7 +131,7 @@ test('sorting', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Notes' }).click()
|
||||
await expect(page).toHaveURL(/sort=num_notes/)
|
||||
await page.getByRole('button', { name: 'Sort' }).click()
|
||||
await page.locator('.w-100 > label > .toolbaricon').first().click()
|
||||
await page.locator('.w-100 > label > i-bs').first().click()
|
||||
await expect(page).not.toHaveURL(/reverse=1/)
|
||||
})
|
||||
|
||||
|
||||
1441
src-ui/messages.xlf
1441
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",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^17.0.1",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
@@ -13850,6 +13851,22 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"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": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"bootstrap": "^5.3.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^17.0.1",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
|
||||
@@ -110,6 +110,175 @@ import { DocumentLinkComponent } from './components/common/input/document-link/d
|
||||
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
||||
import { SwitchComponent } from './components/common/input/switch/switch.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 localeAr from '@angular/common/locales/ar'
|
||||
@@ -267,6 +436,7 @@ function initializeApp(settings: SettingsService) {
|
||||
PreviewPopupComponent,
|
||||
SwitchComponent,
|
||||
ConfigComponent,
|
||||
FileComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -280,6 +450,7 @@ function initializeApp(settings: SettingsService) {
|
||||
ColorSliderModule,
|
||||
TourNgBootstrapModule,
|
||||
DragDropModule,
|
||||
NgxBootstrapIconsModule.pick(icons),
|
||||
],
|
||||
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">
|
||||
|
||||
@@ -17,9 +23,7 @@
|
||||
<h6>
|
||||
{{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">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
|
||||
</svg>
|
||||
<i-bs name="info-circle"></i-bs>
|
||||
</a>
|
||||
</h6>
|
||||
</div>
|
||||
@@ -27,9 +31,10 @@
|
||||
@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.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.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>
|
||||
|
||||
@@ -15,12 +15,15 @@ import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.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'
|
||||
|
||||
describe('ConfigComponent', () => {
|
||||
let component: ConfigComponent
|
||||
let fixture: ComponentFixture<ConfigComponent>
|
||||
let configService: ConfigService
|
||||
let toastService: ToastService
|
||||
let settingService: SettingsService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -30,6 +33,7 @@ describe('ConfigComponent', () => {
|
||||
SelectComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
FileComponent,
|
||||
PageHeaderComponent,
|
||||
],
|
||||
imports: [
|
||||
@@ -44,6 +48,7 @@ describe('ConfigComponent', () => {
|
||||
|
||||
configService = TestBed.inject(ConfigService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
settingService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(ConfigComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
@@ -100,4 +105,39 @@ describe('ConfigComponent', () => {
|
||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||
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 { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-config',
|
||||
@@ -55,7 +56,8 @@ export class ConfigComponent
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService
|
||||
private toastService: ToastService,
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
this.configForm.addControl('id', new FormControl())
|
||||
@@ -145,6 +147,7 @@ export class ConfigComponent
|
||||
this.loading = false
|
||||
this.initialize(config)
|
||||
this.store.next(config)
|
||||
this.settingsService.initializeSettings().subscribe()
|
||||
this.toastService.showInfo($localize`Configuration updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
@@ -160,4 +163,27 @@ export class ConfigComponent
|
||||
public discardChanges() {
|
||||
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>
|
||||
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
||||
<pngx-page-header
|
||||
title="Logs"
|
||||
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>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
OnInit,
|
||||
AfterViewChecked,
|
||||
ViewChild,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
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',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
})
|
||||
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
constructor(private logService: LogService) {}
|
||||
export class LogsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private changedetectorRef: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
public logs: string[] = []
|
||||
|
||||
@@ -47,10 +50,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
@@ -66,6 +65,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
next: (result) => {
|
||||
this.logs = result
|
||||
this.isLoading = false
|
||||
this.scrollToBottom()
|
||||
},
|
||||
error: () => {
|
||||
this.logs = []
|
||||
@@ -89,6 +89,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
this.changedetectorRef.detectChanges()
|
||||
this.logContainer?.nativeElement.scroll({
|
||||
top: this.logContainer.nativeElement.scrollHeight,
|
||||
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>
|
||||
<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>
|
||||
<svg class="sidebaricon ms-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
||||
</svg>
|
||||
<i-bs name="arrow-up-right"></i-bs>
|
||||
</a>
|
||||
</pngx-page-header>
|
||||
|
||||
@@ -135,204 +138,202 @@
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg><ng-container i18n>Reset</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
||||
</button>
|
||||
</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="offset-md-3 col">
|
||||
<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/>
|
||||
Actual updating of the app must still be performed manually.
|
||||
</p>
|
||||
<p i18n>
|
||||
<em>No tracking data is collected by the app in any way.</em>
|
||||
</p>
|
||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<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/>
|
||||
Actual updating of the app must still be performed manually.
|
||||
</p>
|
||||
<p i18n>
|
||||
<em>No tracking data is collected by the app in any way.</em>
|
||||
</p>
|
||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||
</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="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="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<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="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Notes</h4>
|
||||
<h4 class="mt-4" i18n>Notes</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.Permissions">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
<li [ngbNavItem]="SettingsNavIDs.Permissions">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Default Permissions</h4>
|
||||
<h4 i18n>Default Permissions</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<p i18n>
|
||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||
</p>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<p i18n>
|
||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||
</p>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Owner</span>
|
||||
</div>
|
||||
<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 class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Owner</span>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default View Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
|
||||
</ng-container>
|
||||
</div>
|
||||
<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 class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default View Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<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 class="col">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<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 class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Edit Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<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 class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Edit Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<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="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 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>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Document processing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<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 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 class="row">
|
||||
<div class="col-3">
|
||||
<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="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>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
|
||||
<a ngbNavLink i18n>Saved views</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4 i18n>Document processing</h4>
|
||||
|
||||
<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 class="row mb-3">
|
||||
<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 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>
|
||||
}
|
||||
|
||||
<h4 i18n>Views</h4>
|
||||
<div formGroupName="savedViews">
|
||||
@if (savedViews && savedViews.length === 0) {
|
||||
<div i18n>No saved views defined.</div>
|
||||
}
|
||||
|
||||
@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>
|
||||
}
|
||||
@if (!savedViews) {
|
||||
<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>
|
||||
}
|
||||
|
||||
@if (savedViews && savedViews.length === 0) {
|
||||
<div i18n>No saved views defined.</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!savedViews) {
|
||||
<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>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
</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>
|
||||
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||
</form>
|
||||
|
||||
@@ -363,7 +363,7 @@ export class SettingsComponent
|
||||
}
|
||||
|
||||
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.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">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
</div>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||
<i-bs name="check2-all"></i-bs> {{dismissButtonText}}
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
@if (!tasksService.completedFileTasks && tasksService.loading) {
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
}
|
||||
@if (!tasksService.completedFileTasks && tasksService.loading) {
|
||||
<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 let-tasks="tasks" #tasksTemplate>
|
||||
<table class="table table-striped align-middle border shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check">
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
<ng-template let-tasks="tasks" #tasksTemplate>
|
||||
<table class="table table-striped align-middle border shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check">
|
||||
<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>
|
||||
</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)">
|
||||
<li ngbNavItem="failed">
|
||||
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
||||
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="completed">
|
||||
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="started">
|
||||
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
|
||||
<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>
|
||||
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
|
||||
@if (tasks.length > 0) {
|
||||
<div class="pb-2 pb-sm-0">
|
||||
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
|
||||
@if (selectedTasks.size > 0) {
|
||||
<ng-container i18n> ({{selectedTasks.size}} selected)</ng-container>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (tasks.length > pageSize) {
|
||||
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
|
||||
<li ngbNavItem="failed">
|
||||
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
||||
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="completed">
|
||||
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="started">
|
||||
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@if (users) {
|
||||
<h4 class="d-flex">
|
||||
<ng-container i18n>Users</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add User</ng-container>
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add User</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group">
|
||||
@@ -29,14 +32,10 @@
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,10 +49,7 @@
|
||||
<h4 class="mt-4 d-flex">
|
||||
<ng-container i18n>Groups</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Group</ng-container>
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
@if (groups.length > 0) {
|
||||
@@ -75,14 +71,10 @@
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,30 +4,36 @@
|
||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<a class="navbar-brand 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"
|
||||
<a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
|
||||
[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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
||||
<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"
|
||||
transform="translate(0 0)" />
|
||||
</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>
|
||||
<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 }">
|
||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||
<svg width="1em" height="1em" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#search" />
|
||||
</svg>
|
||||
<i-bs style="top: .25em;" width="1em" height="1em" name="search"></i-bs>
|
||||
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
|
||||
(selectItem)="itemSelected($event)" i18n-placeholder>
|
||||
@if (!searchFieldEmpty) {
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
@@ -38,9 +44,7 @@
|
||||
<span class="small me-2 d-none d-sm-inline">
|
||||
{{this.settingsService.displayName}}
|
||||
</span>
|
||||
<svg width="1.3em" height="1.3em" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-circle" />
|
||||
</svg>
|
||||
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown">
|
||||
<div class="d-sm-none">
|
||||
@@ -48,27 +52,19 @@
|
||||
<div class="dropdown-divider"></div>
|
||||
</div>
|
||||
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person" />
|
||||
</svg><ng-container i18n>My Profile</ng-container>
|
||||
<i-bs class="me-2" name="person"></i-bs> <ng-container i18n>My Profile</ng-container>
|
||||
</button>
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear" />
|
||||
</svg><ng-container i18n>Settings</ng-container>
|
||||
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
|
||||
</a>
|
||||
<a ngbDropdownItem class="nav-link" href="accounts/logout/" (click)="onLogout()">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#door-open" />
|
||||
</svg><ng-container i18n>Logout</ng-container>
|
||||
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
|
||||
<i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
|
||||
href="https://docs.paperless-ngx.com">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
|
||||
</svg><ng-container i18n>Documentation</ng-container>
|
||||
<i-bs class="me-2" name="question-circle"></i-bs><ng-container i18n>Documentation</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
@@ -81,13 +77,11 @@
|
||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
||||
[ngbCollapse]="isMenuCollapsed">
|
||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||
<svg class="sidebaricon-sm" fill="currentColor">
|
||||
@if (slimSidebarEnabled) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-right" />
|
||||
} @else {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left" />
|
||||
}
|
||||
</svg>
|
||||
@if (slimSidebarEnabled) {
|
||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
||||
} @else {
|
||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
||||
}
|
||||
</button>
|
||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||
<ul class="nav flex-column">
|
||||
@@ -95,18 +89,14 @@
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#house" />
|
||||
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||
<i-bs class="me-1" name="house"></i-bs><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#files" />
|
||||
</svg><span> <ng-container i18n>Documents</ng-container></span>
|
||||
<i-bs class="me-1" name="files"></i-bs><span> <ng-container i18n>Documents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -128,15 +118,11 @@
|
||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||
popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
||||
</svg><span> {{view.name}}</span>
|
||||
<i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}}</span>
|
||||
</a>
|
||||
@if (settingsService.organizingSidebarSavedViews) {
|
||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
||||
<svg class="sidebaricon text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical" />
|
||||
</svg>
|
||||
<i-bs name="grip-vertical"></i-bs>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
@@ -157,13 +143,9 @@
|
||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||
popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text" />
|
||||
</svg><span> {{d.title | documentTitle}}</span>
|
||||
<i-bs class="me-1" name="file-text"></i-bs><span> {{d.title | documentTitle}}</span>
|
||||
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
||||
<svg fill="currentColor" class="toolbaricon">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||
</svg>
|
||||
<i-bs name="x"></i-bs>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -173,9 +155,7 @@
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
||||
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||
<i-bs class="me-1" name="x"></i-bs><span> <ng-container i18n>Close all</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@@ -191,9 +171,7 @@
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person" />
|
||||
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<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"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#tags" />
|
||||
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
@@ -211,27 +187,21 @@
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hash" />
|
||||
</svg><span> <ng-container i18n>Document Types</ng-container></span>
|
||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder" />
|
||||
</svg><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
|
||||
</svg><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
@@ -240,9 +210,7 @@
|
||||
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#boxes" />
|
||||
</svg><span> <ng-container i18n>Workflows</ng-container></span>
|
||||
<i-bs class="me-1" name="boxes"></i-bs><span> <ng-container i18n>Workflows</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<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"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#envelope" />
|
||||
</svg><span> <ng-container i18n>Mail</ng-container></span>
|
||||
<i-bs class="me-1" name="envelope"></i-bs><span> <ng-container i18n>Mail</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -266,27 +232,21 @@
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear" />
|
||||
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
||||
<i-bs class="me-1" name="gear"></i-bs><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sliders2-vertical" />
|
||||
</svg><span> <ng-container i18n>Configuration</ng-container></span>
|
||||
<i-bs class="me-1" name="sliders2-vertical"></i-bs><span> <ng-container i18n>Configuration</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#people" />
|
||||
</svg><span> <ng-container i18n>Users & Groups</ng-container></span>
|
||||
<i-bs class="me-1" name="people"></i-bs><span> <ng-container i18n>Users & Groups</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
@@ -295,23 +255,19 @@
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
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) {
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-left" />
|
||||
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
||||
<i-bs class="me-1" name="text-left"></i-bs><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<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"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
|
||||
</svg><span class="ms-1"> <ng-container i18n>Documentation</ng-container></span>
|
||||
<i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1"> <ng-container i18n>Documentation</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||
@@ -360,10 +314,7 @@
|
||||
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
||||
container="body">
|
||||
<svg fill="currentColor" class="me-1" 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>
|
||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||
@if (appRemoteVersion?.update_available) {
|
||||
<ng-container i18n>Update available</ng-container>
|
||||
}
|
||||
@@ -373,10 +324,7 @@
|
||||
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
||||
container="body">
|
||||
<svg fill="currentColor" class="me-1" 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>
|
||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -152,9 +152,9 @@ main {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidebaricon {
|
||||
margin-right: 4px;
|
||||
color: inherit;
|
||||
i-bs {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,11 +186,11 @@ main {
|
||||
width: 1.8rem;
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
i-bs {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
&:hover i-bs {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ main {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
svg {
|
||||
i-bs {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
@@ -217,9 +217,16 @@ main {
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 1rem;
|
||||
|
||||
.flex-column {
|
||||
padding: 0.15rem 0;
|
||||
}
|
||||
|
||||
.byline {
|
||||
font-size: 0.5rem;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
@@ -241,7 +248,7 @@ main {
|
||||
.navbar .dropdown-menu {
|
||||
font-size: 0.875rem; // body size
|
||||
|
||||
a svg {
|
||||
a i-bs {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
@@ -252,7 +259,7 @@ main {
|
||||
form {
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
> i-bs {
|
||||
position: absolute;
|
||||
left: 0.6rem;
|
||||
top: 0.5rem;
|
||||
@@ -262,7 +269,7 @@ main {
|
||||
|
||||
|
||||
&:focus-within {
|
||||
form > svg {
|
||||
form > i-bs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -316,6 +323,6 @@ main {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
::ng-deep .navItemDrag .position-absolute svg {
|
||||
::ng-deep .navItemDrag .position-absolute i-bs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('AppFrameComponent', () => {
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support collapsable menu', () => {
|
||||
it('should support collapsible menu', () => {
|
||||
const button: HTMLButtonElement = (
|
||||
fixture.nativeElement as HTMLDivElement
|
||||
).querySelector('button[data-toggle=collapse]')
|
||||
|
||||
@@ -102,6 +102,10 @@ export class AppFrameComponent
|
||||
}, 200) // slightly longer than css animation for slim sidebar
|
||||
}
|
||||
|
||||
get customAppTitle(): string {
|
||||
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||
}
|
||||
|
||||
get slimSidebarEnabled(): boolean {
|
||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
@if (active) {
|
||||
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
||||
@if (!isNumbered && selected) {
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
|
||||
</svg>
|
||||
<i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs>
|
||||
}
|
||||
@if (isNumbered) {
|
||||
<div class="number">{{number}}<span class="visually-hidden">selected</span></div>
|
||||
}
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
|
||||
</svg>
|
||||
<i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs>
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ button:hover {
|
||||
.x {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: calc(50% - 4px);
|
||||
top: .4em;
|
||||
left: calc(50% - .4em);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span>
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||
<span>
|
||||
|
||||
@@ -37,6 +37,12 @@ export class ConfirmDialogComponent {
|
||||
@Input()
|
||||
alternativeBtnCaption
|
||||
|
||||
@Input()
|
||||
cancelBtnClass = 'btn-outline-secondary'
|
||||
|
||||
@Input()
|
||||
cancelBtnCaption = $localize`Cancel`
|
||||
|
||||
@Input()
|
||||
buttonsEnabled = true
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()">
|
||||
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
|
||||
</svg>
|
||||
<i-bs name="ui-radios"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
||||
@@ -20,14 +18,10 @@
|
||||
</pngx-input-select>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#asterisk"/>
|
||||
</svg><ng-container i18n>Create New Field</ng-container>
|
||||
<i-bs width="1em" height="1em" name="asterisk"></i-bs> <ng-container i18n>Create New Field</ng-container>
|
||||
</button>
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle"/>
|
||||
</svg><ng-container i18n>Add</ng-container>
|
||||
<i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs> <ng-container i18n>Add</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -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)">
|
||||
<div class="selected-icon">
|
||||
@if (relativeDate === rd.id) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
@@ -32,9 +30,7 @@
|
||||
<div i18n>After</div>
|
||||
@if (dateAfter) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
@@ -44,9 +40,7 @@
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -57,9 +51,7 @@
|
||||
<div i18n>Before</div>
|
||||
@if (dateBefore) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
@@ -69,9 +61,7 @@
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
this.closeEnabled = true
|
||||
})
|
||||
|
||||
@@ -27,10 +27,7 @@
|
||||
<div class="d-flex">
|
||||
<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()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Trigger</ng-container>
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Trigger</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordion [closeOthers]="true">
|
||||
@@ -42,10 +39,7 @@
|
||||
<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)">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
@@ -71,10 +65,7 @@
|
||||
<div class="d-flex">
|
||||
<p class="p-2" i18n>Apply Actions:</p>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Action</ng-container>
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Action</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
@@ -99,7 +87,7 @@
|
||||
<input type="hidden" formControlName="id" />
|
||||
<div class="row">
|
||||
<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-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>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
<i-bs name="{{icon}}"></i-bs>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
@if (!editing && selectionModel.totalCount > 0) {
|
||||
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
|
||||
@@ -49,9 +47,7 @@
|
||||
@if (editing) {
|
||||
<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>
|
||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
</svg>
|
||||
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
||||
</button>
|
||||
}
|
||||
@if (!editing && manyToOne) {
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
small > svg {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.list-group-item-note {
|
||||
line-height: 1;
|
||||
|
||||
|
||||
@@ -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">
|
||||
<div class="selected-icon me-1">
|
||||
@if (isChecked()) {
|
||||
<svg fill="currentColor" class="buttonicon-sm bi-check">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
@if (isPartiallyChecked()) {
|
||||
<svg fill="currentColor" class="buttonicon-sm bi-dash">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#dash"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="dash"></i-bs>
|
||||
}
|
||||
@if (isExcluded()) {
|
||||
<svg fill="currentColor" class="buttonicon-sm bi-x">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1">
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
<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)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</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">
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<i-bs name="dice5"></i-bs>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<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)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -16,15 +14,11 @@
|
||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||
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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon">
|
||||
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="calendar"></i-bs>
|
||||
</button>
|
||||
@if (showFilter) {
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -6,51 +6,45 @@
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</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>
|
||||
<i-bs name="x"></i-bs> <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">
|
||||
<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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('NumberComponent', () => {
|
||||
component.step = 0.1
|
||||
component.writeValue(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)
|
||||
expect(component.value).toEqual(11.1)
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
@if (showReveal) {
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#eye" />
|
||||
</svg>
|
||||
<i-bs name="eye"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('PasswordComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -40,16 +38,12 @@
|
||||
</ng-select>
|
||||
@if (allowCreateNew) {
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
|
||||
</button>
|
||||
}
|
||||
@if (showFilter) {
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
<div class="row">
|
||||
@if (!horizontal) {
|
||||
<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) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -16,7 +19,12 @@
|
||||
<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">
|
||||
@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) {
|
||||
<div class="form-text text-muted">{{hint}}</div>
|
||||
@@ -25,3 +33,8 @@
|
||||
</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,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
describe('SwitchComponent', () => {
|
||||
let component: SwitchComponent
|
||||
@@ -15,7 +16,7 @@ describe('SwitchComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SwitchComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgbTooltipModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(SwitchComponent)
|
||||
@@ -36,4 +37,9 @@ describe('SwitchComponent', () => {
|
||||
fixture.detectChanges()
|
||||
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 { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@@ -15,7 +15,14 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
styleUrls: ['./switch.component.scss'],
|
||||
})
|
||||
export class SwitchComponent extends AbstractInputComponent<boolean> {
|
||||
@Input()
|
||||
showUnsetNote: boolean = false
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
get isUnset(): boolean {
|
||||
return this.value === null || this.value === undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<i-bs name="x"></i-bs>
|
||||
@if (item.id && tags) {
|
||||
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
|
||||
}
|
||||
@@ -36,16 +34,12 @@
|
||||
</ng-select>
|
||||
@if (allowCreate) {
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
|
||||
</button>
|
||||
}
|
||||
@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">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('TextComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
|
||||
@@ -4,27 +4,23 @@
|
||||
<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)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div [class.col-md-9]="horizontal">
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
|
||||
<svg class="buttonicon mb-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up-right" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
</div>
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div [class.col-md-9]="horizontal">
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
|
||||
<i-bs width="1.2em" height="1.2em" name="box-arrow-up-right"></i-bs>
|
||||
</a>
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
</div>
|
||||
@if (hint) {
|
||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
}
|
||||
</div>
|
||||
@if (hint) {
|
||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('TextComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
|
||||
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
||||
<g class="text" style="fill:#000">
|
||||
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
|
||||
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
|
||||
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
|
||||
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
|
||||
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
|
||||
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
|
||||
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
|
||||
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
|
||||
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
|
||||
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
|
||||
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
|
||||
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
|
||||
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
@if (customLogo) {
|
||||
<img src="{{customLogo}}" height="100%" width="100%" [attr.style]="'height:'+height" />
|
||||
} @else {
|
||||
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
|
||||
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
||||
<g class="text" style="fill:#000">
|
||||
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
|
||||
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
|
||||
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
|
||||
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
|
||||
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
|
||||
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
|
||||
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
|
||||
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
|
||||
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
|
||||
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
|
||||
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
|
||||
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
|
||||
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -2,15 +2,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { LogoComponent } from './logo.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
|
||||
describe('LogoComponent', () => {
|
||||
let component: LogoComponent
|
||||
let fixture: ComponentFixture<LogoComponent>
|
||||
let settingsService: SettingsService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogoComponent],
|
||||
imports: [HttpClientTestingModule],
|
||||
})
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(LogoComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
@@ -33,4 +39,9 @@ describe('LogoComponent', () => {
|
||||
'height:10em'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support getting custom logo', () => {
|
||||
settingsService.set(SETTINGS_KEYS.APP_LOGO, '/logo/test.png')
|
||||
expect(component.customLogo).toEqual('http://localhost:8000/logo/test.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-logo',
|
||||
@@ -12,6 +15,17 @@ export class LogoComponent {
|
||||
@Input()
|
||||
height = '6em'
|
||||
|
||||
get customLogo(): string {
|
||||
return this.settingsService.get(SETTINGS_KEYS.APP_LOGO)?.length
|
||||
? environment.apiBaseUrl.replace(
|
||||
/\/api\/$/,
|
||||
this.settingsService.get(SETTINGS_KEYS.APP_LOGO)
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
constructor(private settingsService: SettingsService) {}
|
||||
|
||||
getClasses() {
|
||||
return ['logo'].concat(this.extra_classes).join(' ')
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
@if (subTitle) {
|
||||
<span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
|
||||
}
|
||||
@if (info) {
|
||||
<button class="btn btn-sm btn-link text-muted me-auto p-0 p-md-2" title="What's this?" i18n-title type="button" [ngbPopover]="infoPopover" [autoClose]="true">
|
||||
<i-bs name="question-circle"></i-bs>
|
||||
</button>
|
||||
<ng-template #infoPopover>
|
||||
<p [class.mb-0]="!infoLink" [innerHTML]="info"></p>
|
||||
@if (infoLink) {
|
||||
<a href="https://docs.paperless-ngx.com/{{infoLink}}" target="_blank" referrerpolicy="noopener noreferrer" i18n>Read more</a>
|
||||
<i-bs class="ms-1" width=".8em" height=".8em" name="box-arrow-up-right"></i-bs>
|
||||
}
|
||||
</ng-template>
|
||||
}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="btn-toolbar col col-md-auto">
|
||||
|
||||
@@ -24,4 +24,10 @@ export class PageHeaderComponent {
|
||||
|
||||
@Input()
|
||||
subTitle: string = ''
|
||||
|
||||
@Input()
|
||||
info: string
|
||||
|
||||
@Input()
|
||||
infoLink: string
|
||||
}
|
||||
|
||||
@@ -465,33 +465,42 @@ export class PdfViewerComponent
|
||||
|
||||
this.clear()
|
||||
|
||||
this.setupViewer()
|
||||
|
||||
this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
|
||||
|
||||
this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
|
||||
this.onProgress.emit(progressData)
|
||||
if (this.pdfViewer) {
|
||||
this.pdfViewer._resetView()
|
||||
this.pdfViewer = null
|
||||
}
|
||||
|
||||
const src = this.src
|
||||
this.setupViewer()
|
||||
|
||||
from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (pdf) => {
|
||||
this._pdf = pdf
|
||||
this.lastLoaded = src
|
||||
try {
|
||||
this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
|
||||
|
||||
this.afterLoadComplete.emit(pdf)
|
||||
this.resetPdfDocument()
|
||||
this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
|
||||
this.onProgress.emit(progressData)
|
||||
}
|
||||
|
||||
this.update()
|
||||
},
|
||||
error: (error) => {
|
||||
this.lastLoaded = null
|
||||
this.onError.emit(error)
|
||||
},
|
||||
})
|
||||
const src = this.src
|
||||
|
||||
from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (pdf) => {
|
||||
this._pdf = pdf
|
||||
this.lastLoaded = src
|
||||
|
||||
this.afterLoadComplete.emit(pdf)
|
||||
this.resetPdfDocument()
|
||||
|
||||
this.update()
|
||||
},
|
||||
error: (error) => {
|
||||
this.lastLoaded = null
|
||||
this.onError.emit(error)
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
this.onError.emit(e)
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
|
||||
</svg>
|
||||
<i-bs name="person-fill-lock"></i-bs>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||
</button>
|
||||
@@ -11,9 +9,7 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
|
||||
<div class="selected-icon me-1">
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.NONE) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1">
|
||||
@@ -23,9 +19,7 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
|
||||
<div class="selected-icon me-1">
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.SELF) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1">
|
||||
@@ -35,9 +29,7 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
|
||||
<div class="selected-icon me-1">
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1">
|
||||
@@ -47,9 +39,7 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
|
||||
<div class="selected-icon me-1">
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1">
|
||||
@@ -59,9 +49,7 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
|
||||
<div class="selected-icon me-1">
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1">
|
||||
@@ -71,9 +59,7 @@
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" 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" [disabled]="disabled">
|
||||
<div class="selected-icon me-1">
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.OTHERS) {
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1 w-100">
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
} @else {
|
||||
@if (requiresPassword) {
|
||||
<div class="w-100 h-100 position-relative">
|
||||
<svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/>
|
||||
</svg>
|
||||
<i-bs width="2em" height="2em" class="position-absolute top-50 start-50 translate-middle" name="file-earmark-lock"></i-bs>
|
||||
</div>
|
||||
}
|
||||
@if (!requiresPassword) {
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('PreviewPopupComponent', () => {
|
||||
component.onError({ name: 'PasswordException' })
|
||||
fixture.detectChanges()
|
||||
expect(component.requiresPassword).toBeTruthy()
|
||||
expect(fixture.debugElement.query(By.css('svg'))).not.toBeNull()
|
||||
expect(fixture.debugElement.query(By.css('i-bs'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should fall back to object for non-pdf', () => {
|
||||
|
||||
@@ -33,19 +33,16 @@
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" formControlName="auth_token" readonly>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
@if (!copied) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
|
||||
<i-bs width="1em" height="1em" name="clipboard-fill"></i-bs>
|
||||
}
|
||||
@if (copied) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
|
||||
<i-bs width="1em" height="1em" name="clipboard-check-fill"></i-bs>
|
||||
}
|
||||
</svg><span class="visually-hidden" i18n>Copy</span>
|
||||
<span class="visually-hidden" i18n>Copy</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="arrow-repeat"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" id="shareLinksDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#link" />
|
||||
</svg>
|
||||
<i-bs name="link"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Share Links</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown">
|
||||
@@ -22,26 +20,21 @@
|
||||
</span>
|
||||
}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
@if (copied !== link.id) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
|
||||
<i-bs width="1.2em" height="1.2em" name="clipboard-fill"></i-bs>
|
||||
}
|
||||
@if (copied === link.id) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
|
||||
<i-bs width="1.2em" height="1.2em" name="clipboard-check-fill"></i-bs>
|
||||
}
|
||||
</svg><span class="visually-hidden" i18n>Copy</span>
|
||||
<span class="visually-hidden" i18n>Copy</span>
|
||||
</button>
|
||||
@if (canShare(link)) {
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" />
|
||||
</svg><span class="visually-hidden" i18n>Share</span>
|
||||
<i-bs width="1.2em" height="1.2em" name="box-arrow-up"></i-bs><span class="visually-hidden" i18n>Share</span>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg><span class="visually-hidden" i18n>Delete</span>
|
||||
<i-bs width="1.2em" height="1.2em" name="trash"></i-bs><span class="visually-hidden" i18n>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied === link.id" i18n>Copied!</span>
|
||||
@@ -66,9 +59,7 @@
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
}
|
||||
@if (!loading) {
|
||||
<svg class="buttonicon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
<i-bs name="plus"></i-bs>
|
||||
}
|
||||
<ng-container i18n>Create</ng-container>
|
||||
</button>
|
||||
|
||||
@@ -8,16 +8,14 @@
|
||||
<ngb-progressbar class="position-absolute h-100 w-100 top-90 start-0 bottom-0 end-0 pe-none" type="dark" [max]="toast.delay" [value]="toast.delayRemaining"></ngb-progressbar>
|
||||
<span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span>
|
||||
<div class="d-flex align-items-top">
|
||||
<svg class="sidebaricon-sm mt-1 me-2 flex-shrink-0" fill="currentColor">
|
||||
@if (!toast.error) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
|
||||
}
|
||||
@if (toast.error) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
|
||||
}
|
||||
</svg>
|
||||
@if (!toast.error) {
|
||||
<i-bs width="0.9em" height="0.9em" name="info-circle"></i-bs>
|
||||
}
|
||||
@if (toast.error) {
|
||||
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
|
||||
}
|
||||
<div>
|
||||
<p class="mb-0">{{toast.content}}</p>
|
||||
<p class="ms-2 mb-0">{{toast.content}}</p>
|
||||
@if (toast.error) {
|
||||
<details>
|
||||
<div class="mt-2">
|
||||
@@ -34,25 +32,24 @@
|
||||
<div class="row">
|
||||
<div class="col offset-sm-3">
|
||||
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
@if (!copied) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#clipboard" />
|
||||
}
|
||||
@if (copied) {
|
||||
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
|
||||
}
|
||||
</svg> <ng-container i18n>Copy Raw Error</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@if (!copied) {
|
||||
<i-bs name="clipboard"></i-bs>
|
||||
}
|
||||
@if (copied) {
|
||||
<i-bs name="clipboard-check"></i-bs>
|
||||
}
|
||||
<ng-container i18n>Copy Raw Error</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
@if (toast.action) {
|
||||
<p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
|
||||
}
|
||||
</div>
|
||||
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
@if (toast.action) {
|
||||
<p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
|
||||
}
|
||||
</div>
|
||||
</ngb-toast>
|
||||
}
|
||||
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button>
|
||||
</div>
|
||||
</ngb-toast>
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import { ComponentWithPermissions } from '../with-permissions/with-permissions.c
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import {
|
||||
CdkDragDrop,
|
||||
CdkDragEnd,
|
||||
CdkDragStart,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-dashboard',
|
||||
@@ -35,9 +35,9 @@ export class DashboardComponent extends ComponentWithPermissions {
|
||||
|
||||
get subtitle() {
|
||||
if (this.settingsService.displayName) {
|
||||
return $localize`Hello ${this.settingsService.displayName}, welcome to Paperless-ngx`
|
||||
return $localize`Hello ${this.settingsService.displayName}, welcome to ${environment.appTitle}`
|
||||
} else {
|
||||
return $localize`Welcome to Paperless-ngx`
|
||||
return $localize`Welcome to ${environment.appTitle}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,17 +39,13 @@
|
||||
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
|
||||
<svg class="buttonicon-xs" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#eye"/>
|
||||
</svg>
|
||||
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
|
||||
</ng-template>
|
||||
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
|
||||
<svg class="buttonicon-xs" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#download"/>
|
||||
</svg>
|
||||
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -14,10 +14,7 @@
|
||||
@if (getStatusCompleted().length > 0) {
|
||||
<a class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
|
||||
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
|
||||
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
|
||||
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
|
||||
</svg>
|
||||
<i-bs name="check2-all"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@@ -64,9 +61,7 @@
|
||||
@if (status.documentId) {
|
||||
<button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
<i-bs name="arrow-right-short"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ form {
|
||||
.btn-open {
|
||||
line-height: 1;
|
||||
|
||||
svg {
|
||||
i-bs {
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<div class="d-flex">
|
||||
@if (draggable) {
|
||||
<div class="ms-n2 me-1" cdkDragHandle>
|
||||
<svg class="sidebaricon text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
|
||||
</svg>
|
||||
<i-bs name="grip-vertical"></i-bs>
|
||||
</div>
|
||||
}
|
||||
<h6 class="card-title mb-0">{{title}}</h6>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
svg {
|
||||
i-bs {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
@@ -19,363 +19,347 @@
|
||||
}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-danger me-4" (click)="delete()" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
||||
</button>
|
||||
<i-bs width="1.2em" height="1.2em" name="trash"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
||||
</button>
|
||||
|
||||
<div class="btn-group me-2">
|
||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||
<svg class="buttonicon me-md-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
||||
</svg><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</a>
|
||||
<div class="btn-group me-2">
|
||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</a>
|
||||
|
||||
@if (metadata?.has_archive_version) {
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (metadata?.has_archive_version) {
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ms-auto" ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
||||
</svg>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
|
||||
<button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
|
||||
</svg><span class="ps-1" i18n>Redo OCR</span>
|
||||
</button>
|
||||
<div class="ms-auto" ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle>
|
||||
<i-bs name="three-dots"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
|
||||
<button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit">
|
||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs><span class="ps-1" i18n>Redo OCR</span>
|
||||
</button>
|
||||
|
||||
<button ngbDropdownItem (click)="moreLike()">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#diagram-3" />
|
||||
</svg><span class="ps-1" i18n>More like this</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button ngbDropdownItem (click)="moreLike()">
|
||||
<i-bs width="1em" height="1em" name="diagram-3"></i-bs><span class="ps-1" i18n>More like this</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pngx-custom-fields-dropdown
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
|
||||
class="me-2"
|
||||
[documentId]="documentId"
|
||||
[disabled]="!userIsOwner"
|
||||
[existingFields]="document?.custom_fields"
|
||||
(created)="refreshCustomFields()"
|
||||
(added)="addField($event)">
|
||||
</pngx-custom-fields-dropdown>
|
||||
<pngx-custom-fields-dropdown
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
|
||||
class="me-2"
|
||||
[documentId]="documentId"
|
||||
[disabled]="!userIsOwner"
|
||||
[existingFields]="document?.custom_fields"
|
||||
(created)="refreshCustomFields()"
|
||||
(added)="addField($event)">
|
||||
</pngx-custom-fields-dropdown>
|
||||
|
||||
<pngx-share-links-dropdown [documentId]="documentId" [hasArchiveVersion]="!!document?.archived_file_name" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ShareLink }"></pngx-share-links-dropdown>
|
||||
</pngx-page-header>
|
||||
<pngx-share-links-dropdown [documentId]="documentId" [hasArchiveVersion]="!!document?.archived_file_name" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ShareLink }"></pngx-share-links-dropdown>
|
||||
</pngx-page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xl-4 mb-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xl-4 mb-4">
|
||||
|
||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||
|
||||
<div class="btn-toolbar mb-1 pb-3 border-bottom">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="!hasPrevious()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-left" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Next" (click)="nextDoc()" [disabled]="!hasNext()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
||||
</div>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Details">
|
||||
<a ngbNavLink i18n>Details</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div>
|
||||
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
|
||||
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
|
||||
<pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
[error]="error?.created_date"></pngx-input-date>
|
||||
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
|
||||
<pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
|
||||
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
|
||||
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
|
||||
@for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
|
||||
<div [formGroup]="customFieldFormFields.controls[i]">
|
||||
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
|
||||
@case (PaperlessCustomFieldDataType.String) {
|
||||
<pngx-input-text formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-text>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Date) {
|
||||
<pngx-input-date formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-date>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Integer) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Float) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".1"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Monetary) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".01"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Boolean) {
|
||||
<pngx-input-check formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"></pngx-input-check>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Url) {
|
||||
<pngx-input-url formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-url>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.DocumentLink) {
|
||||
<pngx-input-document-link formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[parentDocumentID]="documentId"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-document-link>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-flex border-top pt-3">
|
||||
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Content">
|
||||
<a ngbNavLink i18n>Content</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div>
|
||||
<textarea class="form-control" id="content" rows="20" formControlName='content' [class.rtl]="isRTL"></textarea>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Metadata">
|
||||
<a ngbNavLink i18n>Metadata</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
@if (document) {
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n>Date modified</td>
|
||||
<td>{{document.modified | customDate}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Date added</td>
|
||||
<td>{{document.added | customDate}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Media filename</td>
|
||||
<td>{{metadata?.media_filename}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original filename</td>
|
||||
<td>{{metadata?.original_filename}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original MD5 checksum</td>
|
||||
<td>{{metadata?.original_checksum}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original file size</td>
|
||||
<td>{{metadata?.original_size | fileSize}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original mime type</td>
|
||||
<td>{{metadata?.original_mime_type}}</td>
|
||||
</tr>
|
||||
@if (metadata?.has_archive_version) {
|
||||
<tr>
|
||||
<td i18n>Archive MD5 checksum</td>
|
||||
<td>{{metadata?.archive_checksum}}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (metadata?.has_archive_version) {
|
||||
<tr>
|
||||
<td i18n>Archive file size</td>
|
||||
<td>{{metadata?.archive_size | fileSize}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (metadata?.original_metadata?.length > 0) {
|
||||
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata"></pngx-metadata-collapse>
|
||||
}
|
||||
@if (metadata?.archive_metadata?.length > 0) {
|
||||
<pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata"></pngx-metadata-collapse>
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none">
|
||||
<a ngbNavLink i18n>Preview</a>
|
||||
@if (!pdfPreview.offsetParent) {
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
</ng-template>
|
||||
}
|
||||
</li>
|
||||
|
||||
@if (notesEnabled) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Notes">
|
||||
<a class="text-nowrap" ngbNavLink i18n>Notes @if (document?.notes.length) {
|
||||
<span class="badge text-bg-secondary ms-1">{{document.notes.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (showPermissions) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mb-3">
|
||||
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
@if (renderAsPlainText) {
|
||||
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
|
||||
}
|
||||
@if (requiresPassword) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||
|
||||
<div class="btn-toolbar mb-1 border-bottom">
|
||||
<div class="btn-group pb-3">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">
|
||||
<i-bs width="1.2em" height="1.2em" name="x"></i-bs>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="!hasPrevious()">
|
||||
<i-bs width="1.2em" height="1.2em" name="arrow-left"></i-bs>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Next" (click)="nextDoc()" [disabled]="!hasNext()">
|
||||
<i-bs width="1.2em" height="1.2em" name="arrow-right"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template #saveButtons>
|
||||
<div class="btn-group ms-auto">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
|
||||
@if (hasNext()) {
|
||||
<button type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & next</button>
|
||||
}
|
||||
@if (!hasNext()) {
|
||||
<button type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & close</button>
|
||||
}
|
||||
</ng-container>
|
||||
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #previewContent>
|
||||
@if (!metadata) {
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Details">
|
||||
<a ngbNavLink i18n>Details</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div>
|
||||
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
|
||||
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
|
||||
<pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
[error]="error?.created_date"></pngx-input-date>
|
||||
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
|
||||
<pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
|
||||
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
|
||||
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
|
||||
@for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
|
||||
<div [formGroup]="customFieldFormFields.controls[i]">
|
||||
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
|
||||
@case (PaperlessCustomFieldDataType.String) {
|
||||
<pngx-input-text formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-text>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Date) {
|
||||
<pngx-input-date formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-date>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Integer) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Float) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".1"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Monetary) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".01"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Boolean) {
|
||||
<pngx-input-check formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"></pngx-input-check>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Url) {
|
||||
<pngx-input-url formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-url>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.DocumentLink) {
|
||||
<pngx-input-document-link formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[parentDocumentID]="documentId"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-document-link>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (getContentType() === 'application/pdf') {
|
||||
@if (!useNativePdfViewer ) {
|
||||
<div class="preview-sticky pdf-viewer-container">
|
||||
<pngx-pdf-viewer
|
||||
[src]="{ url: previewUrl, password: password }"
|
||||
[original-size]="false"
|
||||
[show-borders]="true"
|
||||
[show-all]="true"
|
||||
[(page)]="previewCurrentPage"
|
||||
[zoom-scale]="previewZoomScale"
|
||||
[zoom]="previewZoomSetting"
|
||||
(error)="onError($event)"
|
||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
||||
</pngx-pdf-viewer>
|
||||
</div>
|
||||
} @else {
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
|
||||
<div class="d-flex border-top pt-3">
|
||||
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Content">
|
||||
<a ngbNavLink i18n>Content</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div>
|
||||
<textarea class="form-control" id="content" rows="20" formControlName='content' [class.rtl]="isRTL"></textarea>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Metadata">
|
||||
<a ngbNavLink i18n>Metadata</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
@if (document) {
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n>Date modified</td>
|
||||
<td>{{document.modified | customDate}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Date added</td>
|
||||
<td>{{document.added | customDate}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Media filename</td>
|
||||
<td>{{metadata?.media_filename}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original filename</td>
|
||||
<td>{{metadata?.original_filename}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original MD5 checksum</td>
|
||||
<td>{{metadata?.original_checksum}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original file size</td>
|
||||
<td>{{metadata?.original_size | fileSize}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Original mime type</td>
|
||||
<td>{{metadata?.original_mime_type}}</td>
|
||||
</tr>
|
||||
@if (metadata?.has_archive_version) {
|
||||
<tr>
|
||||
<td i18n>Archive MD5 checksum</td>
|
||||
<td>{{metadata?.archive_checksum}}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (metadata?.has_archive_version) {
|
||||
<tr>
|
||||
<td i18n>Archive file size</td>
|
||||
<td>{{metadata?.archive_size | fileSize}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (metadata?.original_metadata?.length > 0) {
|
||||
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata"></pngx-metadata-collapse>
|
||||
}
|
||||
@if (metadata?.archive_metadata?.length > 0) {
|
||||
<pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata"></pngx-metadata-collapse>
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none">
|
||||
<a ngbNavLink i18n>Preview</a>
|
||||
@if (!pdfPreview.offsetParent) {
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
</ng-template>
|
||||
}
|
||||
@if (renderAsPlainText) {
|
||||
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
|
||||
}
|
||||
@if (showPasswordField) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
@if (notesEnabled) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Notes">
|
||||
<a class="text-nowrap" ngbNavLink i18n>Notes @if (document?.notes.length) {
|
||||
<span class="badge text-bg-secondary ms-1">{{document.notes.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (showPermissions) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mb-3">
|
||||
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
@if (renderAsPlainText) {
|
||||
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
|
||||
}
|
||||
@if (requiresPassword) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #saveButtons>
|
||||
<div class="btn-group pb-3 ms-auto">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
|
||||
@if (hasNext()) {
|
||||
<button type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & next</button>
|
||||
}
|
||||
@if (!hasNext()) {
|
||||
<button type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & close</button>
|
||||
}
|
||||
</ng-container>
|
||||
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #previewContent>
|
||||
@if (!metadata) {
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (getContentType() === 'application/pdf') {
|
||||
@if (!useNativePdfViewer ) {
|
||||
<div class="preview-sticky pdf-viewer-container">
|
||||
<pngx-pdf-viewer
|
||||
[src]="{ url: previewUrl, password: password }"
|
||||
[original-size]="false"
|
||||
[show-borders]="true"
|
||||
[show-all]="true"
|
||||
[(page)]="previewCurrentPage"
|
||||
[zoom-scale]="previewZoomScale"
|
||||
[zoom]="previewZoomSetting"
|
||||
(error)="onError($event)"
|
||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
||||
</pngx-pdf-viewer>
|
||||
</div>
|
||||
} @else {
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
}
|
||||
}
|
||||
@if (renderAsPlainText) {
|
||||
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
|
||||
}
|
||||
@if (showPasswordField) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -79,8 +79,9 @@ const doc: Document = {
|
||||
storage_path: 31,
|
||||
tags: [41, 42, 43],
|
||||
content: 'text content',
|
||||
added: new Date(),
|
||||
created: new Date(),
|
||||
added: new Date('May 4, 2014 03:24:00'),
|
||||
created: new Date('May 4, 2014 03:24:00'),
|
||||
modified: new Date('May 4, 2014 03:24:00'),
|
||||
archive_serial_number: null,
|
||||
original_file_name: 'file.pdf',
|
||||
owner: null,
|
||||
@@ -254,9 +255,6 @@ describe('DocumentDetailComponent', () => {
|
||||
|
||||
router = TestBed.inject(Router)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3 })))
|
||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
@@ -294,6 +292,17 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents', 3, 'notes'])
|
||||
})
|
||||
|
||||
it('should forward id without section to details', () => {
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3 })))
|
||||
fixture.detectChanges()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents', 3, 'details'], {
|
||||
replaceUrl: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should update title after debounce', fakeAsync(() => {
|
||||
initNormally()
|
||||
component.titleInput.value = 'Foo Bar'
|
||||
@@ -303,7 +312,7 @@ describe('DocumentDetailComponent', () => {
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should update title before doc change if wasnt updated via debounce', fakeAsync(() => {
|
||||
it('should update title before doc change if was not updated via debounce', fakeAsync(() => {
|
||||
initNormally()
|
||||
component.titleInput.value = 'Foo Bar'
|
||||
component.titleInput.inputField.nativeElement.dispatchEvent(
|
||||
@@ -319,6 +328,7 @@ describe('DocumentDetailComponent', () => {
|
||||
})
|
||||
|
||||
it('should load already-opened document via param', () => {
|
||||
initNormally()
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(doc)
|
||||
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||
@@ -399,8 +409,11 @@ describe('DocumentDetailComponent', () => {
|
||||
})
|
||||
|
||||
it('should 404 on invalid id', () => {
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(null))
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 999, section: 'details' })))
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(null))
|
||||
fixture.detectChanges()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true })
|
||||
})
|
||||
@@ -935,7 +948,56 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(refreshSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should get suggestions', () => {
|
||||
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
|
||||
suggestionsSpy.mockReturnValue(of({ tags: [1, 2] }))
|
||||
initNormally()
|
||||
expect(suggestionsSpy).toHaveBeenCalled()
|
||||
expect(component.suggestions).toEqual({ tags: [1, 2] })
|
||||
})
|
||||
|
||||
it('should show error if needed for get suggestions', () => {
|
||||
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
suggestionsSpy.mockImplementationOnce(() =>
|
||||
throwError(() => new Error('failed to get suggestions'))
|
||||
)
|
||||
initNormally()
|
||||
expect(suggestionsSpy).toHaveBeenCalled()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should warn when open document does not match doc retrieved from backend on init', () => {
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modals) => (openModal = modals[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
const openDoc = Object.assign({}, doc)
|
||||
// simulate a document being modified elsewhere and db updated
|
||||
doc.modified = new Date()
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' })))
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
|
||||
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: customFields.length,
|
||||
all: customFields.map((f) => f.id),
|
||||
results: customFields,
|
||||
})
|
||||
)
|
||||
fixture.detectChanges() // calls ngOnInit
|
||||
expect(modalSpy).toHaveBeenCalledWith(ConfirmDialogComponent)
|
||||
const closeSpy = jest.spyOn(openModal, 'close')
|
||||
const confirmDialog = openModal.componentInstance as ConfirmDialogComponent
|
||||
confirmDialog.confirmClicked.next(confirmDialog)
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
function initNormally() {
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' })))
|
||||
jest
|
||||
.spyOn(documentService, 'get')
|
||||
.mockReturnValueOnce(of(Object.assign({}, doc)))
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
map,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
} from 'rxjs/operators'
|
||||
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
||||
import {
|
||||
@@ -157,7 +158,7 @@ export class DocumentDetailComponent
|
||||
|
||||
@ViewChild('nav') nav: NgbNav
|
||||
@ViewChild('pdfPreview') set pdfPreview(element) {
|
||||
// this gets called when compontent added or removed from DOM
|
||||
// this gets called when component added or removed from DOM
|
||||
if (
|
||||
element &&
|
||||
element.nativeElement.offsetParent !== null &&
|
||||
@@ -257,6 +258,13 @@ export class DocumentDetailComponent
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(
|
||||
filter((paramMap) => {
|
||||
// only init when changing docs & section is set
|
||||
return (
|
||||
+paramMap.get('id') !== this.documentId &&
|
||||
paramMap.get('section')?.length > 0
|
||||
)
|
||||
}),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
switchMap((paramMap) => {
|
||||
const documentId = +paramMap.get('id')
|
||||
@@ -289,7 +297,23 @@ export class DocumentDetailComponent
|
||||
const openDocument = this.openDocumentService.getOpenDocument(
|
||||
this.documentId
|
||||
)
|
||||
|
||||
if (openDocument) {
|
||||
if (
|
||||
new Date(doc.modified) > new Date(openDocument.modified) &&
|
||||
!this.modalService.hasOpenModals()
|
||||
) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent)
|
||||
modal.componentInstance.title = $localize`Document changes detected`
|
||||
modal.componentInstance.messageBold = $localize`The version of this document in your browser session appears older than the existing version.`
|
||||
modal.componentInstance.message = $localize`Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.`
|
||||
modal.componentInstance.cancelBtnClass = 'visually-hidden'
|
||||
modal.componentInstance.btnCaption = $localize`Ok`
|
||||
modal.componentInstance.confirmClicked.subscribe(() =>
|
||||
modal.close()
|
||||
)
|
||||
}
|
||||
|
||||
if (this.documentForm.dirty) {
|
||||
Object.assign(openDocument, this.documentForm.value)
|
||||
openDocument['owner'] =
|
||||
@@ -316,7 +340,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: (titleValue) => {
|
||||
// In the rare case when the field changed just after debounced event was fired.
|
||||
// We dont want to overwrite whats actually in the text field, so just return
|
||||
// We dont want to overwrite what's actually in the text field, so just return
|
||||
if (titleValue !== this.titleInput.value) return
|
||||
|
||||
this.title = titleValue
|
||||
@@ -409,11 +433,14 @@ export class DocumentDetailComponent
|
||||
updateComponent(doc: Document) {
|
||||
this.document = doc
|
||||
this.requiresPassword = false
|
||||
// this.customFields = doc.custom_fields.concat([])
|
||||
this.updateFormForCustomFields()
|
||||
this.documentsService
|
||||
.getMetadata(doc.id)
|
||||
.pipe(first())
|
||||
.pipe(
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
takeUntil(this.docChangeNotifier)
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.metadata = result
|
||||
@@ -434,7 +461,11 @@ export class DocumentDetailComponent
|
||||
) {
|
||||
this.documentsService
|
||||
.getSuggestions(doc.id)
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.pipe(
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
takeUntil(this.docChangeNotifier)
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.suggestions = result
|
||||
@@ -830,8 +861,11 @@ export class DocumentDetailComponent
|
||||
get userIsOwner(): boolean {
|
||||
let doc: Document = Object.assign({}, this.document)
|
||||
// dont disable while editing
|
||||
if (this.document && this.store?.value.permissions_form?.owner) {
|
||||
doc.owner = this.store?.value.permissions_form?.owner
|
||||
if (
|
||||
this.document &&
|
||||
this.store?.value.permissions_form?.hasOwnProperty('owner')
|
||||
) {
|
||||
doc.owner = this.store.value.permissions_form.owner
|
||||
}
|
||||
return !this.document || this.permissionsService.currentUserOwnsObject(doc)
|
||||
}
|
||||
@@ -839,8 +873,11 @@ export class DocumentDetailComponent
|
||||
get userCanEdit(): boolean {
|
||||
let doc: Document = Object.assign({}, this.document)
|
||||
// dont disable while editing
|
||||
if (this.document && this.store?.value.permissions_form?.owner) {
|
||||
doc.owner = this.store?.value.permissions_form?.owner
|
||||
if (
|
||||
this.document &&
|
||||
this.store?.value.permissions_form?.hasOwnProperty('owner')
|
||||
) {
|
||||
doc.owner = this.store.value.permissions_form.owner
|
||||
}
|
||||
return (
|
||||
!this.document ||
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm me-2"
|
||||
(click)="expand = !expand">
|
||||
@if (!expand) {
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="caret-down"></i-bs>
|
||||
}
|
||||
@if (expand) {
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
|
||||
</svg>
|
||||
<i-bs width="1.2em" height="1.2em" name="caret-up"></i-bs>
|
||||
}
|
||||
</button>
|
||||
{{title}}
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<div class="d-flex align-items-center" role="group" aria-label="Select">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
||||
</svg> <ng-container i18n>Cancel</ng-container>
|
||||
<i-bs name="slash-circle"></i-bs> <ng-container i18n>Cancel</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2" role="group" aria-label="Select">
|
||||
<label class="me-2" i18n>Select:</label>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
|
||||
</svg> <ng-container i18n>Page</ng-container>
|
||||
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check-all" />
|
||||
</svg> <ng-container i18n>All</ng-container>
|
||||
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,16 +67,12 @@
|
||||
<div class="btn-toolbar">
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
|
||||
</svg><div class="d-none d-sm-inline"> <ng-container i18n>Permissions</ng-container></div>
|
||||
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline"> <ng-container i18n>Permissions</ng-container></div>
|
||||
</button>
|
||||
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
||||
</svg>
|
||||
<i-bs name="three-dots"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
@@ -94,9 +84,7 @@
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
|
||||
@if (!awaitingDownload) {
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
|
||||
</svg>
|
||||
<i-bs name="arrow-down"></i-bs>
|
||||
}
|
||||
@if (awaitingDownload) {
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
@@ -137,9 +125,7 @@
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,7 @@
|
||||
}
|
||||
@for (highlight of searchNoteHighlights; track highlight) {
|
||||
<span class="d-block">
|
||||
<svg width="1em" height="1em" fill="currentColor" class="me-2">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||
</svg>
|
||||
<i-bs name="chat-left-text"></i-bs>
|
||||
<span [innerHtml]="highlight"></span>
|
||||
</span>
|
||||
}
|
||||
@@ -51,66 +49,46 @@
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
|
||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
||||
<i-bs name="diagram-3"></i-bs> <span class="d-none d-md-inline" i18n>More like this</span>
|
||||
</a>
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>Edit</span>
|
||||
<i-bs name="pencil"></i-bs> <span class="d-none d-md-inline" i18n>Edit</span>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
|
||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#eye"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>View</span>
|
||||
<i-bs name="eye"></i-bs> <span class="d-none d-md-inline" i18n>View</span>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<pngx-preview-popup [document]="document"></pngx-preview-popup>
|
||||
</ng-template>
|
||||
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
|
||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#download"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>Download</span>
|
||||
<i-bs name="download"></i-bs> <span class="d-none d-md-inline" i18n>Download</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
||||
@if (notesEnabled && document.notes.length) {
|
||||
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="View notes" i18n-title>
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||
</svg>
|
||||
<small i18n>{{document.notes.length}} Notes</small>
|
||||
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="chat-left-text"></i-bs><small i18n>{{document.notes.length}} Notes</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.document_type) {
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.storage_path) {
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#archive"/>
|
||||
</svg>
|
||||
<small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.archive_serial_number | isNumber) {
|
||||
<div class="list-group-item me-2 bg-light text-dark p-1 border-0">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
|
||||
</svg>
|
||||
<small>#{{document.archive_serial_number}}</small>
|
||||
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
||||
</div>
|
||||
}
|
||||
<ng-template #dateTooltip>
|
||||
@@ -120,26 +98,17 @@
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
|
||||
</svg>
|
||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
|
||||
</svg>
|
||||
<small>{{document.owner | username}}</small>
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.is_shared_by_requester) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
|
||||
</svg>
|
||||
<small i18n>Shared</small>
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.__search_hit__?.score) {
|
||||
|
||||
@@ -82,7 +82,7 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
// only show notes with a match
|
||||
highlights = (this.document['__search_hit__'].note_highlights as string)
|
||||
.split(',')
|
||||
.filter((higlight) => higlight.includes('<span'))
|
||||
.filter((highlight) => highlight.includes('<span'))
|
||||
}
|
||||
return highlights
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
@if (notesEnabled && document.notes.length) {
|
||||
<a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
|
||||
<span class="badge rounded-pill bg-light border text-primary">
|
||||
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="ms-1 me-1" name="chat-left-text"></i-bs>
|
||||
{{document.notes.length}}</span>
|
||||
</a>
|
||||
}
|
||||
@@ -45,18 +43,14 @@
|
||||
@if (document.document_type) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="me-2 text-muted" name="file-earmark"></i-bs>
|
||||
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.storage_path) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="me-2 text-muted" name="folder"></i-bs>
|
||||
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
||||
</button>
|
||||
}
|
||||
@@ -69,33 +63,25 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="me-2 text-muted" name="calendar-event"></i-bs>
|
||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
</div>
|
||||
@if (document.archive_serial_number | isNumber) {
|
||||
<div class="ps-0 p-1">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="me-2 text-muted" name="upc-scan"></i-bs>
|
||||
<small>#{{document.archive_serial_number}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
<div class="ps-0 p-1">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="me-2 text-muted" name="person-fill-lock"></i-bs>
|
||||
<small>{{document.owner | username}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.is_shared_by_requester) {
|
||||
<div class="ps-0 p-1">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="me-2 text-muted" name="people-fill"></i-bs>
|
||||
<small i18n>Shared</small>
|
||||
</div>
|
||||
}
|
||||
@@ -103,26 +89,18 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group w-100">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
<i-bs name="pencil"></i-bs>
|
||||
</a>
|
||||
<a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||
</svg>
|
||||
<i-bs name="eye"></i-bs>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<pngx-preview-popup [document]="document"></pngx-preview-popup>
|
||||
</ng-template>
|
||||
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
<i-bs name="download"></i-bs>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
<div ngbDropdown class="me-2 d-flex">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
||||
</svg>
|
||||
<i-bs name="text-indent-left"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
@@ -16,21 +14,15 @@
|
||||
<div class="btn-group flex-fill" role="group">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
||||
</svg>
|
||||
<i-bs name="list-ul"></i-bs>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
||||
</svg>
|
||||
<i-bs name="grid"></i-bs>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
||||
</svg>
|
||||
<i-bs name="hdd-stack"></i-bs>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -40,15 +32,11 @@
|
||||
<div class="w-100 d-flex pb-2 mb-1 border-bottom">
|
||||
<input type="radio" class="btn-check" [value]="false" [(ngModel)]="listSortReverse" id="listSortReverseFalse">
|
||||
<label class="btn btn-outline-primary btn-sm mx-2 flex-fill" for="listSortReverseFalse">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
|
||||
</svg>
|
||||
<i-bs name="sort-alpha-down"></i-bs>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [value]="true" [(ngModel)]="listSortReverse" id="listSortReverseTrue">
|
||||
<label class="btn btn-outline-primary btn-sm me-2 flex-fill" for="listSortReverseTrue">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
|
||||
</svg>
|
||||
<i-bs name="sort-alpha-up-alt"></i-bs>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
@@ -116,9 +104,7 @@
|
||||
}
|
||||
@if (!list.isReloading && isFiltered) {
|
||||
<button class="btn btn-link py-0" (click)="resetFilters()">
|
||||
<svg fill="currentColor" class="buttonicon-sm">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg><small i18n>Reset filters</small>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -246,9 +232,7 @@
|
||||
@if (d.notes.length) {
|
||||
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
|
||||
<span class="badge rounded-pill bg-light border text-primary">
|
||||
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||
</svg>
|
||||
<i-bs width="0.9rem" height="0.9rem" class="ms-1 me-1" name="chat-left-text"></i-bs>
|
||||
{{d.notes.length}}</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
}
|
||||
@if (_textFilter) {
|
||||
<button class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
|
||||
<svg fill="currentColor" class="buttonicon-sm me-1">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
</button>
|
||||
}
|
||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
|
||||
@@ -87,9 +85,7 @@
|
||||
</div>
|
||||
<div class="d-flex flex-wrap d-none d-sm-inline-block">
|
||||
<button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()">
|
||||
<svg class="toolbaricon ms-n1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"></use>
|
||||
</svg><ng-container i18n>Reset filters</ng-container>
|
||||
<i-bs class="ms-n1" name="x"></i-bs><ng-container i18n>Reset filters</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -355,7 +355,7 @@ describe('FilterEditorComponent', () => {
|
||||
expect(component.textFilterTarget).toEqual('fulltext-morelike') // TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||
expect(moreLikeSpy).toHaveBeenCalledWith(1)
|
||||
expect(component.textFilter).toEqual('Foo Bar')
|
||||
// we have to do this here because it cant be done by user input
|
||||
// we have to do this here because it can't be done by user input
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_MORELIKE,
|
||||
@@ -1264,7 +1264,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateCreatedAfter.nativeElement.value = '05/14/2023'
|
||||
// dateCreatedAfter.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateCreatedAfter = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1284,7 +1284,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateCreatedBefore.nativeElement.value = '05/14/2023'
|
||||
// dateCreatedBefore.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateCreatedBefore = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1341,7 +1341,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateAddedAfter.nativeElement.value = '05/14/2023'
|
||||
// dateAddedAfter.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateAddedAfter = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1361,7 +1361,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateAddedBefore.nativeElement.value = '05/14/2023'
|
||||
// dateAddedBefore.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateAddedBefore = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1524,7 +1524,7 @@ describe('FilterEditorComponent', () => {
|
||||
)
|
||||
ownerToggle.nativeElement.checked = true
|
||||
// ownerToggle.triggerEventHandler('change')
|
||||
// TODO: ngModel isnt doing this here
|
||||
// TODO: ngModel isn't doing this here
|
||||
component.permissionsSelectionModel.hideUnowned = true
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
|
||||
@@ -40,7 +40,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
||||
})
|
||||
|
||||
ngOnInit(): void {
|
||||
// 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(() => {
|
||||
this.closeEnabled = true
|
||||
})
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
|
||||
<span>{{displayName(note)}} - {{ note.created | customDate}}</span>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 fade" title="Delete note" i18n-title (click)="deleteNote(note.id)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Note }">
|
||||
<svg width="13" height="13" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<i-bs name="trash"></i-bs>
|
||||
<span class="visually-hidden" i18n>Delete note</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('CorrespondentListComponent', () => {
|
||||
correspondentsService = TestBed.inject(CorrespondentService)
|
||||
})
|
||||
|
||||
// Tests are included in management-list.compontent.spec.ts
|
||||
// Tests are included in management-list.component.spec.ts
|
||||
|
||||
it('should use correct delete message', () => {
|
||||
jest.spyOn(correspondentsService, 'listFiltered').mockReturnValue(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user