Compare commits

..

91 Commits

Author SHA1 Message Date
shamoon
ce841d4196 Merge branch 'dev' 2024-01-24 11:24:02 -08:00
shamoon
c77f8acf41 Bump version to v2.4.1 2024-01-24 11:02:24 -08:00
github-actions[bot]
212674f9df New Crowdin translations by GitHub Action (#5488)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-24 11:01:37 -08:00
shamoon
283ced56d1 Revert "Enhancement: support remote user auth directly against API (DRF)" (#5534) 2024-01-24 11:00:44 -08:00
shamoon
530e57151d Update PULL_REQUEST_TEMPLATE.md 2024-01-24 10:31:10 -08:00
shamoon
1141c3f361 Fix: fix calendar popup header background color and disable browser autofill (#5514) 2024-01-23 19:48:41 -08:00
shamoon
49416d3372 Fix: install script fails on alpine linux due to the use of head (#5520) 2024-01-23 14:25:16 -08:00
shamoon
88ae60a4a0 Fix: enforce object permissions for app config (#5516) 2024-01-23 12:23:15 -08:00
shamoon
6651c80fb9 Remove unused workflow trigger / action perms from UI 2024-01-23 12:18:52 -08:00
shamoon
6d6650d5f6 Fix: disable clickable name edit if user does not have permissions 2024-01-22 22:12:07 -08:00
shamoon
6df252c99b Chore: Build fix- branches (#5501) 2024-01-22 16:35:45 -08:00
shamoon
5881f05dbc Change workflow permissions assignment to merge (#5496) 2024-01-22 16:34:16 -08:00
dependabot[bot]
00eba3b223 Chore(deps-dev): Bump the development group with 1 update (#5503)
Bumps the development group with 1 update: [ruff](https://github.com/astral-sh/ruff).


Updates `ruff` from 0.1.13 to 0.1.14
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.13...v0.1.14)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 13:45:25 -08:00
Uli Fahrer
f7ab8d23a7 Documentation: update celery monitoring docker usage (#5484)
* docs: update celery monitoring docker usage

* docs: add review comments

Co-Authored-By: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-22 10:11:28 -08:00
Joakim Berglund
85b596d20d Lowercase stack name in docker-compose.portainer.yml (#5491)
Portainer does not allow upper case letters in the stack name. Update documentation to adhere to this limitation.
2024-01-21 10:34:04 -08:00
github-actions[bot]
4d43f6b63d New Crowdin translations by GitHub Action (#5463)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-21 00:35:54 -08:00
shamoon
ea1eb551a7 Enhancement: add background to consumer statuses 2024-01-21 00:34:24 -08:00
shamoon
5842944d1e Fix: render images not converted to pdf, refactor doc detail rendering (#5475) 2024-01-20 08:26:24 -08:00
shamoon
5781a0d51f Fix frontend tests icon imports 2024-01-19 22:28:32 -08:00
shamoon
e5f48739a0 Fix: Dont parse numbers with exponent as integer (#5457) 2024-01-19 06:29:13 -08:00
shamoon
d378c861f6 Reset dev version string 2024-01-18 22:14:01 -08:00
github-actions[bot]
9466bfdb00 [Documentation] Add v2.4.0 changelog (#5453) 2024-01-18 22:11:53 -08:00
shamoon
f02e8e0dc3 Bump version to 2.4.0 2024-01-18 17:43:49 -08:00
github-actions[bot]
c1ed87a44f New Crowdin translations by GitHub Action (#5349)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-18 17:42:21 -08:00
shamoon
16169ca331 Chore: Close outdated support / general discussions (#5443) 2024-01-18 19:46:12 +00:00
shamoon
26900e0766 Fix: doc link removal before assigning value (#5451) 2024-01-18 06:58:41 -08:00
JigSaw
aa798604b3 Fix typo in bug report template (#5450) 2024-01-18 14:28:33 +00:00
shamoon
bb98fc5f65 Chore: better bootstrap icons (#5403) 2024-01-18 00:27:38 +00:00
shamoon
dc1918ad10 Fix: dont lose permissions ui if owner changed from null (#5433) 2024-01-17 17:44:04 +00:00
shamoon
ea632d0417 Fix missing frontend test imports 2024-01-16 23:01:50 -08:00
shamoon
648dc709fd Fix: tweak how auto-scrolling of logs works 2024-01-16 23:00:18 -08:00
shamoon
1a84f6a20e Fix: change auto-refresh click target 2024-01-16 22:58:41 -08:00
shamoon
96af953e6f Fix: save button layout with long button translation text 2024-01-16 21:44:41 -08:00
shamoon
6db9e292ba Enhancement: support remote user auth directly against API (DRF) (#5386) 2024-01-16 23:26:05 +00:00
shamoon
2e2362e2df Fix: outdated confirm dialog confirm 2024-01-16 15:18:26 -08:00
Trenton H
51dd95be3d Fix: Getting next ASN when no documents have an ASN (#5431)
* Fixes the next ASN logic to account for no ASNs yet being assigned

* Updates so the ASN will start at 1

* Do the same calculation without the branch
2024-01-16 23:08:37 +00:00
Trenton H
e16645b146 Feature: Add additional caching support to suggestions and metadata (#5414)
* Adds ETag and Last-Modified headers to suggestions, metadata and previews

* Slight update to the suggestions etag

* Small user message for why classifier didn't train again
2024-01-16 17:01:07 +00:00
dependabot[bot]
0068f091bb Chore(deps): Bump the small-changes group with 2 updates (#5413)
Bumps the small-changes group with 2 updates: [channels-redis](https://github.com/django/channels_redis) and [gotenberg-client](https://github.com/stumpylog/gotenberg-client).


Updates `channels-redis` from 4.1.0 to 4.2.0
- [Changelog](https://github.com/django/channels_redis/blob/main/CHANGELOG.txt)
- [Commits](https://github.com/django/channels_redis/commits)

Updates `gotenberg-client` from 0.4.1 to 0.5.0
- [Release notes](https://github.com/stumpylog/gotenberg-client/releases)
- [Changelog](https://github.com/stumpylog/gotenberg-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/gotenberg-client/compare/0.4.1...0.5.0)

---
updated-dependencies:
- dependency-name: channels-redis
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: gotenberg-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 16:14:45 +00:00
dependabot[bot]
ad6efd2898 Chore(deps-dev): Bump the development group with 2 updates (#5412)
Bumps the development group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


Updates `ruff` from 0.1.11 to 0.1.13
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.11...v0.1.13)

Updates `mkdocs-material` from 9.5.3 to 9.5.4
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.3...9.5.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: mkdocs-material
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 16:02:34 +00:00
shamoon
86e380bb1c Fix signin username floating label (#5424) 2024-01-16 07:32:07 -08:00
shamoon
58aacd4814 Feature: help tooltips (#5383) 2024-01-15 15:40:36 -08:00
shamoon
ad07791bac Enhancement / QoL: show selected tasks count (#5379) 2024-01-15 22:15:30 +00:00
shamoon
783090c2cd Fix shared by me filter with multiple users / groups in postgres (#5396) 2024-01-15 22:06:59 +00:00
Trenton H
41a3c7c89b Fix: Catch new warning when loading the classifier (#5395) 2024-01-14 13:21:17 -08:00
shamoon
16cc7415c1 Fix missed migration for app_logo 2024-01-13 16:23:44 -08:00
shamoon
98c5cf89ef Fix: doc detail component fixes (#5373) 2024-01-13 21:40:22 +00:00
shamoon
53e04e66cf Enhancement: warn when outdated doc detected (#5372)
* Update modified property for target docs w bidirectional links

* Warn on doc change detected
2024-01-13 20:28:10 +00:00
shamoon
2a6e79acc8 Feature: app branding (#5357) 2024-01-13 19:57:25 +00:00
Trenton H
2da5e46386 Refactor file consumption task to allow beginnings of a plugin system (#5367) 2024-01-13 16:11:14 +00:00
Trenton H
4dbf8d7969 Reapply #5304 fix 2024-01-12 13:19:24 -08:00
shamoon
4a52fc27d4 Reset dev version string 2024-01-11 21:13:51 -08:00
shamoon
05cd34c8af Merge branch 'main' into dev 2024-01-11 21:13:44 -08:00
dependabot[bot]
8a622181fc Chore(deps-dev): Bump jinja2 from 3.1.2 to 3.1.3 (#5352)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 13:26:54 -08:00
github-actions[bot]
13f38bf3a1 [Documentation] Add v2.3.3 changelog (#5346)
* Changelog v2.3.3 - GHA

* Fix PR categorization, as usual

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-11 07:17:26 -08:00
shamoon
16acc2d6ad Merge branch 'dev' 2024-01-10 15:59:50 -08:00
github-actions[bot]
c2c9a953d3 New Crowdin translations by GitHub Action (#5314)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-10 15:58:17 -08:00
shamoon
530f4a8b28 Bump version to 2.3.3 2024-01-10 15:55:09 -08:00
shamoon
8eb1dc4f62 Merge branch 'dev' 2024-01-10 15:54:47 -08:00
shamoon
a2b87fe012 Trigger crowdin action on changes to frontend strings file 2024-01-10 15:54:02 -08:00
shamoon
3dcb973adb Enhancement: Explain behavior of unset app config boolean to user (#5345) 2024-01-10 15:34:20 -08:00
shamoon
8e8810cbaa Fix: correct OCR_MAX_IMAGE_PIXELS doc link 2024-01-10 13:22:05 -08:00
shamoon
b0aeec4c43 Fix: Coerce language app config field to None if empty 2024-01-10 13:21:51 -08:00
shamoon
1ac298f6ff Fix empty assign_title validation 2024-01-10 12:53:35 -08:00
shamoon
6d5f4e92cc Enhancement: title assignment placeholder error handling, fallback (#5282) 2024-01-10 10:18:55 -08:00
shamoon
416ad13aaf Update config page text 2024-01-09 11:37:39 -08:00
Trenton H
a7e1299194 Updates all backend, hooks and configures codespell in a slightly easier way (#5336) 2024-01-09 10:30:33 -08:00
Trenton H
a12e1fae72 Fix: Don't require the JSON user arguments field (#5320)
* Allows new user args field to be null

* Coerce empty string to None for user_args JSONField

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-08 13:14:36 -08:00
shamoon
f525ac0af6 Chore: add pre-commit hook for codespell (#5324) 2024-01-08 13:03:05 -08:00
luzpaz
58bf9c552b Documentation: Fix typos with automated tool (#5319)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-08 16:58:41 +00:00
github-actions[bot]
22d257cd1f Changelog v2.3.2 - GHA (#5310)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-07 23:32:11 -08:00
Trenton Holmes
f1bf1ddc54 Resets version string 2024-01-07 17:03:46 -08:00
Trenton Holmes
6015cc0e4a Bumps version to 2.3.2 2024-01-07 17:02:36 -08:00
Trenton Holmes
f9926d77d5 Merge remote-tracking branch 'origin/dev' 2024-01-07 16:47:52 -08:00
Colin Hebert
4f85dcecfc Deployment: Use the default Docker healthcheck from the Dockerfile (Part 2) (#5224)
* Set default healthcheck

* Rely on default healthcheck
2024-01-07 22:49:29 +00:00
github-actions[bot]
30c31a3d4c New Crowdin translations by GitHub Action (#5309)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-07 14:37:33 -08:00
shamoon
c64667d396 Fix: workflow assignment of customfield fails if field exists in v2.3.1 (#5302) 2024-01-07 22:27:57 +00:00
github-actions[bot]
9f6613fe05 New Crowdin translations by GitHub Action (#5268)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-07 14:19:57 -08:00
Trenton H
ea47af7034 Fixes the user arguments json field decoding (#5307) 2024-01-07 14:17:51 -08:00
shamoon
d46abeff01 Fix: empty match field cannot be saved (#5301) 2024-01-07 22:10:26 +00:00
Trenton H
2b39697ffb Fixes usages of UTC datetime instead of local datetime (#5304) 2024-01-07 13:57:40 -08:00
shamoon
4b00a72ff5 Fix: path fnmatch case note 2024-01-07 10:48:04 -08:00
shamoon
e590b2482e Update frontend strings 2024-01-07 08:46:32 -08:00
github-actions[bot]
eb7dd80410 Changelog v2.3.1 - GHA (#5283)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-07 08:44:12 -08:00
shamoon
86338465fb Fix: workflow edit form loses unsaved changes in v2.3.1 (#5299) 2024-01-07 08:16:58 -08:00
shamoon
a41dbdd12c Reset dev version string 2024-01-06 22:43:25 -08:00
shamoon
1e10a438cd Bump version to 2.3.1 2024-01-06 22:42:35 -08:00
shamoon
ab34ea724d Merge branch 'dev' 2024-01-06 22:42:16 -08:00
shamoon
fd8bfe1a80 Fix: edit workflow form not displaying trigger settings in v2.3.0 (#5276) 2024-01-06 17:27:49 +00:00
Trenton H
9043f45350 Adds more documentation for OCR_PAGES and prevents using 0 for actual OCR (#5275) 2024-01-06 09:06:41 -08:00
github-actions[bot]
5921e6d13e [Documentation] Add v2.3.0 changelog (#5263)
* Changelog v2.3.0 - GHA

* Re-categorize some PRs

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-06 09:06:23 -08:00
shamoon
ee2bfe2350 Reset dev version string 2024-01-05 22:12:29 -08:00
287 changed files with 44958 additions and 32654 deletions

3
.codespellrc Normal file
View File

@@ -0,0 +1,3 @@
[codespell]
write-changes = True
ignore-words-list = criterias,afterall,valeu,ureue,equest,ure

View File

@@ -20,7 +20,7 @@ body:
- [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/). - [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/).
- [The installation instructions](https://docs.paperless-ngx.com/setup/#installation). - [The installation instructions](https://docs.paperless-ngx.com/setup/#installation).
- [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues). - [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues).
- Disable any customer container initialization scripts, if using - Disable any custom container initialization scripts, if using
If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support). If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support).
- type: textarea - type: textarea

View File

@@ -9,7 +9,7 @@ Please include a summary of the change and which issue is fixed (if any) and any
--> -->
<!-- <!--
⚠️ Important: Pull requests that implement a new feature *should almost always target an existing feature request*. This is in order to balance the work of implementing and maintaining new features vs. community-interest. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers. ⚠️ Important: Pull requests that implement a new feature or enhancement *should almost always target an existing feature request* with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
--> -->
Closes #(issue or discussion) Closes #(issue or discussion)
@@ -22,7 +22,7 @@ NOTE: Please check only one box!
--> -->
- [ ] Bug fix: non-breaking change which fixes an issue. - [ ] Bug fix: non-breaking change which fixes an issue.
- [ ] New feature: non-breaking change which adds functionality. _Please read the important note above._ - [ ] New feature / Enhancement: non-breaking change which adds functionality. _Please read the important note above._
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected. - [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
- [ ] Documentation only. - [ ] Documentation only.
- [ ] Other. Please explain: - [ ] Other. Please explain:

View File

@@ -169,7 +169,7 @@ jobs:
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
install-frontend-depedendencies: install-frontend-depedendencies:
name: "Install Frontend Dependendencies" name: "Install Frontend Dependencies"
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: needs:
- pre-commit - pre-commit
@@ -182,7 +182,7 @@ jobs:
node-version: 20.x node-version: 20.x
cache: 'npm' cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json' cache-dependency-path: 'src-ui/package-lock.json'
- name: Cache frontend depdendencies - name: Cache frontend dependencies
id: cache-frontend-deps id: cache-frontend-deps
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@@ -219,7 +219,7 @@ jobs:
node-version: 20.x node-version: 20.x
cache: 'npm' cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json' cache-dependency-path: 'src-ui/package-lock.json'
- name: Cache frontend depdendencies - name: Cache frontend dependencies
id: cache-frontend-deps id: cache-frontend-deps
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@@ -310,7 +310,7 @@ jobs:
build-docker-image: build-docker-image:
name: Build Docker image for ${{ github.ref_name }} name: Build Docker image for ${{ github.ref_name }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v')) if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
concurrency: concurrency:
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }} group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
cancel-in-progress: true cancel-in-progress: true

View File

@@ -41,7 +41,7 @@ jobs:
package_name: "${{ matrix.primary-name }}" package_name: "${{ matrix.primary-name }}"
scheme: "branch" scheme: "branch"
repo_name: "paperless-ngx" repo_name: "paperless-ngx"
match_regex: "feature-" match_regex: "(feature|fix)"
do_delete: "true" do_delete: "true"
cleanup-untagged-images: cleanup-untagged-images:

View File

@@ -7,6 +7,7 @@ on:
push: push:
paths: [ paths: [
'src/locale/**', 'src/locale/**',
'src-ui/messages.xlf',
'src-ui/src/locale/**' 'src-ui/src/locale/**'
] ]
branches: [ dev ] branches: [ dev ]

View File

@@ -81,7 +81,7 @@ jobs:
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`) console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
for (const discussion of result.repository.discussions.nodes) { for (const discussion of result.repository.discussions.nodes) {
console.log(`Closing dicussion #${discussion.number} (${discussion.id})`) console.log(`Closing discussion #${discussion.number} (${discussion.id})`)
const addCommentMutation = `mutation($discussion:ID!, $body:String!) { const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) { addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
@@ -107,3 +107,94 @@ jobs:
await sleep(1000) await sleep(1000)
} }
close-outdated-discussions:
name: 'Close Outdated Discussions'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const CUTOFF_DAYS = 180;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - CUTOFF_DAYS);
const query = `query(
$owner:String!,
$name:String!,
$supportCategory:ID!,
$generalCategory:ID!,
) {
supportDiscussions: repository(owner:$owner, name:$name){
discussions(
categoryId:$supportCategory,
last:50,
answered:false,
states:[OPEN],
) {
nodes {
id,
number,
updatedAt
}
},
},
generalDiscussions: repository(owner:$owner, name:$name){
discussions(
categoryId:$generalCategory,
last:50,
states:[OPEN],
) {
nodes {
id,
number,
updatedAt
}
}
}
}`;
const variables = {
owner: context.repo.owner,
name: context.repo.repo,
supportCategory: "DIC_kwDOG1Zs184CBKWK",
generalCategory: "DIC_kwDOG1Zs184CBKWJ"
}
const result = await github.graphql(query, variables);
const combinedDiscussions = [
...result.supportDiscussions.discussions.nodes,
...result.generalDiscussions.discussions.nodes,
]
console.log(`Checking ${combinedDiscussions.length} open discussions`);
for (const discussion of combinedDiscussions) {
if (new Date(discussion.updatedAt) < cutoff) {
console.log(`Closing outdated discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt}`);
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
clientMutationId
}
}`;
const commentVariables = {
discussion: discussion.id,
body: 'This discussion has been automatically closed due to inactivity.',
}
await github.graphql(addCommentMutation, commentVariables);
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
clientMutationId
}
}`;
const closeVariables = {
discussion: discussion.id,
reason: "OUTDATED",
}
await github.graphql(closeDiscussionMutation, closeVariables);
await sleep(1000);
}
}

View File

@@ -28,6 +28,14 @@ repos:
- svg - svg
- id: check-case-conflict - id: check-case-conflict
- id: detect-private-key - id: detect-private-key
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude_types:
- pofile
- json
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: 'v3.1.0' rev: 'v3.1.0'
hooks: hooks:
@@ -39,11 +47,11 @@ repos:
exclude: "(^Pipfile\\.lock$)" exclude: "(^Pipfile\\.lock$)"
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.1.5' rev: 'v0.1.11'
hooks: hooks:
- id: ruff - id: ruff
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.11.0 rev: 23.12.1
hooks: hooks:
- id: black - id: black
# Dockerfile hooks # Dockerfile hooks

View File

@@ -189,7 +189,7 @@ RUN set -eux \
&& chmod 755 /usr/local/bin/paperless_cmd.sh \ && chmod 755 /usr/local/bin/paperless_cmd.sh \
&& mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \ && mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
&& chmod 755 /usr/local/bin/flower-conditional.sh \ && chmod 755 /usr/local/bin/flower-conditional.sh \
&& echo "Installing managment commands" \ && echo "Installing management commands" \
&& chmod +x install_management_commands.sh \ && chmod +x install_management_commands.sh \
&& ./install_management_commands.sh && ./install_management_commands.sh

View File

@@ -7,7 +7,7 @@ name = "pypi"
dateparser = "~=1.2" dateparser = "~=1.2"
# WARNING: django does not use semver. # WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes. # Only patch versions are guaranteed to not introduce breaking changes.
django = "~=4.2.8" django = "~=4.2.9"
django-auditlog = "*" django-auditlog = "*"
django-celery-results = "*" django-celery-results = "*"
django-compression-middleware = "*" django-compression-middleware = "*"
@@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
[dev-packages] [dev-packages]
# Linting # Linting
black = "==23.11.0" black = "*"
pre-commit = "*" pre-commit = "*"
ruff = "*" ruff = "*"
# Testing # Testing

1620
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Docker Compose file for running paperless testing with actual gotenberg # Docker Compose file for running paperless testing with actual gotenberg
# and Tika containers for a more end to end test of the Tika related functionality # and Tika containers for a more end to end test of the Tika related functionality
# Can be used locally or by the CI to start the nessecary containers with the # Can be used locally or by the CI to start the necessary containers with the
# correct networking for the tests # correct networking for the tests
version: "3.7" version: "3.7"

View File

@@ -60,11 +60,6 @@ services:
- tika - tika
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media

View File

@@ -54,11 +54,6 @@ services:
- broker - broker
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media
@@ -73,7 +68,6 @@ services:
PAPERLESS_DBPASS: paperless # only needed if non-default password PAPERLESS_DBPASS: paperless # only needed if non-default password
PAPERLESS_DBPORT: 3306 PAPERLESS_DBPORT: 3306
volumes: volumes:
data: data:
media: media:

View File

@@ -18,7 +18,7 @@
# To install and update paperless with this file, do the following: # To install and update paperless with this file, do the following:
# #
# - Open portainer Stacks list and click 'Add stack' # - Open portainer Stacks list and click 'Add stack'
# - Paste the contents of this file and assign a name, e.g. 'Paperless' # - Paste the contents of this file and assign a name, e.g. 'paperless'
# - Click 'Deploy the stack' and wait for it to be deployed # - Click 'Deploy the stack' and wait for it to be deployed
# - Open the list of containers, select paperless_webserver_1 # - Open the list of containers, select paperless_webserver_1
# - Click 'Console' and then 'Connect' to open the command line inside the container # - Click 'Console' and then 'Connect' to open the command line inside the container
@@ -54,11 +54,6 @@ services:
- broker - broker
ports: ports:
- "8010:8000" - "8010:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media

View File

@@ -58,11 +58,6 @@ services:
- tika - tika
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media

View File

@@ -52,11 +52,6 @@ services:
- broker - broker
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media
@@ -67,7 +62,6 @@ services:
PAPERLESS_REDIS: redis://broker:6379 PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db PAPERLESS_DBHOST: db
volumes: volumes:
data: data:
media: media:

View File

@@ -47,11 +47,6 @@ services:
- tika - tika
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media

View File

@@ -38,11 +38,6 @@ services:
- broker - broker
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media
@@ -52,7 +47,6 @@ services:
environment: environment:
PAPERLESS_REDIS: redis://broker:6379 PAPERLESS_REDIS: redis://broker:6379
volumes: volumes:
data: data:
media: media:

View File

@@ -434,8 +434,10 @@ to view more detailed information about the health of the celery workers
used for asynchronous tasks. This includes details on currently running, used for asynchronous tasks. This includes details on currently running,
queued and completed tasks, timing and more. Flower can also be used queued and completed tasks, timing and more. Flower can also be used
with Prometheus, as it exports metrics. For details on its capabilities, with Prometheus, as it exports metrics. For details on its capabilities,
refer to the Flower documentation. refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
documentation.
Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration/#PAPERLESS_ENABLE_FLOWER).
To configure Flower further, create a `flowerconfig.py` and To configure Flower further, create a `flowerconfig.py` and
place it into the `src/paperless` directory. For a Docker place it into the `src/paperless` directory. For a Docker
installation, you can use volumes to accomplish this: installation, you can use volumes to accomplish this:
@@ -444,6 +446,8 @@ installation, you can use volumes to accomplish this:
services: services:
# ... # ...
webserver: webserver:
environment:
- PAPERLESS_ENABLE_FLOWER
ports: ports:
- 5555:5555 # (2)! - 5555:5555 # (2)!
# ... # ...
@@ -452,7 +456,7 @@ services:
``` ```
1. Note the `:ro` tag means the file will be mounted as read only. 1. Note the `:ro` tag means the file will be mounted as read only.
2. `flower` runs by default on port 5555, but this can be configured 2. By default, Flower runs on port 5555, but this can be configured.
## Custom Container Initialization ## Custom Container Initialization
@@ -613,7 +617,7 @@ scan a completely new "odd numbered pages" one. The old staging file will get di
The collation feature can be used together with the [subdirs as tags](configuration.md#consume_config) The collation feature can be used together with the [subdirs as tags](configuration.md#consume_config)
feature (but this is not a requirement). Just create a correctly named double-sided subdir feature (but this is not a requirement). Just create a correctly named double-sided subdir
in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as in the hierarchy and upload your scans there. For example, both `double-sided/foo/bar` as
well as `foo/bar/double-sided` will cause the collated document to be treated as if it well as `foo/bar/double-sided` will cause the collated document to be treated as if it
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`. were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.

View File

@@ -1,5 +1,203 @@
# Changelog # Changelog
## paperless-ngx 2.4.0
### Features
- Enhancement: support remote user auth directly against API (DRF) [@shamoon](https://github.com/shamoon) ([#5386](https://github.com/paperless-ngx/paperless-ngx/pull/5386))
- Feature: Add additional caching support to suggestions and metadata [@stumpylog](https://github.com/stumpylog) ([#5414](https://github.com/paperless-ngx/paperless-ngx/pull/5414))
- Feature: help tooltips [@shamoon](https://github.com/shamoon) ([#5383](https://github.com/paperless-ngx/paperless-ngx/pull/5383))
- Enhancement: warn when outdated doc detected [@shamoon](https://github.com/shamoon) ([#5372](https://github.com/paperless-ngx/paperless-ngx/pull/5372))
- Feature: app branding [@shamoon](https://github.com/shamoon) ([#5357](https://github.com/paperless-ngx/paperless-ngx/pull/5357))
### Bug Fixes
- Fix: doc link removal when has never been assigned [@shamoon](https://github.com/shamoon) ([#5451](https://github.com/paperless-ngx/paperless-ngx/pull/5451))
- Fix: dont lose permissions ui if owner changed from [@shamoon](https://github.com/shamoon) ([#5433](https://github.com/paperless-ngx/paperless-ngx/pull/5433))
- Fix: Getting next ASN when no documents have an ASN [@stumpylog](https://github.com/stumpylog) ([#5431](https://github.com/paperless-ngx/paperless-ngx/pull/5431))
- Fix: signin username floating label [@shamoon](https://github.com/shamoon) ([#5424](https://github.com/paperless-ngx/paperless-ngx/pull/5424))
- Fix: shared by me filter with multiple users / groups in postgres [@shamoon](https://github.com/shamoon) ([#5396](https://github.com/paperless-ngx/paperless-ngx/pull/5396))
- Fix: Catch new warning when loading the classifier [@stumpylog](https://github.com/stumpylog) ([#5395](https://github.com/paperless-ngx/paperless-ngx/pull/5395))
- Fix: doc detail component fixes [@shamoon](https://github.com/shamoon) ([#5373](https://github.com/paperless-ngx/paperless-ngx/pull/5373))
### Maintenance
- Chore: better bootstrap icons [@shamoon](https://github.com/shamoon) ([#5403](https://github.com/paperless-ngx/paperless-ngx/pull/5403))
- Chore: Close outdated support / general discussions [@shamoon](https://github.com/shamoon) ([#5443](https://github.com/paperless-ngx/paperless-ngx/pull/5443))
### Dependencies
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#5413](https://github.com/paperless-ngx/paperless-ngx/pull/5413))
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5412](https://github.com/paperless-ngx/paperless-ngx/pull/5412))
- Chore(deps-dev): Bump jinja2 from 3.1.2 to 3.1.3 [@dependabot](https://github.com/dependabot) ([#5352](https://github.com/paperless-ngx/paperless-ngx/pull/5352))
### All App Changes
<details>
<summary>16 changes</summary>
- Fix: doc link removal when has never been assigned [@shamoon](https://github.com/shamoon) ([#5451](https://github.com/paperless-ngx/paperless-ngx/pull/5451))
- Chore: better bootstrap icons [@shamoon](https://github.com/shamoon) ([#5403](https://github.com/paperless-ngx/paperless-ngx/pull/5403))
- Fix: dont lose permissions ui if owner changed from [@shamoon](https://github.com/shamoon) ([#5433](https://github.com/paperless-ngx/paperless-ngx/pull/5433))
- Enhancement: support remote user auth directly against API (DRF) [@shamoon](https://github.com/shamoon) ([#5386](https://github.com/paperless-ngx/paperless-ngx/pull/5386))
- Fix: Getting next ASN when no documents have an ASN [@stumpylog](https://github.com/stumpylog) ([#5431](https://github.com/paperless-ngx/paperless-ngx/pull/5431))
- Feature: Add additional caching support to suggestions and metadata [@stumpylog](https://github.com/stumpylog) ([#5414](https://github.com/paperless-ngx/paperless-ngx/pull/5414))
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#5413](https://github.com/paperless-ngx/paperless-ngx/pull/5413))
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5412](https://github.com/paperless-ngx/paperless-ngx/pull/5412))
- Fix: signin username floating label [@shamoon](https://github.com/shamoon) ([#5424](https://github.com/paperless-ngx/paperless-ngx/pull/5424))
- Feature: help tooltips [@shamoon](https://github.com/shamoon) ([#5383](https://github.com/paperless-ngx/paperless-ngx/pull/5383))
- Enhancement / QoL: show selected tasks count [@shamoon](https://github.com/shamoon) ([#5379](https://github.com/paperless-ngx/paperless-ngx/pull/5379))
- Fix: shared by me filter with multiple users / groups in postgres [@shamoon](https://github.com/shamoon) ([#5396](https://github.com/paperless-ngx/paperless-ngx/pull/5396))
- Fix: doc detail component fixes [@shamoon](https://github.com/shamoon) ([#5373](https://github.com/paperless-ngx/paperless-ngx/pull/5373))
- Enhancement: warn when outdated doc detected [@shamoon](https://github.com/shamoon) ([#5372](https://github.com/paperless-ngx/paperless-ngx/pull/5372))
- Feature: app branding [@shamoon](https://github.com/shamoon) ([#5357](https://github.com/paperless-ngx/paperless-ngx/pull/5357))
- Chore: Initial refactor of consume task [@stumpylog](https://github.com/stumpylog) ([#5367](https://github.com/paperless-ngx/paperless-ngx/pull/5367))
</details>
## paperless-ngx 2.3.3
### Enhancements
- Enhancement: Explain behavior of unset app config boolean to user [@shamoon](https://github.com/shamoon) ([#5345](https://github.com/paperless-ngx/paperless-ngx/pull/5345))
- Enhancement: title assignment placeholder error handling, fallback [@shamoon](https://github.com/shamoon) ([#5282](https://github.com/paperless-ngx/paperless-ngx/pull/5282))
### Bug Fixes
- Fix: Don't require the JSON user arguments field, interpret empty string as [@stumpylog](https://github.com/stumpylog) ([#5320](https://github.com/paperless-ngx/paperless-ngx/pull/5320))
### Maintenance
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5336](https://github.com/paperless-ngx/paperless-ngx/pull/5336))
- Chore: add pre-commit hook for codespell [@shamoon](https://github.com/shamoon) ([#5324](https://github.com/paperless-ngx/paperless-ngx/pull/5324))
### All App Changes
<details>
<summary>5 changes</summary>
- Enhancement: Explain behavior of unset app config boolean to user [@shamoon](https://github.com/shamoon) ([#5345](https://github.com/paperless-ngx/paperless-ngx/pull/5345))
- Enhancement: title assignment placeholder error handling, fallback [@shamoon](https://github.com/shamoon) ([#5282](https://github.com/paperless-ngx/paperless-ngx/pull/5282))
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5336](https://github.com/paperless-ngx/paperless-ngx/pull/5336))
- Fix: Don't require the JSON user arguments field, interpret empty string as [@stumpylog](https://github.com/stumpylog) ([#5320](https://github.com/paperless-ngx/paperless-ngx/pull/5320))
- Chore: add pre-commit hook for codespell [@shamoon](https://github.com/shamoon) ([#5324](https://github.com/paperless-ngx/paperless-ngx/pull/5324))
</details>
## paperless-ngx 2.3.2
### Bug Fixes
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
### All App Changes
<details>
<summary>5 changes</summary>
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
</details>
## paperless-ngx 2.3.1
### Bug Fixes
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
### All App Changes
<details>
<summary>2 changes</summary>
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
</details>
## paperless-ngx 2.3.0
### Notable Changes
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
### Features
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
### Bug Fixes
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
### Documentation
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
### Maintenance
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
### Dependencies
<details>
<summary>4 changes</summary>
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
</details>
### All App Changes
<details>
<summary>21 changes</summary>
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
- Enhancement: add basic filters for listing of custom fields [@shamoon](https://github.com/shamoon) ([#5257](https://github.com/paperless-ngx/paperless-ngx/pull/5257))
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
</details>
## paperless-ngx 2.2.1 ## paperless-ngx 2.2.1
### Bug Fixes ### Bug Fixes
@@ -311,7 +509,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233)) - Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225)) - Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176)) - Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189)) - Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996)) - Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184)) - Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183)) - Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
@@ -521,7 +719,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176)) - Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198)) - Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193)) - Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189)) - Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996)) - Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174)) - Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179)) - Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
@@ -553,11 +751,11 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
### Bug Fixes ### Bug Fixes
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092)) - Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
### All App Changes ### All App Changes
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092)) - Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
## paperless-ngx 1.17.3 ## paperless-ngx 1.17.3
@@ -1224,7 +1422,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
### Documentation ### Documentation
- Whitespace changes, making sure the example is correcly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089)) - Whitespace changes, making sure the example is correctly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
- Docs: Include additional information about barcodes [@stumpylog](https://github.com/stumpylog) ([#2889](https://github.com/paperless-ngx/paperless-ngx/pull/2889)) - Docs: Include additional information about barcodes [@stumpylog](https://github.com/stumpylog) ([#2889](https://github.com/paperless-ngx/paperless-ngx/pull/2889))
- Fix formatting in Setup documentation page [@igrybkov](https://github.com/igrybkov) ([#2880](https://github.com/paperless-ngx/paperless-ngx/pull/2880)) - Fix formatting in Setup documentation page [@igrybkov](https://github.com/igrybkov) ([#2880](https://github.com/paperless-ngx/paperless-ngx/pull/2880))
- [Documentation] Update docker-compose steps to support podman [@white-gecko](https://github.com/white-gecko) ([#2855](https://github.com/paperless-ngx/paperless-ngx/pull/2855)) - [Documentation] Update docker-compose steps to support podman [@white-gecko](https://github.com/white-gecko) ([#2855](https://github.com/paperless-ngx/paperless-ngx/pull/2855))
@@ -1279,7 +1477,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
- Fix: update PaperlessTask on hard failures [@shamoon](https://github.com/shamoon) ([#3062](https://github.com/paperless-ngx/paperless-ngx/pull/3062)) - Fix: update PaperlessTask on hard failures [@shamoon](https://github.com/shamoon) ([#3062](https://github.com/paperless-ngx/paperless-ngx/pull/3062))
- Bump typescript from 4.8.4 to 4.9.5 in /src-ui [@dependabot](https://github.com/dependabot) ([#3071](https://github.com/paperless-ngx/paperless-ngx/pull/3071)) - Bump typescript from 4.8.4 to 4.9.5 in /src-ui [@dependabot](https://github.com/dependabot) ([#3071](https://github.com/paperless-ngx/paperless-ngx/pull/3071))
- Bulk Bump npm packages 04.23 [@dependabot](https://github.com/dependabot) ([#3068](https://github.com/paperless-ngx/paperless-ngx/pull/3068)) - Bulk Bump npm packages 04.23 [@dependabot](https://github.com/dependabot) ([#3068](https://github.com/paperless-ngx/paperless-ngx/pull/3068))
- Fix: Hide UI tour steps if user doesnt have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060)) - Fix: Hide UI tour steps if user doesn't have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
- Fix: Hide Permissions tab if user cannot view users [@shamoon](https://github.com/shamoon) ([#3061](https://github.com/paperless-ngx/paperless-ngx/pull/3061)) - Fix: Hide Permissions tab if user cannot view users [@shamoon](https://github.com/shamoon) ([#3061](https://github.com/paperless-ngx/paperless-ngx/pull/3061))
- v1.14.0 delete document fixes [@shamoon](https://github.com/shamoon) ([#3020](https://github.com/paperless-ngx/paperless-ngx/pull/3020)) - v1.14.0 delete document fixes [@shamoon](https://github.com/shamoon) ([#3020](https://github.com/paperless-ngx/paperless-ngx/pull/3020))
- Bump wait-on from 6.0.1 to 7.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#2990](https://github.com/paperless-ngx/paperless-ngx/pull/2990)) - Bump wait-on from 6.0.1 to 7.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#2990](https://github.com/paperless-ngx/paperless-ngx/pull/2990))
@@ -1484,7 +1682,7 @@ older comments. The Docker image will automatically perform this reindex, bare m
- [Docs] Add Paperless Mobile app to docs [@astubenbord](https://github.com/astubenbord) ([#2378](https://github.com/paperless-ngx/paperless-ngx/pull/2378)) - [Docs] Add Paperless Mobile app to docs [@astubenbord](https://github.com/astubenbord) ([#2378](https://github.com/paperless-ngx/paperless-ngx/pull/2378))
- Tiny spelling change [@veverkap](https://github.com/veverkap) ([#2369](https://github.com/paperless-ngx/paperless-ngx/pull/2369)) - Tiny spelling change [@veverkap](https://github.com/veverkap) ([#2369](https://github.com/paperless-ngx/paperless-ngx/pull/2369))
- Documentation: update build instructions to remove deprecated [@shamoon](https://github.com/shamoon) ([#2334](https://github.com/paperless-ngx/paperless-ngx/pull/2334)) - Documentation: update build instructions to remove deprecated [@shamoon](https://github.com/shamoon) ([#2334](https://github.com/paperless-ngx/paperless-ngx/pull/2334))
- [Documentation] Add note that PAPERLESS_URL cant contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319)) - [Documentation] Add note that PAPERLESS_URL can't contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
- [Documentation] Add v1.11.3 changelog [@github-actions](https://github.com/github-actions) ([#2311](https://github.com/paperless-ngx/paperless-ngx/pull/2311)) - [Documentation] Add v1.11.3 changelog [@github-actions](https://github.com/github-actions) ([#2311](https://github.com/paperless-ngx/paperless-ngx/pull/2311))
### Maintenance ### Maintenance
@@ -1815,7 +2013,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
### All App Changes ### All App Changes
- Add info that re-do OCR doesnt automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025)) - Add info that re-do OCR doesn't automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
- Bugfix: Fix created_date being a string [@stumpylog](https://github.com/stumpylog) ([#2023](https://github.com/paperless-ngx/paperless-ngx/pull/2023)) - Bugfix: Fix created_date being a string [@stumpylog](https://github.com/stumpylog) ([#2023](https://github.com/paperless-ngx/paperless-ngx/pull/2023))
- Bugfix: Fixes an issue with mixed text and images when redoing OCR [@stumpylog](https://github.com/stumpylog) ([#2017](https://github.com/paperless-ngx/paperless-ngx/pull/2017)) - Bugfix: Fixes an issue with mixed text and images when redoing OCR [@stumpylog](https://github.com/stumpylog) ([#2017](https://github.com/paperless-ngx/paperless-ngx/pull/2017))
- Bugfix: Don't allow exceptions during date parsing to fail consume [@stumpylog](https://github.com/stumpylog) ([#1998](https://github.com/paperless-ngx/paperless-ngx/pull/1998)) - Bugfix: Don't allow exceptions during date parsing to fail consume [@stumpylog](https://github.com/stumpylog) ([#1998](https://github.com/paperless-ngx/paperless-ngx/pull/1998))
@@ -2226,7 +2424,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
- Fix local Docker image building [\@stumpylog](https://github.com/stumpylog) ([\#849](https://github.com/paperless-ngx/paperless-ngx/pull/849)) - Fix local Docker image building [\@stumpylog](https://github.com/stumpylog) ([\#849](https://github.com/paperless-ngx/paperless-ngx/pull/849))
- Fix: show errors on invalid date input [\@shamoon](https://github.com/shamoon) ([\#862](https://github.com/paperless-ngx/paperless-ngx/pull/862)) - Fix: show errors on invalid date input [\@shamoon](https://github.com/shamoon) ([\#862](https://github.com/paperless-ngx/paperless-ngx/pull/862))
- Fix: Older dates do not display on frontend [\@shamoon](https://github.com/shamoon) ([\#852](https://github.com/paperless-ngx/paperless-ngx/pull/852)) - Fix: Older dates do not display on frontend [\@shamoon](https://github.com/shamoon) ([\#852](https://github.com/paperless-ngx/paperless-ngx/pull/852))
- Fixes IMAP UTF8 Authenication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725)) - Fixes IMAP UTF8 Authentication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
- Fix password field remains visible [\@shamoon](https://github.com/shamoon) ([\#840](https://github.com/paperless-ngx/paperless-ngx/pull/840)) - Fix password field remains visible [\@shamoon](https://github.com/shamoon) ([\#840](https://github.com/paperless-ngx/paperless-ngx/pull/840))
- Fixes Pillow build for armv7 [\@stumpylog](https://github.com/stumpylog) ([\#815](https://github.com/paperless-ngx/paperless-ngx/pull/815)) - Fixes Pillow build for armv7 [\@stumpylog](https://github.com/stumpylog) ([\#815](https://github.com/paperless-ngx/paperless-ngx/pull/815))
- Update frontend localization source file [\@shamoon](https://github.com/shamoon) ([\#814](https://github.com/paperless-ngx/paperless-ngx/pull/814)) - Update frontend localization source file [\@shamoon](https://github.com/shamoon) ([\#814](https://github.com/paperless-ngx/paperless-ngx/pull/814))
@@ -2347,7 +2545,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
[\@shamoon](https://github.com/shamoon) ([\#313](https://github.com/paperless-ngx/paperless-ngx/pull/313)) [\@shamoon](https://github.com/shamoon) ([\#313](https://github.com/paperless-ngx/paperless-ngx/pull/313))
- Fix imap tools bug [\@stumpylog](https://github.com/stumpylog) - Fix imap tools bug [\@stumpylog](https://github.com/stumpylog)
([\#393](https://github.com/paperless-ngx/paperless-ngx/pull/393)) ([\#393](https://github.com/paperless-ngx/paperless-ngx/pull/393))
- Fix filterable dropdown buttons arent translated - Fix filterable dropdown buttons aren't translated
[\@shamoon](https://github.com/shamoon) ([\#366](https://github.com/paperless-ngx/paperless-ngx/pull/366)) [\@shamoon](https://github.com/shamoon) ([\#366](https://github.com/paperless-ngx/paperless-ngx/pull/366))
- Fix 224: "Auto-detected date is day before receipt date" - Fix 224: "Auto-detected date is day before receipt date"
[\@a17t](https://github.com/a17t) ([\#246](https://github.com/paperless-ngx/paperless-ngx/pull/246)) [\@a17t](https://github.com/a17t) ([\#246](https://github.com/paperless-ngx/paperless-ngx/pull/246))
@@ -3183,7 +3381,7 @@ primarily.
[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR [OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR
on documents. It still uses tesseract under the hood, but the on documents. It still uses tesseract under the hood, but the
PDF parser of Paperless has changed considerably and will behave PDF parser of Paperless has changed considerably and will behave
different for some douments. different for some documents.
- OCRmyPDF creates archived PDF/A documents with embedded text - OCRmyPDF creates archived PDF/A documents with embedded text
that can be selected in the front end. that can be selected in the front end.
- Paperless stores archived versions of documents alongside with - Paperless stores archived versions of documents alongside with
@@ -3234,7 +3432,7 @@ primarily.
crash. crash.
- Mail handling no longer exits entirely when encountering errors. - Mail handling no longer exits entirely when encountering errors.
It will skip the account/rule/message on which the error It will skip the account/rule/message on which the error
occured. occurred.
- Assigning correspondents from mail sender names failed for very - Assigning correspondents from mail sender names failed for very
long names. Paperless no longer assigns correspondents in these long names. Paperless no longer assigns correspondents in these
cases. cases.

View File

@@ -4,9 +4,9 @@ Paperless provides a wide range of customizations. Depending on how you
run paperless, these settings have to be defined in different places. run paperless, these settings have to be defined in different places.
Certain configuration options may be set via the UI. This currently includes Certain configuration options may be set via the UI. This currently includes
common [OCR](#ocr) related settings. If set, these will take preference over the common [OCR](#ocr) related settings and some frontend settings. If set, these will take
settings via environment variables. If not set, the environment setting or applicable preference over the settings via environment variables. If not set, the environment setting
default will be utilized instead. or applicable default will be utilized instead.
- If you run paperless on docker, `paperless.conf` is not used. - If you run paperless on docker, `paperless.conf` is not used.
Rather, configure paperless by copying necessary options to Rather, configure paperless by copying necessary options to
@@ -665,11 +665,13 @@ completely.
Specifying 1 here will only use the first page. Specifying 1 here will only use the first page.
The value must be greater than or equal to 1 to be used.
When combined with `PAPERLESS_OCR_MODE=redo` or When combined with `PAPERLESS_OCR_MODE=redo` or
`PAPERLESS_OCR_MODE=force`, paperless will not modify any text it `PAPERLESS_OCR_MODE=force`, paperless will not modify any text it
finds on excluded pages and copy it verbatim. finds on excluded pages and copy it verbatim.
Defaults to 0, which disables this feature and always uses all Defaults to unset, which disables this feature and always uses all
pages. pages.
#### [`PAPERLESS_OCR_IMAGE_DPI=<num>`](#PAPERLESS_OCR_IMAGE_DPI) {#PAPERLESS_OCR_IMAGE_DPI} #### [`PAPERLESS_OCR_IMAGE_DPI=<num>`](#PAPERLESS_OCR_IMAGE_DPI) {#PAPERLESS_OCR_IMAGE_DPI}
@@ -683,7 +685,7 @@ fails, it uses this value as a fallback.
Set this to the DPI your scanner produces images at. Set this to the DPI your scanner produces images at.
Default is none, which will automatically calculate image DPI so Defaults to unset, which will automatically calculate image DPI so
that the produced PDF documents are A4 sized. that the produced PDF documents are A4 sized.
#### [`PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>`](#PAPERLESS_OCR_MAX_IMAGE_PIXELS) {#PAPERLESS_OCR_MAX_IMAGE_PIXELS} #### [`PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>`](#PAPERLESS_OCR_MAX_IMAGE_PIXELS) {#PAPERLESS_OCR_MAX_IMAGE_PIXELS}
@@ -1327,7 +1329,15 @@ started by the container.
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring). You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
## Update Checking {#update-checking} ## Frontend Settings
#### [`PAPERLESS_APP_TITLE=<bool>`](#PAPERLESS_APP_TITLE) {#PAPERLESS_APP_TITLE}
: If set, overrides the default name "Paperless-ngx"
#### [`PAPERLESS_APP_LOGO=<path>`](#PAPERLESS_APP_LOGO) {#PAPERLESS_APP_LOGO}
: Path to an image file in the /media/logo directory, must include 'logo', e.g. `/logo/Atari_logo.svg`
#### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK} #### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK}

View File

@@ -96,7 +96,7 @@ steps described in [Docker setup](#docker_hub) automatically.
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume - /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
``` ```
Don't change the part after the colon or paperless wont find your Don't change the part after the colon or paperless won't find your
documents. documents.
You may also need to change the default port that the webserver will You may also need to change the default port that the webserver will

View File

@@ -138,7 +138,7 @@ command:
You might encounter errors such as: You might encounter errors such as:
```shell-session ```shell-session
The following error occured while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf' The following error occurred while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
``` ```
This happens when paperless does not have permission to delete files This happens when paperless does not have permission to delete files

View File

@@ -149,7 +149,7 @@ different means. These are as follows:
- **Flag:** Sets the 'important' flag on mails with consumed - **Flag:** Sets the 'important' flag on mails with consumed
documents. Paperless will not consume flagged mails. documents. Paperless will not consume flagged mails.
- **Move to folder:** Moves consumed mails out of the way so that - **Move to folder:** Moves consumed mails out of the way so that
paperless wont consume them again. paperless won't consume them again.
- **Add custom Tag:** Adds a custom tag to mails with consumed - **Add custom Tag:** Adds a custom tag to mails with consumed
documents (the IMAP standard calls these "keywords"). Paperless documents (the IMAP standard calls these "keywords"). Paperless
will not consume mails already tagged. Not all mail servers support will not consume mails already tagged. Not all mail servers support
@@ -411,7 +411,7 @@ The following custom field types are supported:
## Share Links ## Share Links
Paperless-ngx added the abiltiy to create shareable links to files in version 2.0. You can find the button for this on the document detail screen. Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
- Share links do not require a user to login and thus link directly to a file. - Share links do not require a user to login and thus link directly to a file.
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`. - Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.

View File

@@ -315,7 +315,7 @@ fi
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' < /dev/urandom | head --bytes 64) SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' < /dev/urandom | dd bs=1 count=64 2>/dev/null)
DEFAULT_LANGUAGES=("deu eng fra ita spa") DEFAULT_LANGUAGES=("deu eng fra ita spa")

View File

@@ -131,7 +131,7 @@ test('sorting', async ({ page }) => {
await page.getByRole('button', { name: 'Notes' }).click() await page.getByRole('button', { name: 'Notes' }).click()
await expect(page).toHaveURL(/sort=num_notes/) await expect(page).toHaveURL(/sort=num_notes/)
await page.getByRole('button', { name: 'Sort' }).click() await page.getByRole('button', { name: 'Sort' }).click()
await page.locator('.w-100 > label > .toolbaricon').first().click() await page.locator('.w-100 > label > i-bs').first().click()
await expect(page).not.toHaveURL(/reverse=1/) await expect(page).not.toHaveURL(/reverse=1/)
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^9.0.0", "ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.0.1", "ngx-cookie-service": "^17.0.1",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
@@ -13850,6 +13851,22 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "dev": true
}, },
"node_modules/ngx-bootstrap-icons": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/ngx-bootstrap-icons/-/ngx-bootstrap-icons-1.9.3.tgz",
"integrity": "sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": ">= 16.18.1",
"npm": ">= 8.11.0"
},
"peerDependencies": {
"@angular/common": ">= 13.3.8",
"@angular/core": ">= 13.3.8"
}
},
"node_modules/ngx-color": { "node_modules/ngx-color": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz", "resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",

View File

@@ -27,6 +27,7 @@
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^9.0.0", "ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.0.1", "ngx-cookie-service": "^17.0.1",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",

View File

@@ -186,8 +186,8 @@ export const routes: Routes = [
canActivate: [PermissionsGuard], canActivate: [PermissionsGuard],
data: { data: {
requiredPermission: { requiredPermission: {
action: PermissionAction.View, action: PermissionAction.Change,
type: PermissionType.Admin, type: PermissionType.AppConfig,
}, },
}, },
}, },

View File

@@ -110,6 +110,175 @@ import { DocumentLinkComponent } from './components/common/input/document-link/d
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component' import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
import { SwitchComponent } from './components/common/input/switch/switch.component' import { SwitchComponent } from './components/common/input/switch/switch.component'
import { ConfigComponent } from './components/admin/config/config.component' import { ConfigComponent } from './components/admin/config/config.component'
import { FileComponent } from './components/common/input/file/file.component'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
archive,
arrowCounterclockwise,
arrowDown,
arrowLeft,
arrowRepeat,
arrowRight,
arrowRightShort,
arrowUpRight,
asterisk,
boxArrowUp,
boxArrowUpRight,
boxes,
calendar,
calendarEvent,
caretDown,
caretUp,
chatLeftText,
check,
check2All,
checkAll,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
clipboard,
clipboardCheckFill,
clipboardFill,
dash,
diagram3,
dice5,
doorOpen,
download,
envelope,
exclamationTriangle,
eye,
fileEarmark,
fileEarmarkCheck,
fileEarmarkFill,
fileEarmarkLock,
files,
fileText,
filter,
folder,
folderFill,
funnel,
gear,
grid,
gripVertical,
hash,
hddStack,
house,
infoCircle,
link,
listTask,
listUl,
pencil,
people,
peopleFill,
person,
personCircle,
personFill,
personFillLock,
personLock,
plus,
plusCircle,
questionCircle,
search,
slashCircle,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
tagFill,
tags,
textIndentLeft,
textLeft,
threeDots,
threeDotsVertical,
trash,
uiRadios,
upcScan,
x,
xLg,
} from 'ngx-bootstrap-icons'
const icons = {
archive,
arrowCounterclockwise,
arrowDown,
arrowLeft,
arrowRepeat,
arrowRight,
arrowRightShort,
arrowUpRight,
asterisk,
boxArrowUp,
boxArrowUpRight,
boxes,
calendar,
calendarEvent,
caretDown,
caretUp,
chatLeftText,
check,
check2All,
checkAll,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
clipboard,
clipboardCheckFill,
clipboardFill,
dash,
diagram3,
dice5,
doorOpen,
download,
envelope,
exclamationTriangle,
eye,
fileEarmark,
fileEarmarkCheck,
fileEarmarkFill,
fileEarmarkLock,
files,
fileText,
filter,
folder,
folderFill,
funnel,
gear,
grid,
gripVertical,
hash,
hddStack,
house,
infoCircle,
link,
listTask,
listUl,
pencil,
people,
peopleFill,
person,
personCircle,
personFill,
personFillLock,
personLock,
plus,
plusCircle,
questionCircle,
search,
slashCircle,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
tagFill,
tags,
textIndentLeft,
textLeft,
threeDots,
threeDotsVertical,
trash,
uiRadios,
upcScan,
x,
xLg,
}
import localeAf from '@angular/common/locales/af' import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar' import localeAr from '@angular/common/locales/ar'
@@ -267,6 +436,7 @@ function initializeApp(settings: SettingsService) {
PreviewPopupComponent, PreviewPopupComponent,
SwitchComponent, SwitchComponent,
ConfigComponent, ConfigComponent,
FileComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -280,6 +450,7 @@ function initializeApp(settings: SettingsService) {
ColorSliderModule, ColorSliderModule,
TourNgBootstrapModule, TourNgBootstrapModule,
DragDropModule, DragDropModule,
NgxBootstrapIconsModule.pick(icons),
], ],
providers: [ providers: [
{ {

View File

@@ -1,4 +1,10 @@
<pngx-page-header title="Configuration" i18n-title></pngx-page-header> <pngx-page-header
title="Application Configuration"
i18n-title
info="Global app configuration options which apply to <strong>every</strong> user of this install of Paperless-ngx. Options can also be set using environment variables or the configuration file but the value here will always take precedence."
i18n-info
infoLink="configuration">
</pngx-page-header>
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4"> <form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">
@@ -17,9 +23,7 @@
<h6> <h6>
{{option.title}} {{option.title}}
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer"> <a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="info-circle"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
</svg>
</a> </a>
</h6> </h6>
</div> </div>
@@ -27,9 +31,10 @@
@switch (option.type) { @switch (option.type) {
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> } @case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> } @case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> } @case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> } @case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> } @case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
} }
</div> </div>
</div> </div>

View File

@@ -15,12 +15,16 @@ import { SwitchComponent } from '../../common/input/switch/switch.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SelectComponent } from '../../common/input/select/select.component' import { SelectComponent } from '../../common/input/select/select.component'
import { FileComponent } from '../../common/input/file/file.component'
import { SettingsService } from 'src/app/services/settings.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('ConfigComponent', () => { describe('ConfigComponent', () => {
let component: ConfigComponent let component: ConfigComponent
let fixture: ComponentFixture<ConfigComponent> let fixture: ComponentFixture<ConfigComponent>
let configService: ConfigService let configService: ConfigService
let toastService: ToastService let toastService: ToastService
let settingService: SettingsService
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@@ -30,6 +34,7 @@ describe('ConfigComponent', () => {
SelectComponent, SelectComponent,
NumberComponent, NumberComponent,
SwitchComponent, SwitchComponent,
FileComponent,
PageHeaderComponent, PageHeaderComponent,
], ],
imports: [ imports: [
@@ -39,11 +44,13 @@ describe('ConfigComponent', () => {
NgSelectModule, NgSelectModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()
configService = TestBed.inject(ConfigService) configService = TestBed.inject(ConfigService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
settingService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(ConfigComponent) fixture = TestBed.createComponent(ConfigComponent)
component = fixture.componentInstance component = fixture.componentInstance
fixture.detectChanges() fixture.detectChanges()
@@ -100,4 +107,39 @@ describe('ConfigComponent', () => {
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' }) component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
expect(component.errors).toEqual({ user_args: null }) expect(component.errors).toEqual({ user_args: null })
}) })
it('should upload file, show error if necessary', () => {
const uploadSpy = jest.spyOn(configService, 'uploadFile')
const errorSpy = jest.spyOn(toastService, 'showError')
uploadSpy.mockReturnValueOnce(
throwError(() => new Error('Error uploading file'))
)
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(uploadSpy).toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
uploadSpy.mockReturnValueOnce(
of({ app_logo: 'https://example.com/logo/test.png' } as any)
)
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(component.initialConfig).toEqual({
app_logo: 'https://example.com/logo/test.png',
})
})
it('should refresh ui settings after save or upload', () => {
const saveSpy = jest.spyOn(configService, 'saveConfig')
const initSpy = jest.spyOn(settingService, 'initializeSettings')
saveSpy.mockReturnValueOnce(
of({ output_type: OutputTypeConfig.PDF_A } as any)
)
component.saveConfig()
expect(initSpy).toHaveBeenCalled()
const uploadSpy = jest.spyOn(configService, 'uploadFile')
uploadSpy.mockReturnValueOnce(
of({ app_logo: 'https://example.com/logo/test.png' } as any)
)
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(initSpy).toHaveBeenCalled()
})
}) })

View File

@@ -19,6 +19,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms' import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { SettingsService } from 'src/app/services/settings.service'
@Component({ @Component({
selector: 'pngx-config', selector: 'pngx-config',
@@ -55,7 +56,8 @@ export class ConfigComponent
constructor( constructor(
private configService: ConfigService, private configService: ConfigService,
private toastService: ToastService private toastService: ToastService,
private settingsService: SettingsService
) { ) {
super() super()
this.configForm.addControl('id', new FormControl()) this.configForm.addControl('id', new FormControl())
@@ -145,6 +147,7 @@ export class ConfigComponent
this.loading = false this.loading = false
this.initialize(config) this.initialize(config)
this.store.next(config) this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.toastService.showInfo($localize`Configuration updated`) this.toastService.showInfo($localize`Configuration updated`)
}, },
error: (e) => { error: (e) => {
@@ -160,4 +163,27 @@ export class ConfigComponent
public discardChanges() { public discardChanges() {
this.configForm.reset(this.initialConfig) this.configForm.reset(this.initialConfig)
} }
public uploadFile(file: File, key: string) {
this.loading = true
this.configService
.uploadFile(file, this.configForm.value['id'], key)
.pipe(takeUntil(this.unsubscribeNotifier), first())
.subscribe({
next: (config) => {
this.loading = false
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.toastService.showInfo($localize`File successfully updated`)
},
error: (e) => {
this.loading = false
this.toastService.showError(
$localize`An error occurred uploading file`,
e
)
},
})
}
} }

View File

@@ -1,6 +1,10 @@
<pngx-page-header title="Logs" i18n-title> <pngx-page-header
<div class="form-check form-switch" (click)="toggleAutoRefresh()"> title="Logs"
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval"> i18n-title
info="Review the log files for the application and for email checking."
i18n-info>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div> </div>
</pngx-page-header> </pngx-page-header>

View File

@@ -11,6 +11,7 @@ import { of, throwError } from 'rxjs'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap' import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule, By } from '@angular/platform-browser' import { BrowserModule, By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const paperless_logs = [ const paperless_logs = [
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.', '[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
@@ -37,7 +38,12 @@ describe('LogsComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [LogsComponent, PageHeaderComponent], declarations: [LogsComponent, PageHeaderComponent],
providers: [], providers: [],
imports: [HttpClientTestingModule, BrowserModule, NgbModule], imports: [
HttpClientTestingModule,
BrowserModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
],
}).compileComponents() }).compileComponents()
logService = TestBed.inject(LogService) logService = TestBed.inject(LogService)

View File

@@ -2,9 +2,9 @@ import {
Component, Component,
ElementRef, ElementRef,
OnInit, OnInit,
AfterViewChecked,
ViewChild, ViewChild,
OnDestroy, OnDestroy,
ChangeDetectorRef,
} from '@angular/core' } from '@angular/core'
import { Subject, takeUntil } from 'rxjs' import { Subject, takeUntil } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service' import { LogService } from 'src/app/services/rest/log.service'
@@ -14,8 +14,11 @@ import { LogService } from 'src/app/services/rest/log.service'
templateUrl: './logs.component.html', templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'], styleUrls: ['./logs.component.scss'],
}) })
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { export class LogsComponent implements OnInit, OnDestroy {
constructor(private logService: LogService) {} constructor(
private logService: LogService,
private changedetectorRef: ChangeDetectorRef
) {}
public logs: string[] = [] public logs: string[] = []
@@ -47,10 +50,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
}) })
} }
ngAfterViewChecked() {
this.scrollToBottom()
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.unsubscribeNotifier.next(true) this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete() this.unsubscribeNotifier.complete()
@@ -66,6 +65,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
next: (result) => { next: (result) => {
this.logs = result this.logs = result
this.isLoading = false this.isLoading = false
this.scrollToBottom()
}, },
error: () => { error: () => {
this.logs = [] this.logs = []
@@ -89,6 +89,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
} }
scrollToBottom(): void { scrollToBottom(): void {
this.changedetectorRef.detectChanges()
this.logContainer?.nativeElement.scroll({ this.logContainer?.nativeElement.scroll({
top: this.logContainer.nativeElement.scrollHeight, top: this.logContainer.nativeElement.scrollHeight,
left: 0, left: 0,

View File

@@ -1,10 +1,13 @@
<pngx-page-header title="Settings" i18n-title> <pngx-page-header
title="Settings"
i18n-title
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button> <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank"> <a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container> <ng-container i18n>Open Django Admin</ng-container>
<svg class="sidebaricon ms-1" fill="currentColor"> <i-bs name="arrow-up-right"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
</svg>
</a> </a>
</pngx-page-header> </pngx-page-header>
@@ -135,204 +138,202 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()"> <button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<svg fill="currentColor" class="buttonicon-sm me-1"> <i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/> </button>
</svg><ng-container i18n>Reset</ng-container>
</button>
</div>
</div> </div>
</div>
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4> <h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<p i18n> <p i18n>
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/> Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
Actual updating of the app must still be performed manually. Actual updating of the app must still be performed manually.
</p> </p>
<p i18n> <p i18n>
<em>No tracking data is collected by the app in any way.</em> <em>No tracking data is collected by the app in any way.</em>
</p> </p>
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check> <pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
</div>
</div> </div>
</div>
<h4 class="mt-4" i18n>Bulk editing</h4> <h4 class="mt-4" i18n>Bulk editing</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check> <pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check> <pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
</div>
</div> </div>
</div>
<h4 class="mt-4" i18n>Notes</h4> <h4 class="mt-4" i18n>Notes</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check> <pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
</div>
</div> </div>
</div>
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="SettingsNavIDs.Permissions"> <li [ngbNavItem]="SettingsNavIDs.Permissions">
<a ngbNavLink i18n>Permissions</a> <a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<h4 i18n>Default Permissions</h4> <h4 i18n>Default Permissions</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<p i18n> <p i18n>
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
</p> </p>
</div>
</div> </div>
<div class="row mb-3"> </div>
<div class="col-md-3 col-form-label pt-0"> <div class="row mb-3">
<span i18n>Default Owner</span> <div class="col-md-3 col-form-label pt-0">
</div> <span i18n>Default Owner</span>
<div class="col-md-5">
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div>
</div> </div>
<div class="row mb-3"> <div class="col-md-5">
<div class="col-md-3 col-form-label pt-0"> <pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<span i18n>Default View Permissions</span> <small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div> </div>
<div class="col-md-5"> </div>
<div class="row"> <div class="row mb-3">
<div class="col-3"> <div class="col-md-3 col-form-label pt-0">
<span class="d-block pt-1" i18n>Users:</span> <span i18n>Default View Permissions</span>
</div> </div>
<div class="col"> <div class="col-md-5">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }"> <div class="row">
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user> <div class="col-3">
</ng-container> <span class="d-block pt-1" i18n>Users:</span>
</div>
</div> </div>
<div class="row"> <div class="col">
<div class="col-3"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<span class="d-block pt-1" i18n>Groups:</span> <pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
</div> </ng-container>
<div class="col"> </div>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }"> </div>
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group> <div class="row">
</ng-container> <div class="col-3">
</div> <span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
</ng-container>
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-3"> </div>
<div class="col-md-3 col-form-label pt-0"> <div class="row mb-3">
<span i18n>Default Edit Permissions</span> <div class="col-md-3 col-form-label pt-0">
</div> <span i18n>Default Edit Permissions</span>
<div class="col-md-5"> </div>
<div class="row"> <div class="col-md-5">
<div class="col-3"> <div class="row">
<span class="d-block pt-1" i18n>Users:</span> <div class="col-3">
</div> <span class="d-block pt-1" i18n>Users:</span>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
</ng-container>
</div>
</div> </div>
<div class="row"> <div class="col">
<div class="col-3"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<span class="d-block pt-1" i18n>Groups:</span> <pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
</div> </ng-container>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
</ng-container>
</div>
</div>
<div class="row">
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div> </div>
</div> </div>
</div> <div class="row">
</ng-template> <div class="col-3">
</li> <span class="d-block pt-1" i18n>Groups:</span>
</div>
<li [ngbNavItem]="SettingsNavIDs.Notifications"> <div class="col">
<a ngbNavLink i18n>Notifications</a> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<ng-template ngbNavContent> <pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
</ng-container>
<h4 i18n>Document processing</h4> </div>
</div>
<div class="row mb-3"> <div class="row">
<div class="offset-md-3 col"> <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
</div> </div>
</div> </div>
</div>
</ng-template>
</li>
</ng-template> <li [ngbNavItem]="SettingsNavIDs.Notifications">
</li> <a ngbNavLink i18n>Notifications</a>
<ng-template ngbNavContent>
<li [ngbNavItem]="SettingsNavIDs.SavedViews"> <h4 i18n>Document processing</h4>
<a ngbNavLink i18n>Saved views</a>
<ng-template ngbNavContent>
<h4 i18n>Settings</h4> <div class="row mb-3">
<div class="row mb-3"> <div class="offset-md-3 col">
<div class="offset-md-3 col"> <pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check> <pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
</div>
</div>
</ng-template>
</li>
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
<a ngbNavLink i18n>Saved views</a>
<ng-template ngbNavContent>
<h4 i18n>Settings</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
</div>
</div>
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
@for (view of savedViews; track view) {
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
</div>
</div> </div>
</div> }
<h4 i18n>Views</h4> @if (savedViews && savedViews.length === 0) {
<div formGroupName="savedViews"> <div i18n>No saved views defined.</div>
}
@for (view of savedViews; track view) { @if (!savedViews) {
<div [formGroupName]="view.id" class="row"> <div>
<div class="mb-3 col"> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<label class="form-label" for="name_{{view.id}}" i18n>Name</label> <div class="visually-hidden" i18n>Loading...</div>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}"> </div>
</div> }
<div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
</div>
</div>
}
@if (savedViews && savedViews.length === 0) { </div>
<div i18n>No saved views defined.</div>
}
@if (!savedViews) { </ng-template>
<div> </li>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> </ul>
<div class="visually-hidden" i18n>Loading...</div>
</div>
}
</div> <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
</ng-template> <button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
</li> </form>
</ul>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
</form>

View File

@@ -37,6 +37,7 @@ import { TextComponent } from '../../common/input/text/text.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SettingsComponent } from './settings.component' import { SettingsComponent } from './settings.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const savedViews = [ const savedViews = [
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true }, { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
@@ -92,6 +93,7 @@ describe('SettingsComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbAlertModule, NgbAlertModule,
NgSelectModule, NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -363,7 +363,7 @@ export class SettingsComponent
} }
ngOnDestroy() { ngOnDestroy() {
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didnt save if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
this.storeSub && this.storeSub.unsubscribe() this.storeSub && this.storeSub.unsubscribe()
this.settings.organizingSidebarSavedViews = false this.settings.organizingSidebarSavedViews = false
} }

View File

@@ -1,155 +1,155 @@
<pngx-page-header title="File Tasks" i18n-title> <pngx-page-header
title="File Tasks"
i18n-title
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
i18n-info
>
<div class="btn-toolbar col col-md-auto align-items-center"> <div class="btn-toolbar col col-md-auto align-items-center">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0"> <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/> </button>
</svg>&nbsp;<ng-container i18n>Clear selection</ng-container> <button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
</button> <i-bs name="check2-all"></i-bs>&nbsp;{{dismissButtonText}}
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0"> </button>
<svg class="sidebaricon" fill="currentColor"> <div class="form-check form-switch mb-0">
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/> <input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
</svg>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</button> </div>
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()"> </div>
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval"> </pngx-page-header>
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</div>
</pngx-page-header>
@if (!tasksService.completedFileTasks && tasksService.loading) { @if (!tasksService.completedFileTasks && tasksService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
} }
<ng-template let-tasks="tasks" #tasksTemplate> <ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm"> <table class="table table-striped align-middle border shadow-sm">
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();"> <input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-tasks"></label> <label class="form-check-label" for="all-tasks"></label>
</div>
</th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</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) {
&hellip;
}</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>&nbsp;<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>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td>
</tr>
}
</tbody>
</table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</div> </div>
</ng-template> </th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</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) {
&hellip;
}</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>&nbsp;<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>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td>
</tr>
}
</tbody>
</table>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)"> <div class="pb-3 d-sm-flex justify-content-between align-items-center">
<li ngbNavItem="failed"> @if (tasks.length > 0) {
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) { <div class="pb-2 pb-sm-0">
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span> <ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
}</a> @if (selectedTasks.size > 0) {
<ng-template ngbNavContent> <ng-container i18n>&nbsp;({{selectedTasks.size}} selected)</ng-container>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> }
</ng-template> </div>
</li> }
<li ngbNavItem="completed"> @if (tasks.length > pageSize) {
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) { <ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span> }
}</a> </div>
<ng-template ngbNavContent> </ng-template>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template> <ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
</li> <li ngbNavItem="failed">
<li ngbNavItem="started"> <a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) { <span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span> }</a>
}</a> <ng-template ngbNavContent>
<ng-template ngbNavContent> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> </ng-template>
</ng-template> </li>
</li> <li ngbNavItem="completed">
<li ngbNavItem="queued"> <a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) { <span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span> }</a>
}</a> <ng-template ngbNavContent>
<ng-template ngbNavContent> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container> </ng-template>
</ng-template> </li>
</li> <li ngbNavItem="started">
</ul> <a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<div [ngbNavOutlet]="nav"></div> <span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>

View File

@@ -28,6 +28,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TasksComponent } from './tasks.component' import { TasksComponent } from './tasks.component'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const tasks: PaperlessTask[] = [ const tasks: PaperlessTask[] = [
{ {
@@ -138,6 +139,7 @@ describe('TasksComponent', () => {
NgbModule, NgbModule,
HttpClientTestingModule, HttpClientTestingModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -1,14 +1,17 @@
<pngx-page-header title="Users & Groups" i18n-title> <pngx-page-header
title="Users & Groups"
i18n-title
info="Create, delete and edit users and groups."
i18n-info
infoLink="usage/#users-and-groups"
>
</pngx-page-header> </pngx-page-header>
@if (users) { @if (users) {
<h4 class="d-flex"> <h4 class="d-flex">
<ng-container i18n>Users</ng-container> <ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
<svg class="sidebaricon me-1" fill="currentColor"> <i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add User</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add User</ng-container>
</button> </button>
</h4> </h4>
<ul class="list-group"> <ul class="list-group">
@@ -29,14 +32,10 @@
<div class="col"> <div class="col">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
<svg class="buttonicon-sm" fill="currentColor"> <i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }"> <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
<svg class="buttonicon-sm" fill="currentColor"> <i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>
@@ -50,10 +49,7 @@
<h4 class="mt-4 d-flex"> <h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container> <ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
<svg class="sidebaricon me-1" fill="currentColor"> <i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Group</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add Group</ng-container>
</button> </button>
</h4> </h4>
@if (groups.length > 0) { @if (groups.length > 0) {
@@ -75,14 +71,10 @@
<div class="col"> <div class="col">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
<svg class="buttonicon-sm" fill="currentColor"> <i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }"> <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
<svg class="buttonicon-sm" fill="currentColor"> <i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -43,6 +43,7 @@ import { SettingsComponent } from '../settings/settings.component'
import { UsersAndGroupsComponent } from './users-groups.component' import { UsersAndGroupsComponent } from './users-groups.component'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const users = [ const users = [
{ id: 1, username: 'user1', is_superuser: false }, { id: 1, username: 'user1', is_superuser: false },
@@ -92,6 +93,7 @@ describe('UsersAndGroupsComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbAlertModule, NgbAlertModule,
NgSelectModule, NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(UsersAndGroupsComponent) fixture = TestBed.createComponent(UsersAndGroupsComponent)

View File

@@ -4,30 +4,36 @@
(click)="isMenuCollapsed = !isMenuCollapsed"> (click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0" <a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard" [ngClass]="{ 'slim': slimSidebarEnabled, 'd-flex col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard"
tourAnchor="tour.intro"> tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
<path <path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" /> transform="translate(0 0)" />
</svg> </svg>
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span> <div class="ms-2 d-inline-block" [class.visually-hidden]="slimSidebarEnabled">
@if (customAppTitle?.length) {
<div class="d-flex flex-column align-items-start">
<span class="title">{{customAppTitle}}</span>
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
</div>
} @else {
Paperless-ngx
}
</div>
</a> </a>
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1" <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<form (ngSubmit)="search()" class="form-inline flex-grow-1"> <form (ngSubmit)="search()" class="form-inline flex-grow-1">
<svg width="1em" height="1em" fill="currentColor"> <i-bs style="top: .25em;" width="1em" height="1em" name="search"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#search" />
</svg>
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
(selectItem)="itemSelected($event)" i18n-placeholder> (selectItem)="itemSelected($event)" i18n-placeholder>
@if (!searchFieldEmpty) { @if (!searchFieldEmpty) {
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> <button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<svg fill="currentColor" class="buttonicon-sm me-1"> <i-bs width="1em" height="1em" name="x"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
</button> </button>
} }
</form> </form>
@@ -38,9 +44,7 @@
<span class="small me-2 d-none d-sm-inline"> <span class="small me-2 d-none d-sm-inline">
{{this.settingsService.displayName}} {{this.settingsService.displayName}}
</span> </span>
<svg width="1.3em" height="1.3em" fill="currentColor"> <i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#person-circle" />
</svg>
</button> </button>
<div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown"> <div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown">
<div class="d-sm-none"> <div class="d-sm-none">
@@ -48,27 +52,19 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</div> </div>
<button ngbDropdownItem class="nav-link" (click)="editProfile()"> <button ngbDropdownItem class="nav-link" (click)="editProfile()">
<svg class="sidebaricon me-2" fill="currentColor"> <i-bs class="me-2" name="person"></i-bs>&nbsp;<ng-container i18n>My Profile</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><ng-container i18n>My Profile</ng-container>
</button> </button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }"> *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<svg class="sidebaricon me-2" fill="currentColor"> <i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><ng-container i18n>Settings</ng-container>
</a> </a>
<a ngbDropdownItem class="nav-link" href="accounts/logout/" (click)="onLogout()"> <a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
<svg class="sidebaricon me-2" fill="currentColor"> <i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#door-open" />
</svg><ng-container i18n>Logout</ng-container>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer" <a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
href="https://docs.paperless-ngx.com"> href="https://docs.paperless-ngx.com">
<svg class="sidebaricon me-2" fill="currentColor"> <i-bs class="me-2" name="question-circle"></i-bs><ng-container i18n>Documentation</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><ng-container i18n>Documentation</ng-container>
</a> </a>
</div> </div>
</li> </li>
@@ -81,13 +77,11 @@
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
[ngbCollapse]="isMenuCollapsed"> [ngbCollapse]="isMenuCollapsed">
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()"> <button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
<svg class="sidebaricon-sm" fill="currentColor"> @if (slimSidebarEnabled) {
@if (slimSidebarEnabled) { <i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-right" /> } @else {
} @else { <i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left" /> }
}
</svg>
</button> </button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around"> <div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
<ul class="nav flex-column"> <ul class="nav flex-column">
@@ -95,18 +89,14 @@
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="house"></i-bs><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#house" />
</svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="files"></i-bs><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#files" />
</svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -128,15 +118,11 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim"> popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="funnel"></i-bs><span>&nbsp;{{view.name}}</span>
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg><span>&nbsp;{{view.name}}</span>
</a> </a>
@if (settingsService.organizingSidebarSavedViews) { @if (settingsService.organizingSidebarSavedViews) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle> <div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
<svg class="sidebaricon text-muted" fill="currentColor"> <i-bs name="grip-vertical"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical" />
</svg>
</div> </div>
} }
</li> </li>
@@ -157,13 +143,9 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim"> popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="file-text"></i-bs><span>&nbsp;{{d.title | documentTitle}}</span>
<use xlink:href="assets/bootstrap-icons.svg#file-text" />
</svg><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close" (click)="closeDocument(d); $event.preventDefault()"> <span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg fill="currentColor" class="toolbaricon"> <i-bs name="x"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
</span> </span>
</a> </a>
</li> </li>
@@ -173,9 +155,7 @@
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" <a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="x"></i-bs><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a> </a>
</li> </li>
} }
@@ -191,9 +171,7 @@
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="person"></i-bs><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
@@ -201,9 +179,7 @@
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" <a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="tags"></i-bs><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#tags" />
</svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" <li class="nav-item"
@@ -211,27 +187,21 @@
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="hash"></i-bs><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#hash" />
</svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="folder"></i-bs><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#folder" />
</svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="ui-radios"></i-bs><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" <li class="nav-item"
@@ -240,9 +210,7 @@
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="boxes"></i-bs><span>&nbsp;<ng-container i18n>Workflows</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#boxes" />
</svg><span>&nbsp;<ng-container i18n>Workflows</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }" <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
@@ -250,9 +218,7 @@
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail" <a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="envelope"></i-bs><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#envelope" />
</svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -266,27 +232,21 @@
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="gear"></i-bs><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="sliders2-vertical"></i-bs><span>&nbsp;<ng-container i18n>Configuration</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#sliders2-vertical" />
</svg><span>&nbsp;<ng-container i18n>Configuration</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="people"></i-bs><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#people" />
</svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" <li class="nav-item"
@@ -295,23 +255,19 @@
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="list-task"></i-bs><span>&nbsp;<ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
}</span>
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) { @if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span> <span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
} }
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#list-task" />
</svg><span>&nbsp;<ng-container i18n>File Tasks@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
}</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" <a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="me-1" name="text-left"></i-bs><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#text-left" />
</svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item mt-2" tourAnchor="tour.outro"> <li class="nav-item mt-2" tourAnchor="tour.outro">
@@ -319,9 +275,7 @@
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled"> <li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
@@ -360,10 +314,7 @@
href="https://github.com/paperless-ngx/paperless-ngx/releases" href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
container="body"> container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" <i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
@if (appRemoteVersion?.update_available) { @if (appRemoteVersion?.update_available) {
<ng-container i18n>Update available</ng-container> <ng-container i18n>Update available</ng-container>
} }
@@ -373,10 +324,7 @@
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
container="body"> container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" <i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
</a> </a>
} }
</div> </div>

View File

@@ -152,9 +152,9 @@ main {
font-weight: bold; font-weight: bold;
} }
.sidebaricon { i-bs {
margin-right: 4px; position: relative;
color: inherit; top: -1px;
} }
} }
@@ -186,11 +186,11 @@ main {
width: 1.8rem; width: 1.8rem;
height: 100%; height: 100%;
svg { i-bs {
opacity: 0.5; opacity: 0.5;
} }
&:hover svg { &:hover i-bs {
opacity: 1; opacity: 1;
} }
} }
@@ -205,7 +205,7 @@ main {
text-decoration: underline; text-decoration: underline;
} }
svg { i-bs {
margin-bottom: 2px; margin-bottom: 2px;
} }
} }
@@ -217,9 +217,16 @@ main {
*/ */
.navbar-brand { .navbar-brand {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 1rem; font-size: 1rem;
.flex-column {
padding: 0.15rem 0;
}
.byline {
font-size: 0.5rem;
letter-spacing: 0.1rem;
}
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
@@ -241,7 +248,7 @@ main {
.navbar .dropdown-menu { .navbar .dropdown-menu {
font-size: 0.875rem; // body size font-size: 0.875rem; // body size
a svg { a i-bs {
opacity: 0.6; opacity: 0.6;
} }
} }
@@ -252,7 +259,7 @@ main {
form { form {
position: relative; position: relative;
> svg { > i-bs {
position: absolute; position: absolute;
left: 0.6rem; left: 0.6rem;
top: 0.5rem; top: 0.5rem;
@@ -262,7 +269,7 @@ main {
&:focus-within { &:focus-within {
form > svg { form > i-bs {
display: none; display: none;
} }
@@ -316,6 +323,6 @@ main {
cursor: move; cursor: move;
} }
::ng-deep .navItemDrag .position-absolute svg { ::ng-deep .navItemDrag .position-absolute i-bs {
display: none; display: none;
} }

View File

@@ -33,6 +33,7 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop' import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const saved_views = [ const saved_views = [
{ {
@@ -101,6 +102,7 @@ describe('AppFrameComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
DragDropModule, DragDropModule,
NgbModalModule, NgbModalModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [ providers: [
SettingsService, SettingsService,
@@ -248,7 +250,7 @@ describe('AppFrameComponent', () => {
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
}) })
it('should support collapsable menu', () => { it('should support collapsible menu', () => {
const button: HTMLButtonElement = ( const button: HTMLButtonElement = (
fixture.nativeElement as HTMLDivElement fixture.nativeElement as HTMLDivElement
).querySelector('button[data-toggle=collapse]') ).querySelector('button[data-toggle=collapse]')

View File

@@ -102,6 +102,10 @@ export class AppFrameComponent
}, 200) // slightly longer than css animation for slim sidebar }, 200) // slightly longer than css animation for slim sidebar
} }
get customAppTitle(): string {
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
}
get slimSidebarEnabled(): boolean { get slimSidebarEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR) return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
} }

View File

@@ -1,15 +1,11 @@
@if (active) { @if (active) {
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)"> <button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
@if (!isNumbered && selected) { @if (!isNumbered && selected) {
<svg width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
</svg>
} }
@if (isNumbered) { @if (isNumbered) {
<div class="number">{{number}}<span class="visually-hidden">selected</span></div> <div class="number">{{number}}<span class="visually-hidden">selected</span></div>
} }
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
</svg>
</button> </button>
} }

View File

@@ -22,7 +22,7 @@ button:hover {
.x { .x {
display: inline-block; display: inline-block;
position: absolute; position: absolute;
top: 5px; top: .4em;
left: calc(50% - 4px); left: calc(50% - .4em);
} }
} }

View File

@@ -1,5 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ClearableBadgeComponent } from './clearable-badge.component' import { ClearableBadgeComponent } from './clearable-badge.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('ClearableBadgeComponent', () => { describe('ClearableBadgeComponent', () => {
let component: ClearableBadgeComponent let component: ClearableBadgeComponent
@@ -8,6 +9,7 @@ describe('ClearableBadgeComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ClearableBadgeComponent], declarations: [ClearableBadgeComponent],
imports: [NgxBootstrapIconsModule.pick(allIcons)],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(ClearableBadgeComponent) fixture = TestBed.createComponent(ClearableBadgeComponent)

View File

@@ -12,8 +12,8 @@
} }
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span> <span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button> </button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
<span> <span>

View File

@@ -37,6 +37,12 @@ export class ConfirmDialogComponent {
@Input() @Input()
alternativeBtnCaption alternativeBtnCaption
@Input()
cancelBtnClass = 'btn-outline-secondary'
@Input()
cancelBtnCaption = $localize`Cancel`
@Input() @Input()
buttonsEnabled = true buttonsEnabled = true

View File

@@ -1,8 +1,6 @@
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()"> <div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()">
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor"> <i-bs name="ui-radios"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
</svg>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Custom Fields</ng-container></div> <div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Custom Fields</ng-container></div>
</button> </button>
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown"> <div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
@@ -20,14 +18,10 @@
</pngx-input-select> </pngx-input-select>
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields"> <button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields">
<svg fill="currentColor" class="buttonicon-sm me-1 mb-1"> <i-bs width="1em" height="1em" name="asterisk"></i-bs>&nbsp;<ng-container i18n>Create New Field</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#asterisk"/>
</svg><ng-container i18n>Create New Field</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-primary me-1" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined"> <button class="btn btn-sm btn-outline-primary me-1" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
<svg fill="currentColor" class="buttonicon me-1"> <i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#plus-circle"/>
</svg><ng-container i18n>Add</ng-container>
</button> </button>
</div> </div>
</li> </li>

View File

@@ -20,6 +20,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const fields: CustomField[] = [ const fields: CustomField[] = [
{ {
@@ -40,7 +41,6 @@ describe('CustomFieldsDropdownComponent', () => {
let customFieldService: CustomFieldsService let customFieldService: CustomFieldsService
let toastService: ToastService let toastService: ToastService
let modalService: NgbModal let modalService: NgbModal
let httpController: HttpTestingController
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -52,10 +52,10 @@ describe('CustomFieldsDropdownComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbModalModule, NgbModalModule,
NgbDropdownModule, NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}) })
customFieldService = TestBed.inject(CustomFieldsService) customFieldService = TestBed.inject(CustomFieldsService)
httpController = TestBed.inject(HttpTestingController)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
modalService = TestBed.inject(NgbModal) modalService = TestBed.inject(NgbModal)
jest.spyOn(customFieldService, 'listAll').mockReturnValue( jest.spyOn(customFieldService, 'listAll').mockReturnValue(

View File

@@ -9,9 +9,7 @@
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)"> <button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
<div class="selected-icon"> <div class="selected-icon">
@if (relativeDate === rd.id) { @if (relativeDate === rd.id) {
<svg fill="currentColor" class="buttonicon-sm"> <i-bs width="1em" height="1em" name="check"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
} }
</div> </div>
<div class="d-flex justify-content-between w-100 align-items-center ps-2"> <div class="d-flex justify-content-between w-100 align-items-center ps-2">
@@ -32,9 +30,7 @@
<div i18n>After</div> <div i18n>After</div>
@if (dateAfter) { @if (dateAfter) {
<a class="btn btn-link p-0 m-0" (click)="clearAfter()"> <a class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg fill="currentColor" class="buttonicon-sm"> <i-bs width="1em" height="1em" name="x"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
} }
@@ -44,9 +40,7 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)" <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker"> maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button"> <button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm"> <i-bs width="1em" height="1em" name="calendar"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button> </button>
</div> </div>
@@ -57,9 +51,7 @@
<div i18n>Before</div> <div i18n>Before</div>
@if (dateBefore) { @if (dateBefore) {
<a class="btn btn-link p-0 m-0" (click)="clearBefore()"> <a class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg fill="currentColor" class="buttonicon-sm"> <i-bs width="1em" height="1em" name="x"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
} }
@@ -69,9 +61,7 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)" <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker"> maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button"> <button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm"> <i-bs width="1em" height="1em" name="calendar"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button> </button>
</div> </div>

View File

@@ -10,20 +10,17 @@ import {
DateSelection, DateSelection,
RelativeDate, RelativeDate,
} from './date-dropdown.component' } from './date-dropdown.component'
import { import { HttpClientTestingModule } from '@angular/common/http/testing'
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('DateDropdownComponent', () => { describe('DateDropdownComponent', () => {
let component: DateDropdownComponent let component: DateDropdownComponent
let httpTestingController: HttpTestingController
let settingsService: SettingsService let settingsService: SettingsService
let settingsSpy let settingsSpy
@@ -40,10 +37,10 @@ describe('DateDropdownComponent', () => {
NgbModule, NgbModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()
httpTestingController = TestBed.inject(HttpTestingController)
settingsService = TestBed.inject(SettingsService) settingsService = TestBed.inject(SettingsService)
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat') settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>

View File

@@ -5,7 +5,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select> <pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
@if (typeFieldDisabled) { @if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small> <small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>

View File

@@ -97,7 +97,7 @@ export abstract class EditDialogComponent<
}) })
} }
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM // wait to enable close button so it doesn't steal focus from input since its the first clickable element in the DOM
setTimeout(() => { setTimeout(() => {
this.closeEnabled = true this.closeEnabled = true
}) })

View File

@@ -7,7 +7,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></pngx-permissions-select> <pngx-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></pngx-permissions-select>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></pngx-input-text> <pngx-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></pngx-input-text>
<pngx-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></pngx-input-text> <pngx-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></pngx-input-text>
<pngx-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></pngx-input-select> <pngx-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></pngx-input-select>

View File

@@ -7,7 +7,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select> <pngx-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select>
<pngx-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></pngx-input-text> <pngx-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></pngx-input-text>
<pngx-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></pngx-input-number> <pngx-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></pngx-input-number>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text> <pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {

View File

@@ -5,7 +5,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color> <pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>

View File

@@ -13,6 +13,7 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { TagEditDialogComponent } from './tag-edit-dialog.component' import { TagEditDialogComponent } from './tag-edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('TagEditDialogComponent', () => { describe('TagEditDialogComponent', () => {
let component: TagEditDialogComponent let component: TagEditDialogComponent
@@ -38,6 +39,7 @@ describe('TagEditDialogComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgSelectModule, NgSelectModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -7,7 +7,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
</div> </div>
<div class="col-4"> <div class="col-4">
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number> <pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
@@ -27,10 +27,7 @@
<div class="d-flex"> <div class="d-flex">
<p class="p-2" i18n>Trigger Workflow On:</p> <p class="p-2" i18n>Trigger Workflow On:</p>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()"> <button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
<svg class="sidebaricon me-1" fill="currentColor"> <i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Trigger</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add Trigger</ng-container>
</button> </button>
</div> </div>
<div ngbAccordion [closeOthers]="true"> <div ngbAccordion [closeOthers]="true">
@@ -42,10 +39,7 @@
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span> <span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
} }
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)"> <button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)">
<svg class="sidebaricon me-1" fill="currentColor"> <i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>
<ng-container i18n>Delete</ng-container>
</button> </button>
</button> </button>
</div> </div>
@@ -71,10 +65,7 @@
<div class="d-flex"> <div class="d-flex">
<p class="p-2" i18n>Apply Actions:</p> <p class="p-2" i18n>Apply Actions:</p>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()"> <button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
<svg class="sidebaricon me-1" fill="currentColor"> <i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Action</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add Action</ng-container>
</button> </button>
</div> </div>
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)"> <div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
@@ -86,10 +77,7 @@
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span> <span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
} }
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)"> <button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
<svg class="sidebaricon me-1" fill="currentColor"> <i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>
<ng-container i18n>Delete</ng-container>
</button> </button>
</button> </button>
</div> </div>
@@ -99,7 +87,7 @@
<input type="hidden" formControlName="id" /> <input type="hidden" formControlName="id" />
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.assign_title"></pngx-input-text> <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags> <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select> <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
@@ -182,7 +170,7 @@
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text> <pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) { @if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select> <pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text> <pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select> <pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
} }
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) { @if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {

View File

@@ -66,7 +66,7 @@ const workflow: Workflow = {
], ],
} }
describe('ConsumptionTemplateEditDialogComponent', () => { describe('WorkflowEditDialogComponent', () => {
let component: WorkflowEditDialogComponent let component: WorkflowEditDialogComponent
let settingsService: SettingsService let settingsService: SettingsService
let fixture: ComponentFixture<WorkflowEditDialogComponent> let fixture: ComponentFixture<WorkflowEditDialogComponent>
@@ -219,6 +219,7 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
const action1 = workflow.actions[0] const action1 = workflow.actions[0]
const action2 = workflow.actions[1] const action2 = workflow.actions[1]
component.object = workflow component.object = workflow
component.ngOnInit()
component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop< component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
WorkflowAction[] WorkflowAction[]
>) >)

View File

@@ -19,6 +19,7 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
import { CustomField } from 'src/app/data/custom-field' import { CustomField } from 'src/app/data/custom-field'
import { import {
DocumentSource, DocumentSource,
WorkflowTrigger,
WorkflowTriggerType, WorkflowTriggerType,
} from 'src/app/data/workflow-trigger' } from 'src/app/data/workflow-trigger'
import { import {
@@ -157,7 +158,7 @@ export class WorkflowEditDialogComponent
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit() super.ngOnInit()
this.updateTriggerActionFields() this.updateAllTriggerActionFields()
} }
get triggerFields(): FormArray { get triggerFields(): FormArray {
@@ -168,52 +169,66 @@ export class WorkflowEditDialogComponent
return this.objectForm.get('actions') as FormArray return this.objectForm.get('actions') as FormArray
} }
private updateTriggerActionFields(emitEvent: boolean = false) { private createTriggerField(
trigger: WorkflowTrigger,
emitEvent: boolean = false
) {
this.triggerFields.push(
new FormGroup({
id: new FormControl(trigger.id),
type: new FormControl(trigger.type),
sources: new FormControl(trigger.sources),
filter_filename: new FormControl(trigger.filter_filename),
filter_path: new FormControl(trigger.filter_path),
filter_mailrule: new FormControl(trigger.filter_mailrule),
matching_algorithm: new FormControl(trigger.matching_algorithm),
match: new FormControl(trigger.match),
is_insensitive: new FormControl(trigger.is_insensitive),
filter_has_tags: new FormControl(trigger.filter_has_tags),
filter_has_correspondent: new FormControl(
trigger.filter_has_correspondent
),
filter_has_document_type: new FormControl(
trigger.filter_has_document_type
),
}),
{ emitEvent }
)
}
private createActionField(
action: WorkflowAction,
emitEvent: boolean = false
) {
this.actionFields.push(
new FormGroup({
id: new FormControl(action.id),
type: new FormControl(action.type),
assign_title: new FormControl(action.assign_title),
assign_tags: new FormControl(action.assign_tags),
assign_owner: new FormControl(action.assign_owner),
assign_document_type: new FormControl(action.assign_document_type),
assign_correspondent: new FormControl(action.assign_correspondent),
assign_storage_path: new FormControl(action.assign_storage_path),
assign_view_users: new FormControl(action.assign_view_users),
assign_view_groups: new FormControl(action.assign_view_groups),
assign_change_users: new FormControl(action.assign_change_users),
assign_change_groups: new FormControl(action.assign_change_groups),
assign_custom_fields: new FormControl(action.assign_custom_fields),
}),
{ emitEvent }
)
}
private updateAllTriggerActionFields(emitEvent: boolean = false) {
this.triggerFields.clear({ emitEvent: false }) this.triggerFields.clear({ emitEvent: false })
this.object?.triggers.forEach((trigger) => { this.object?.triggers.forEach((trigger) => {
this.triggerFields.push( this.createTriggerField(trigger, emitEvent)
new FormGroup({
id: new FormControl(trigger.id),
type: new FormControl(trigger.type),
sources: new FormControl(trigger.sources),
filter_filename: new FormControl(trigger.filter_filename),
filter_path: new FormControl(trigger.filter_path),
filter_mailrule: new FormControl(trigger.filter_mailrule),
matching_algorithm: new FormControl(MATCH_NONE),
match: new FormControl(''),
is_insensitive: new FormControl(true),
filter_has_tags: new FormControl(trigger.filter_has_tags),
filter_has_correspondent: new FormControl(
trigger.filter_has_correspondent
),
filter_has_document_type: new FormControl(
trigger.filter_has_document_type
),
}),
{ emitEvent }
)
}) })
this.actionFields.clear({ emitEvent: false }) this.actionFields.clear({ emitEvent: false })
this.object?.actions.forEach((action) => { this.object?.actions.forEach((action) => {
this.actionFields.push( this.createActionField(action, emitEvent)
new FormGroup({
id: new FormControl(action.id),
type: new FormControl(action.type),
assign_title: new FormControl(action.assign_title),
assign_tags: new FormControl(action.assign_tags),
assign_owner: new FormControl(action.assign_owner),
assign_document_type: new FormControl(action.assign_document_type),
assign_correspondent: new FormControl(action.assign_correspondent),
assign_storage_path: new FormControl(action.assign_storage_path),
assign_view_users: new FormControl(action.assign_view_users),
assign_view_groups: new FormControl(action.assign_view_groups),
assign_change_users: new FormControl(action.assign_change_users),
assign_change_groups: new FormControl(action.assign_change_groups),
assign_custom_fields: new FormControl(action.assign_custom_fields),
}),
{ emitEvent }
)
}) })
} }
@@ -233,7 +248,7 @@ export class WorkflowEditDialogComponent
if (!this.object) { if (!this.object) {
this.object = Object.assign({}, this.objectForm.value) this.object = Object.assign({}, this.objectForm.value)
} }
this.object.triggers.push({ const trigger: WorkflowTrigger = {
type: WorkflowTriggerType.Consumption, type: WorkflowTriggerType.Consumption,
sources: [], sources: [],
filter_filename: null, filter_filename: null,
@@ -242,9 +257,12 @@ export class WorkflowEditDialogComponent
filter_has_tags: [], filter_has_tags: [],
filter_has_correspondent: null, filter_has_correspondent: null,
filter_has_document_type: null, filter_has_document_type: null,
}) matching_algorithm: MATCH_NONE,
match: '',
this.updateTriggerActionFields() is_insensitive: true,
}
this.object.triggers.push(trigger)
this.createTriggerField(trigger)
} }
get actionTypeOptions() { get actionTypeOptions() {
@@ -259,7 +277,7 @@ export class WorkflowEditDialogComponent
if (!this.object) { if (!this.object) {
this.object = Object.assign({}, this.objectForm.value) this.object = Object.assign({}, this.objectForm.value)
} }
this.object.actions.push({ const action: WorkflowAction = {
type: WorkflowActionType.Assignment, type: WorkflowActionType.Assignment,
assign_title: null, assign_title: null,
assign_tags: [], assign_tags: [],
@@ -272,19 +290,19 @@ export class WorkflowEditDialogComponent
assign_change_users: [], assign_change_users: [],
assign_change_groups: [], assign_change_groups: [],
assign_custom_fields: [], assign_custom_fields: [],
}) }
this.object.actions.push(action)
this.updateTriggerActionFields() this.createActionField(action)
} }
removeTrigger(index: number) { removeTrigger(index: number) {
this.object.triggers.splice(index, 1) this.object.triggers.splice(index, 1).pop()
this.updateTriggerActionFields() this.triggerFields.removeAt(index)
} }
removeAction(index: number) { removeAction(index: number) {
this.object.actions.splice(index, 1) this.object.actions.splice(index, 1)
this.updateTriggerActionFields() this.actionFields.removeAt(index)
} }
onActionDrop(event: CdkDragDrop<WorkflowAction[]>) { onActionDrop(event: CdkDragDrop<WorkflowAction[]>) {
@@ -293,8 +311,10 @@ export class WorkflowEditDialogComponent
event.previousIndex, event.previousIndex,
event.currentIndex event.currentIndex
) )
const actionField = this.actionFields.at(event.previousIndex)
this.actionFields.removeAt(event.previousIndex)
this.actionFields.insert(event.currentIndex, actionField)
// removing id will effectively re-create the actions in this order // removing id will effectively re-create the actions in this order
this.object.actions.forEach((a) => (a.id = null)) this.object.actions.forEach((a) => (a.id = null))
this.updateTriggerActionFields()
} }
} }

View File

@@ -1,8 +1,6 @@
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)"> <div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)">
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> <button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<svg class="toolbaricon" fill="currentColor"> <i-bs name="{{icon}}"></i-bs>
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div> <div class="d-none d-sm-inline">&nbsp;{{title}}</div>
@if (!editing && selectionModel.totalCount > 0) { @if (!editing && selectionModel.totalCount > 0) {
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge> <pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
@@ -49,9 +47,7 @@
@if (editing) { @if (editing) {
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled"> <button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small> <small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg>
</button> </button>
} }
@if (!editing && manyToOne) { @if (!editing && manyToOne) {

View File

@@ -25,10 +25,6 @@
} }
} }
small > svg {
margin-top: -2px;
}
.list-group-item-note { .list-group-item-note {
line-height: 1; line-height: 1;

View File

@@ -25,6 +25,7 @@ import {
import { TagComponent } from '../tag/tag.component' import { TagComponent } from '../tag/tag.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const items: Tag[] = [ const items: Tag[] = [
{ {
@@ -63,7 +64,12 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
ClearableBadgeComponent, ClearableBadgeComponent,
], ],
providers: [FilterPipe], providers: [FilterPipe],
imports: [NgbModule, FormsModule, ReactiveFormsModule], imports: [
NgbModule,
FormsModule,
ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(FilterableDropdownComponent) fixture = TestBed.createComponent(FilterableDropdownComponent)
@@ -215,6 +221,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should apply changes and close when apply button clicked', () => { it('should apply changes and close when apply button clicked', () => {
component.items = items component.items = items
component.icon = 'tag-fill'
component.editing = true component.editing = true
component.selectionModel = selectionModel component.selectionModel = selectionModel
fixture.nativeElement fixture.nativeElement
@@ -236,6 +243,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should apply on close if enabled', () => { it('should apply on close if enabled', () => {
component.items = items component.items = items
component.icon = 'tag-fill'
component.editing = true component.editing = true
component.applyOnClose = true component.applyOnClose = true
component.selectionModel = selectionModel component.selectionModel = selectionModel
@@ -253,6 +261,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => { it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open .dispatchEvent(new MouseEvent('click')) // open
@@ -279,6 +288,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => { it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
expect(component.selectionModel.getSelectedItems()).toEqual([]) expect(component.selectionModel.getSelectedItems()).toEqual([])
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
@@ -298,6 +308,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => { it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
component.editing = true component.editing = true
let applyResult: ChangedItems let applyResult: ChangedItems
component.apply.subscribe((result) => (applyResult = result)) component.apply.subscribe((result) => (applyResult = result))
@@ -319,6 +330,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should support arrow keyboard navigation', fakeAsync(() => { it('should support arrow keyboard navigation', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open .dispatchEvent(new MouseEvent('click')) // open
@@ -363,6 +375,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => { it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open .dispatchEvent(new MouseEvent('click')) // open
@@ -398,6 +411,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should support arrow keyboard navigation after click', fakeAsync(() => { it('should support arrow keyboard navigation after click', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open .dispatchEvent(new MouseEvent('click')) // open
@@ -422,6 +436,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should toggle logical operator', fakeAsync(() => { it('should toggle logical operator', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
component.manyToOne = true component.manyToOne = true
selectionModel.set(items[0].id, ToggleableItemState.Selected) selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected) selectionModel.set(items[1].id, ToggleableItemState.Selected)
@@ -450,6 +465,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should toggle intersection include / exclude', fakeAsync(() => { it('should toggle intersection include / exclude', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill'
selectionModel.set(items[0].id, ToggleableItemState.Selected) selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected) selectionModel.set(items[1].id, ToggleableItemState.Selected)
component.selectionModel = selectionModel component.selectionModel = selectionModel

View File

@@ -1,19 +1,13 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
@if (isChecked()) { @if (isChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-check"> <i-bs width="1em" height="1em" name="check"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
} }
@if (isPartiallyChecked()) { @if (isPartiallyChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-dash"> <i-bs width="1em" height="1em" name="dash"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg>
} }
@if (isExcluded()) { @if (isExcluded()) {
<svg fill="currentColor" class="buttonicon-sm bi-x"> <i-bs width="1em" height="1em" name="x"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
} }
</div> </div>
<div class="me-1"> <div class="me-1">

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core' import { Component, EventEmitter, Input, Output } from '@angular/core'
import { MatchingModel } from 'src/app/data/matching-model' import { MatchingModel } from 'src/app/data/matching-model'
export enum ToggleableItemState { export enum ToggleableItemState {

View File

@@ -5,9 +5,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
} }
</div> </div>

View File

@@ -16,10 +16,7 @@
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow"> <input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
<button class="btn btn-outline-secondary" type="button" (click)="randomize()"> <button class="btn btn-outline-secondary" type="button" (click)="randomize()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16"> <i-bs name="dice5"></i-bs>
<path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/>
<path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
</svg>
</button> </button>
</div> </div>

View File

@@ -7,6 +7,7 @@ import {
import { ColorComponent } from './color.component' import { ColorComponent } from './color.component'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { ColorSliderModule } from 'ngx-color/slider' import { ColorSliderModule } from 'ngx-color/slider'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('ColorComponent', () => { describe('ColorComponent', () => {
let component: ColorComponent let component: ColorComponent
@@ -22,6 +23,7 @@ describe('ColorComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbPopoverModule, NgbPopoverModule,
ColorSliderModule, ColorSliderModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -4,9 +4,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
} }
</div> </div>
@@ -16,15 +14,11 @@
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)" (dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled"> name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled"> <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon"> <i-bs width="1.2em" height="1.2em" name="calendar"></i-bs>
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
</svg>
</button> </button>
@if (showFilter) { @if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}"> <button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor"> <i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button> </button>
} }
</div> </div>

View File

@@ -12,6 +12,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter' import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('DateComponent', () => { describe('DateComponent', () => {
let component: DateComponent let component: DateComponent
@@ -33,6 +34,7 @@ describe('DateComponent', () => {
HttpClientTestingModule, HttpClientTestingModule,
NgbDatepickerModule, NgbDatepickerModule,
RouterTestingModule, RouterTestingModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -6,51 +6,45 @@
} }
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/> </button>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> }
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div>
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
[disabled]="disabled"
[items]="foundDocuments$ | async"
placeholder="Search for documents"
[notFoundText]="notFoundText"
[multiple]="true"
bindValue="id"
[compareWith]="compareDocuments"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="loading"
[typeahead]="documentsInput$"
(change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center">
<svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
<svg class="sidebaricon-sm me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>{{document.title}}</span>
</a>
</div>
</ng-template>
<ng-template ng-loadingspinner-tmp>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-template>
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
</ng-template>
</ng-select>
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div>
</div>
</div> </div>
<div [class.col-md-9]="horizontal">
<div>
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
[disabled]="disabled"
[items]="foundDocuments$ | async"
placeholder="Search for documents"
[notFoundText]="notFoundText"
[multiple]="true"
bindValue="id"
[compareWith]="compareDocuments"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="loading"
[typeahead]="documentsInput$"
(change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center">
<i-bs (click)="unselect(document)" name="x"></i-bs>
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<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>

View File

@@ -7,7 +7,7 @@
} }
} }
.sidebaricon { i-bs {
cursor: pointer; cursor: pointer;
} }

View File

@@ -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>&nbsp;<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>

View File

@@ -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()
})
})

View File

@@ -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)
}
}

View File

@@ -6,9 +6,7 @@
} }
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
} }
</div> </div>

View File

@@ -56,8 +56,13 @@ describe('NumberComponent', () => {
component.step = 0.1 component.step = 0.1
component.writeValue(12.3456) component.writeValue(12.3456)
expect(component.value).toEqual(12.3456) expect(component.value).toEqual(12.3456)
// float (step = .1) doesnt force 2 decimals // float (step = .1) doesn't force 2 decimals
component.writeValue(11.1) component.writeValue(11.1)
expect(component.value).toEqual(11.1) expect(component.value).toEqual(11.1)
}) })
it('should support scientific notation', () => {
component.writeValue(1.23456789e8)
expect(component.value).toEqual(123456789)
})
}) })

View File

@@ -37,7 +37,8 @@ export class NumberComponent extends AbstractInputComponent<number> {
} }
writeValue(newValue: any): void { writeValue(newValue: any): void {
if (this.step === 1) newValue = parseInt(newValue, 10) if (this.step === 1 && newValue?.toString().indexOf('e') === -1)
newValue = parseInt(newValue, 10)
if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2) if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2)
super.writeValue(newValue) super.writeValue(newValue)
} }

View File

@@ -4,9 +4,7 @@
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete"> <input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
@if (showReveal) { @if (showReveal) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle"> <button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <i-bs name="eye"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg>
</button> </button>
} }
</div> </div>

View File

@@ -6,6 +6,7 @@ import {
} from '@angular/forms' } from '@angular/forms'
import { PasswordComponent } from './password.component' import { PasswordComponent } from './password.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('PasswordComponent', () => { describe('PasswordComponent', () => {
let component: PasswordComponent let component: PasswordComponent
@@ -16,7 +17,11 @@ describe('PasswordComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordComponent], declarations: [PasswordComponent],
providers: [], providers: [],
imports: [FormsModule, ReactiveFormsModule], imports: [
FormsModule,
ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(PasswordComponent) fixture = TestBed.createComponent(PasswordComponent)
@@ -28,7 +33,7 @@ describe('PasswordComponent', () => {
it('should support use of input field', () => { it('should support use of input field', () => {
expect(component.value).toBeUndefined() expect(component.value).toBeUndefined()
// TODO: why doesnt this work? // TODO: why doesn't this work?
// input.value = 'foo' // input.value = 'foo'
// input.dispatchEvent(new Event('change')) // input.dispatchEvent(new Event('change'))
// fixture.detectChanges() // fixture.detectChanges()

View File

@@ -6,9 +6,7 @@
} }
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
} }
</div> </div>
@@ -40,16 +38,12 @@
</ng-select> </ng-select>
@if (allowCreateNew) { @if (allowCreateNew) {
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled"> <button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor"> <i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button> </button>
} }
@if (showFilter) { @if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}"> <button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor"> <i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button> </button>
} }
</div> </div>

View File

@@ -2,12 +2,15 @@
<div class="row"> <div class="row">
@if (!horizontal) { @if (!horizontal) {
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3"> <div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
<label class="form-label" [for]="inputId">{{title}}</label> <label class="form-label" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
{{title}}
@if (showUnsetNote && isUnset) {
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
</label>
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
} }
</div> </div>
@@ -16,7 +19,12 @@
<div class="form-check form-switch"> <div class="form-check form-switch">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled"> <input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
@if (horizontal) { @if (horizontal) {
<label class="form-check-label" [for]="inputId">{{title}}</label> <label class="form-check-label" [class.text-muted]="showUnsetNote && isUnset" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
{{title}}
@if (showUnsetNote && isUnset) {
&nbsp;<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
</label>
} }
@if (hint) { @if (hint) {
<div class="form-text text-muted">{{hint}}</div> <div class="form-text text-muted">{{hint}}</div>
@@ -25,3 +33,8 @@
</div> </div>
</div> </div>
</div> </div>
<ng-template #tipContent>
<span class="text-light fst-italic" i18n>Note: value has not yet been set and will not apply until explicitly changed</span>
</ng-template>

View File

@@ -5,6 +5,7 @@ import {
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
describe('SwitchComponent', () => { describe('SwitchComponent', () => {
let component: SwitchComponent let component: SwitchComponent
@@ -15,7 +16,7 @@ describe('SwitchComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [SwitchComponent], declarations: [SwitchComponent],
providers: [], providers: [],
imports: [FormsModule, ReactiveFormsModule], imports: [FormsModule, ReactiveFormsModule, NgbTooltipModule],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(SwitchComponent) fixture = TestBed.createComponent(SwitchComponent)
@@ -36,4 +37,9 @@ describe('SwitchComponent', () => {
fixture.detectChanges() fixture.detectChanges()
expect(component.value).toBeFalsy() expect(component.value).toBeFalsy()
}) })
it('should show note if unset', () => {
component.value = null
expect(component.isUnset).toBeTruthy()
})
}) })

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef } from '@angular/core' import { Component, Input, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms' import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input' import { AbstractInputComponent } from '../abstract-input'
@@ -15,7 +15,14 @@ import { AbstractInputComponent } from '../abstract-input'
styleUrls: ['./switch.component.scss'], styleUrls: ['./switch.component.scss'],
}) })
export class SwitchComponent extends AbstractInputComponent<boolean> { export class SwitchComponent extends AbstractInputComponent<boolean> {
@Input()
showUnsetNote: boolean = false
constructor() { constructor() {
super() super()
} }
get isUnset(): boolean {
return this.value === null || this.value === undefined
}
} }

View File

@@ -18,9 +18,7 @@
<ng-template ng-label-tmp let-item="item"> <ng-template ng-label-tmp let-item="item">
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)"> <span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <i-bs name="x"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
@if (item.id && tags) { @if (item.id && tags) {
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag> <pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
} }
@@ -36,16 +34,12 @@
</ng-select> </ng-select>
@if (allowCreate) { @if (allowCreate) {
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled"> <button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor"> <i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button> </button>
} }
@if (showFilter) { @if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags"> <button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
<svg class="buttonicon" fill="currentColor"> <i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button> </button>
} }
</div> </div>

View File

@@ -30,6 +30,7 @@ import { ColorComponent } from '../color/color.component'
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component' import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../select/select.component' import { SelectComponent } from '../select/select.component'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
const tags: Tag[] = [ const tags: Tag[] = [
{ {
@@ -99,6 +100,7 @@ describe('TagsComponent', () => {
NgbModalModule, NgbModalModule,
NgbAccordionModule, NgbAccordionModule,
NgbPopoverModule, NgbPopoverModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@@ -6,9 +6,7 @@
} }
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
} }
</div> </div>

View File

@@ -27,7 +27,7 @@ describe('TextComponent', () => {
it('should support use of input field', () => { it('should support use of input field', () => {
expect(component.value).toBeUndefined() expect(component.value).toBeUndefined()
// TODO: why doesnt this work? // TODO: why doesn't this work?
// input.value = 'foo' // input.value = 'foo'
// input.dispatchEvent(new Event('change')) // input.dispatchEvent(new Event('change'))
// fixture.detectChanges() // fixture.detectChanges()

View File

@@ -4,27 +4,23 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) { @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#x"/> </button>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> }
</button> </div>
} <div [class.col-md-9]="horizontal">
</div> <div class="input-group" [class.is-invalid]="error">
<div [class.col-md-9]="horizontal"> <input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
<div class="input-group" [class.is-invalid]="error"> <a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled"> <i-bs width="1.2em" height="1.2em" name="box-arrow-up-right"></i-bs>
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank"> </a>
<svg class="buttonicon mb-1" fill="currentColor"> <div class="invalid-feedback position-absolute top-100">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up-right" /> {{error}}
</svg>
</a>
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
</div> </div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div> </div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div> </div>
</div> </div>
</div>

Some files were not shown because too many files have changed in this diff Show More