Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cf9f239ed | ||
|
|
f009d9868e | ||
|
|
1bbcd0961b | ||
|
|
4fa2b54aed | ||
|
|
7281c110c6 | ||
|
|
f812f2af4d | ||
|
|
47b4a602a7 | ||
|
|
ca73c0d1f3 | ||
|
|
7f6a50be5b | ||
|
|
10e10f9ff4 | ||
|
|
95c24a50f7 | ||
|
|
d06faa2fcb | ||
|
|
bed66cced0 | ||
|
|
ceaf60e6ad | ||
|
|
9885ca5103 | ||
|
|
2f22beaaee | ||
|
|
fb2c6282a4 | ||
|
|
8c5b5d3948 | ||
|
|
4e5135fe70 | ||
|
|
579c35a3fe | ||
|
|
4aedcb856d | ||
|
|
0b34e70f6c | ||
|
|
7afc91e7b1 | ||
|
|
56b17ce6a2 | ||
|
|
954912cac3 | ||
|
|
e46f6b1156 | ||
|
|
1d85caa8d0 | ||
|
|
622fcf96a0 | ||
|
|
654cc05f0e | ||
|
|
974dd24e69 | ||
|
|
fe824e0faa | ||
|
|
377d89ae06 | ||
|
|
629e24e031 | ||
|
|
38414025c8 | ||
|
|
1dc5b7a707 | ||
|
|
f076418c50 | ||
|
|
bbaad2cdfb | ||
|
|
ef01658335 | ||
|
|
9f4a6c3b42 | ||
|
|
5450bfb67b | ||
|
|
ae2b302962 | ||
|
|
957691c454 | ||
|
|
6b17ba2934 | ||
|
|
4d3616cda9 | ||
|
|
c4a9697e02 | ||
|
|
c57b7520b9 | ||
|
|
46bd09227f | ||
|
|
971f92a05c | ||
|
|
2c43b06910 | ||
|
|
0f8b2e69c9 | ||
|
|
00b04c2e86 | ||
|
|
b3c66cae06 | ||
|
|
6a79d417b4 | ||
|
|
98ef68f720 | ||
|
|
ed3b7aa8f2 | ||
|
|
e536600052 | ||
|
|
bb820a2127 | ||
|
|
129933ff30 | ||
|
|
41fc11efff | ||
|
|
a712bc72ca | ||
|
|
fbe7acc6b0 | ||
|
|
c4153b6fbf | ||
|
|
1f355a22e0 | ||
|
|
4af8070450 | ||
|
|
d6d0071175 | ||
|
|
ef51633b2c | ||
|
|
d4963b9cbe | ||
|
|
01dabf7c05 | ||
|
|
fc68f79cc8 | ||
|
|
ebe1479503 | ||
|
|
8c9fe4da06 | ||
|
|
b2ef51af55 | ||
|
|
32b35d8e4b |
@@ -47,11 +47,11 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.3.2'
|
||||
rev: 'v0.3.5'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.2.0
|
||||
rev: 24.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
# Dockerfile hooks
|
||||
@@ -67,6 +67,6 @@ repos:
|
||||
args:
|
||||
- "--tab"
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: "v0.9.0.6"
|
||||
rev: "v0.10.0.1"
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
||||
@@ -17,7 +17,7 @@ django-extensions = "*"
|
||||
django-filter = "~=24.1"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
djangorestframework = "~=3.14"
|
||||
djangorestframework = "==3.14.0"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
|
||||
Generated
+401
-399
File diff suppressed because it is too large
Load Diff
@@ -349,11 +349,17 @@ When you use the provided docker compose script, put the export inside
|
||||
the `export` folder in your paperless source directory. Specify
|
||||
`../export` as the `source`.
|
||||
|
||||
Note that .zip files (as can be generated from the exporter) are not supported.
|
||||
|
||||
!!! note
|
||||
|
||||
Importing from a previous version of Paperless may work, but for best
|
||||
results it is suggested to match the versions.
|
||||
|
||||
!!! warning
|
||||
|
||||
The importer should be run against a completely empty installation (database and directories) of Paperless-ngx.
|
||||
|
||||
### Document retagger {#retagger}
|
||||
|
||||
Say you've imported a few hundred documents and now want to introduce a
|
||||
|
||||
@@ -649,8 +649,9 @@ external authentication solution using one of the following methods:
|
||||
|
||||
This is a simple option that uses remote user authentication made available by certain SSO
|
||||
applications. See the relevant configuration options for more information:
|
||||
[PAPERLESS_ENABLE_HTTP_REMOTE_USER](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER) and
|
||||
[PAPERLESS_ENABLE_HTTP_REMOTE_USER](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER),
|
||||
[PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME](configuration.md#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME)
|
||||
and [PAPERLESS_LOGOUT_REDIRECT_URL](configuration.md#PAPERLESS_LOGOUT_REDIRECT_URL)
|
||||
|
||||
### OpenID Connect and social authentication
|
||||
|
||||
|
||||
+14
@@ -288,6 +288,8 @@ The endpoint supports the following optional form fields:
|
||||
- `tags`: Similar to correspondent. Specify this multiple times to
|
||||
have multiple tags added to the document.
|
||||
- `archive_serial_number`: An optional archive serial number to set.
|
||||
- `custom_fields`: An array of custom field ids to assign (with an empty
|
||||
value) to the document.
|
||||
|
||||
The endpoint will immediately return HTTP 200 if the document consumption
|
||||
process was started successfully, with the UUID of the consumption task
|
||||
@@ -376,6 +378,18 @@ The following methods are supported:
|
||||
- `"merge": true or false` (defaults to false)
|
||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||
removing them) or be merged with existing permissions.
|
||||
- `merge`
|
||||
- No additional `parameters` required.
|
||||
- The ordering of the merged document is determined by the list of IDs.
|
||||
- Optional `parameters`:
|
||||
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
||||
- `split`
|
||||
- Requires `parameters`:
|
||||
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
|
||||
- The split operation only accepts a single document.
|
||||
- `rotate`
|
||||
- Requires `parameters`:
|
||||
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
|
||||
|
||||
### Objects
|
||||
|
||||
|
||||
@@ -1,5 +1,146 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.7.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: select dropdown background colors not visible in light mode [@shamoon](https://github.com/shamoon) ([#6323](https://github.com/paperless-ngx/paperless-ngx/pull/6323))
|
||||
- Fix: spacing in reset and incorrect display in saved views [@shamoon](https://github.com/shamoon) ([#6324](https://github.com/paperless-ngx/paperless-ngx/pull/6324))
|
||||
- Fix: disable invalid create endpoints [@shamoon](https://github.com/shamoon) ([#6320](https://github.com/paperless-ngx/paperless-ngx/pull/6320))
|
||||
- Fix: dont initialize page numbers, allow split with browser pdf viewer [@shamoon](https://github.com/shamoon) ([#6314](https://github.com/paperless-ngx/paperless-ngx/pull/6314))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: select dropdown background colors not visible in light mode [@shamoon](https://github.com/shamoon) ([#6323](https://github.com/paperless-ngx/paperless-ngx/pull/6323))
|
||||
- Fix: spacing in reset and incorrect display in saved views [@shamoon](https://github.com/shamoon) ([#6324](https://github.com/paperless-ngx/paperless-ngx/pull/6324))
|
||||
- Fix: disable invalid create endpoints [@shamoon](https://github.com/shamoon) ([#6320](https://github.com/paperless-ngx/paperless-ngx/pull/6320))
|
||||
- Fix: dont initialize page numbers, allow split with browser pdf viewer [@shamoon](https://github.com/shamoon) ([#6314](https://github.com/paperless-ngx/paperless-ngx/pull/6314))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.7.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Only disable split button if pages = 1 [@shamoon](https://github.com/shamoon) ([#6304](https://github.com/paperless-ngx/paperless-ngx/pull/6304))
|
||||
- Fix: Use correct custom field id when splitting [@shamoon](https://github.com/shamoon) ([#6303](https://github.com/paperless-ngx/paperless-ngx/pull/6303))
|
||||
- Fix: Rotation fails due to celery chord [@stumpylog](https://github.com/stumpylog) ([#6306](https://github.com/paperless-ngx/paperless-ngx/pull/6306))
|
||||
- Fix: split user / group objects error [@shamoon](https://github.com/shamoon) ([#6302](https://github.com/paperless-ngx/paperless-ngx/pull/6302))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: Only disable split button if pages = 1 [@shamoon](https://github.com/shamoon) ([#6304](https://github.com/paperless-ngx/paperless-ngx/pull/6304))
|
||||
- Fix: Use correct custom field id when splitting [@shamoon](https://github.com/shamoon) ([#6303](https://github.com/paperless-ngx/paperless-ngx/pull/6303))
|
||||
- Fix: Rotation fails due to celery chord [@stumpylog](https://github.com/stumpylog) ([#6306](https://github.com/paperless-ngx/paperless-ngx/pull/6306))
|
||||
- Fix: split user / group objects error [@shamoon](https://github.com/shamoon) ([#6302](https://github.com/paperless-ngx/paperless-ngx/pull/6302))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.7.0
|
||||
|
||||
### Notable Changes
|
||||
|
||||
- Feature: PDF actions - merge, split \& rotate @shamoon ([#6094](https://github.com/paperless-ngx/paperless-ngx/pull/6094))
|
||||
- Change: enable auditlog by default, fix import / export @shamoon ([#6267](https://github.com/paperless-ngx/paperless-ngx/pull/6267))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Enhancement: always place search term first in autocomplete results @shamoon ([#6142](https://github.com/paperless-ngx/paperless-ngx/pull/6142))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Standardize subprocess running and logging [@stumpylog](https://github.com/stumpylog) ([#6275](https://github.com/paperless-ngx/paperless-ngx/pull/6275))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Escape the secret key when writing it to the env file [@stumpylog](https://github.com/stumpylog) ([#6243](https://github.com/paperless-ngx/paperless-ngx/pull/6243))
|
||||
- Fix: Hide sidebar labels if group is empty [@shamoon](https://github.com/shamoon) ([#6254](https://github.com/paperless-ngx/paperless-ngx/pull/6254))
|
||||
- Fix: management list clear all should clear header checkbox [@shamoon](https://github.com/shamoon) ([#6253](https://github.com/paperless-ngx/paperless-ngx/pull/6253))
|
||||
- Fix: start-align object names in some UI lists [@shamoon](https://github.com/shamoon) ([#6188](https://github.com/paperless-ngx/paperless-ngx/pull/6188))
|
||||
- Fix: allow scroll long upload files alerts list [@shamoon](https://github.com/shamoon) ([#6184](https://github.com/paperless-ngx/paperless-ngx/pull/6184))
|
||||
- Fix: document_renamer fails with audit_log enabled [@shamoon](https://github.com/shamoon) ([#6175](https://github.com/paperless-ngx/paperless-ngx/pull/6175))
|
||||
- Fix: catch sessionStorage errors for large documents [@shamoon](https://github.com/shamoon) ([#6150](https://github.com/paperless-ngx/paperless-ngx/pull/6150))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>9 changes</summary>
|
||||
|
||||
- Chore(deps): Bump pillow from 10.2.0 to 10.3.0 [@dependabot](https://github.com/dependabot) ([#6268](https://github.com/paperless-ngx/paperless-ngx/pull/6268))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#6276](https://github.com/paperless-ngx/paperless-ngx/pull/6276))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 17 updates [@dependabot](https://github.com/dependabot) ([#6248](https://github.com/paperless-ngx/paperless-ngx/pull/6248))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.42.0 to 1.42.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.42.0 to 1.42.1 in /src-ui @dependabot) ([#6250](https://github.com/paperless-ngx/paperless-ngx/pull/6250))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.11.24 to 20.12.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.11.24 to 20.12.2 in /src-ui @dependabot) ([#6251](https://github.com/paperless-ngx/paperless-ngx/pull/6251))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#6249](https://github.com/paperless-ngx/paperless-ngx/pull/6249))
|
||||
- Chore(deps-dev): Bump express from 4.18.3 to 4.19.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#6207](https://github.com/paperless-ngx/paperless-ngx/pull/6207))
|
||||
- Chore(deps-dev): Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#6161](https://github.com/paperless-ngx/paperless-ngx/pull/6161))
|
||||
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#6131](https://github.com/paperless-ngx/paperless-ngx/pull/6131))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>20 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#6276](https://github.com/paperless-ngx/paperless-ngx/pull/6276))
|
||||
- Chore: Standardize subprocess running and logging [@stumpylog](https://github.com/stumpylog) ([#6275](https://github.com/paperless-ngx/paperless-ngx/pull/6275))
|
||||
- Change: enable auditlog by default, fix import / export [@shamoon](https://github.com/shamoon) ([#6267](https://github.com/paperless-ngx/paperless-ngx/pull/6267))
|
||||
- Fix: Hide sidebar labels if group is empty [@shamoon](https://github.com/shamoon) ([#6254](https://github.com/paperless-ngx/paperless-ngx/pull/6254))
|
||||
- Fix: management list clear all should clear header checkbox [@shamoon](https://github.com/shamoon) ([#6253](https://github.com/paperless-ngx/paperless-ngx/pull/6253))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 17 updates [@dependabot](https://github.com/dependabot) ([#6248](https://github.com/paperless-ngx/paperless-ngx/pull/6248))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.42.0 to 1.42.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.42.0 to 1.42.1 in /src-ui @dependabot) ([#6250](https://github.com/paperless-ngx/paperless-ngx/pull/6250))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.11.24 to 20.12.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.11.24 to 20.12.2 in /src-ui @dependabot) ([#6251](https://github.com/paperless-ngx/paperless-ngx/pull/6251))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#6249](https://github.com/paperless-ngx/paperless-ngx/pull/6249))
|
||||
- Enhancement: support custom fields in post_document endpoint [@shamoon](https://github.com/shamoon) ([#6222](https://github.com/paperless-ngx/paperless-ngx/pull/6222))
|
||||
- Enhancement: add ASN to consume rejection message [@eliasp](https://github.com/eliasp) ([#6217](https://github.com/paperless-ngx/paperless-ngx/pull/6217))
|
||||
- Chore(deps-dev): Bump express from 4.18.3 to 4.19.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#6207](https://github.com/paperless-ngx/paperless-ngx/pull/6207))
|
||||
- Feature: PDF actions - merge, split \& rotate [@shamoon](https://github.com/shamoon) ([#6094](https://github.com/paperless-ngx/paperless-ngx/pull/6094))
|
||||
- Fix: start-align object names in some UI lists [@shamoon](https://github.com/shamoon) ([#6188](https://github.com/paperless-ngx/paperless-ngx/pull/6188))
|
||||
- Fix: allow scroll long upload files alerts list [@shamoon](https://github.com/shamoon) ([#6184](https://github.com/paperless-ngx/paperless-ngx/pull/6184))
|
||||
- Fix: document_renamer fails with audit_log enabled [@shamoon](https://github.com/shamoon) ([#6175](https://github.com/paperless-ngx/paperless-ngx/pull/6175))
|
||||
- Chore(deps-dev): Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#6161](https://github.com/paperless-ngx/paperless-ngx/pull/6161))
|
||||
- Enhancement: always place search term first in autocomplete results [@shamoon](https://github.com/shamoon) ([#6142](https://github.com/paperless-ngx/paperless-ngx/pull/6142))
|
||||
- Fix: catch sessionStorage errors for large documents [@shamoon](https://github.com/shamoon) ([#6150](https://github.com/paperless-ngx/paperless-ngx/pull/6150))
|
||||
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#6131](https://github.com/paperless-ngx/paperless-ngx/pull/6131))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.6.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: allow setting allauth [@shamoon](https://github.com/shamoon) ([#6105](https://github.com/paperless-ngx/paperless-ngx/pull/6105))
|
||||
- Change: dont require empty bulk edit parameters [@shamoon](https://github.com/shamoon) ([#6059](https://github.com/paperless-ngx/paperless-ngx/pull/6059))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump follow-redirects from 1.15.5 to 1.15.6 in /src-ui [@dependabot](https://github.com/dependabot) ([#6120](https://github.com/paperless-ngx/paperless-ngx/pull/6120))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#6079](https://github.com/paperless-ngx/paperless-ngx/pull/6079))
|
||||
- Chore(deps): Bump the django group with 1 update [@dependabot](https://github.com/dependabot) ([#6080](https://github.com/paperless-ngx/paperless-ngx/pull/6080))
|
||||
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#6081](https://github.com/paperless-ngx/paperless-ngx/pull/6081))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>8 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump follow-redirects from 1.15.5 to 1.15.6 in /src-ui [@dependabot](https://github.com/dependabot) ([#6120](https://github.com/paperless-ngx/paperless-ngx/pull/6120))
|
||||
- Fix: allow setting allauth [@shamoon](https://github.com/shamoon) ([#6105](https://github.com/paperless-ngx/paperless-ngx/pull/6105))
|
||||
- Change: remove credentials from redis url in system status [@shamoon](https://github.com/shamoon) ([#6104](https://github.com/paperless-ngx/paperless-ngx/pull/6104))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#6079](https://github.com/paperless-ngx/paperless-ngx/pull/6079))
|
||||
- Chore(deps): Bump the django group with 1 update [@dependabot](https://github.com/dependabot) ([#6080](https://github.com/paperless-ngx/paperless-ngx/pull/6080))
|
||||
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#6081](https://github.com/paperless-ngx/paperless-ngx/pull/6081))
|
||||
- Change: dont require empty bulk edit parameters [@shamoon](https://github.com/shamoon) ([#6059](https://github.com/paperless-ngx/paperless-ngx/pull/6059))
|
||||
- Fix: missing translation string [@DimitriDR](https://github.com/DimitriDR) ([#6054](https://github.com/paperless-ngx/paperless-ngx/pull/6054))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.6.2
|
||||
|
||||
### Features
|
||||
|
||||
@@ -491,8 +491,9 @@ followed by the normalized actual header name.
|
||||
#### [`PAPERLESS_LOGOUT_REDIRECT_URL=<str>`](#PAPERLESS_LOGOUT_REDIRECT_URL) {#PAPERLESS_LOGOUT_REDIRECT_URL}
|
||||
|
||||
: URL to redirect the user to after a logout. This can be used
|
||||
together with PAPERLESS_ENABLE_HTTP_REMOTE_USER to
|
||||
redirect the user back to the SSO application's logout page.
|
||||
together with PAPERLESS_ENABLE_HTTP_REMOTE_USER and SSO to
|
||||
redirect the user back to the SSO application's logout page to
|
||||
complete the logout process.
|
||||
|
||||
Defaults to None, which disables this feature.
|
||||
|
||||
@@ -1311,11 +1312,9 @@ assigns or creates tags if a properly formatted barcode is detected.
|
||||
|
||||
#### [`PAPERLESS_AUDIT_LOG_ENABLED=<bool>`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED}
|
||||
|
||||
: Enables an audit trail for documents, document types, correspondents, and tags. Log entries can be viewed in the Django backend only.
|
||||
: Enables the audit trail for documents, document types, correspondents, and tags.
|
||||
|
||||
!!! warning
|
||||
|
||||
Once enabled cannot be disabled
|
||||
Defaults to true.
|
||||
|
||||
## Collate Double-Sided Documents {#collate}
|
||||
|
||||
|
||||
+17
-1
@@ -241,6 +241,11 @@ permissions can be granted to limit access to certain parts of the UI (and corre
|
||||
|
||||
Superusers can access all parts of the front and backend application as well as any and all objects.
|
||||
|
||||
#### Admin Status
|
||||
|
||||
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
|
||||
as well as accessing the Django backend.
|
||||
|
||||
#### Detailed Explanation of Global Permissions {#global-permissions}
|
||||
|
||||
Global permissions define what areas of the app and API endpoints the user can access. For example, they
|
||||
@@ -249,7 +254,6 @@ still have "object-level" permissions.
|
||||
|
||||
| Type | Details |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Admin | _View_ or higher permissions grants access to the logs view as well as the system status. |
|
||||
| AppConfig | _Change_ or higher permissions grants access to the "Application Configuration" area. |
|
||||
| Correspondent | Grants global permissions to add, edit, delete or view Correspondents. |
|
||||
| CustomField | Grants global permissions to add, edit, delete or view Custom Fields. |
|
||||
@@ -456,6 +460,18 @@ Paperless-ngx added the ability to create shareable links to files in version 2.
|
||||
|
||||
If your paperless-ngx instance is behind a reverse-proxy you may want to create an exception to bypass any authentication layers that are part of your setup in order to make links truly publicly-accessible. Of course, do so with caution.
|
||||
|
||||
## PDF Actions
|
||||
|
||||
Paperless-ngx supports 3 basic editing operations for PDFs (these operations cannot be performed on non-PDF files):
|
||||
|
||||
- Merging documents: available when selecting multiple documents for 'bulk editing'
|
||||
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
|
||||
- Splitting documents: available from an individual document's details page
|
||||
|
||||
!!! important
|
||||
|
||||
Note that rotation alters the Paperless-ngx _original_ file, which would, for example, invalidate a digital signature.
|
||||
|
||||
## Best practices {#basic-searching}
|
||||
|
||||
Paperless offers a couple tools that help you organize your document
|
||||
|
||||
@@ -71,7 +71,17 @@ if ! docker stats --no-stream &> /dev/null ; then
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
default_time_zone=$(timedatectl show -p Timezone --value)
|
||||
# Added handling for timezone for busybox based linux, not having timedatectl available (i.e. QNAP QTS)
|
||||
# if neither timedatectl nor /etc/TZ is succeeding, defaulting to GMT.
|
||||
if command -v timedatectl &> /dev/null ; then
|
||||
default_time_zone=$(timedatectl show -p Timezone --value)
|
||||
elif [ -f /etc/TZ ] && [ -f /etc/tzlist ] ; then
|
||||
TZ=$(cat /etc/TZ)
|
||||
default_time_zone=$(grep -B 1 -m 1 "$TZ" /etc/tzlist | head -1 | cut -f 2 -d =)
|
||||
else
|
||||
echo "WARN: unable to detect timezone, defaulting to Etc/UTC"
|
||||
default_time_zone="Etc/UTC"
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
@@ -335,7 +345,7 @@ read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
|
||||
fi
|
||||
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
||||
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
|
||||
echo "PAPERLESS_SECRET_KEY=$SECRET_KEY"
|
||||
echo "PAPERLESS_SECRET_KEY='$SECRET_KEY'"
|
||||
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${OCR_LANGUAGES_ARRAY[*]} ]] ; then
|
||||
echo "PAPERLESS_OCR_LANGUAGES=${OCR_LANGUAGES_ARRAY[*]}"
|
||||
fi
|
||||
|
||||
+483
-275
File diff suppressed because it is too large
Load Diff
Generated
+1052
-872
File diff suppressed because it is too large
Load Diff
+21
-21
@@ -11,15 +11,15 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^17.2.1",
|
||||
"@angular/common": "~17.2.3",
|
||||
"@angular/compiler": "~17.2.3",
|
||||
"@angular/core": "~17.2.3",
|
||||
"@angular/forms": "~17.2.3",
|
||||
"@angular/localize": "~17.2.3",
|
||||
"@angular/platform-browser": "~17.2.3",
|
||||
"@angular/platform-browser-dynamic": "~17.2.3",
|
||||
"@angular/router": "~17.2.3",
|
||||
"@angular/cdk": "^17.3.2",
|
||||
"@angular/common": "~17.3.2",
|
||||
"@angular/compiler": "~17.3.2",
|
||||
"@angular/core": "~17.3.2",
|
||||
"@angular/forms": "~17.3.2",
|
||||
"@angular/localize": "~17.3.2",
|
||||
"@angular/platform-browser": "~17.3.2",
|
||||
"@angular/platform-browser-dynamic": "~17.3.2",
|
||||
"@angular/router": "~17.3.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@ng-select/ng-select": "^12.0.7",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
@@ -41,19 +41,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "17.0.2",
|
||||
"@angular-devkit/build-angular": "~17.2.2",
|
||||
"@angular-eslint/builder": "17.2.1",
|
||||
"@angular-eslint/eslint-plugin": "17.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "17.2.1",
|
||||
"@angular-eslint/schematics": "17.2.1",
|
||||
"@angular-eslint/template-parser": "17.2.1",
|
||||
"@angular/cli": "~17.2.2",
|
||||
"@angular/compiler-cli": "~17.2.2",
|
||||
"@playwright/test": "^1.42.0",
|
||||
"@angular-devkit/build-angular": "~17.3.2",
|
||||
"@angular-eslint/builder": "17.3.0",
|
||||
"@angular-eslint/eslint-plugin": "17.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "17.3.0",
|
||||
"@angular-eslint/schematics": "17.3.0",
|
||||
"@angular-eslint/template-parser": "17.3.0",
|
||||
"@angular/cli": "~17.3.2",
|
||||
"@angular/compiler-cli": "~17.3.2",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.24",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@types/node": "^20.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -76,7 +76,10 @@ const mock = () => {
|
||||
let storage: { [key: string]: string } = {}
|
||||
return {
|
||||
getItem: (key: string) => (key in storage ? storage[key] : null),
|
||||
setItem: (key: string, value: string) => (storage[key] = value || ''),
|
||||
setItem: (key: string, value: string) => {
|
||||
if (value.length > 1000000) throw new Error('localStorage overflow')
|
||||
storage[key] = value || ''
|
||||
},
|
||||
removeItem: (key: string) => delete storage[key],
|
||||
clear: () => (storage = {}),
|
||||
}
|
||||
|
||||
@@ -141,10 +141,7 @@ export const routes: Routes = [
|
||||
component: LogsComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Admin,
|
||||
},
|
||||
requireAdmin: true,
|
||||
},
|
||||
},
|
||||
// redirect old paths
|
||||
|
||||
@@ -116,9 +116,13 @@ import { ConfirmButtonComponent } from './components/common/confirm-button/confi
|
||||
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
||||
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
|
||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
||||
import {
|
||||
airplane,
|
||||
archive,
|
||||
arrowClockwise,
|
||||
arrowCounterclockwise,
|
||||
arrowDown,
|
||||
arrowLeft,
|
||||
@@ -127,6 +131,7 @@ import {
|
||||
arrowRightShort,
|
||||
arrowUpRight,
|
||||
asterisk,
|
||||
bodyText,
|
||||
boxArrowUp,
|
||||
boxArrowUpRight,
|
||||
boxes,
|
||||
@@ -174,6 +179,7 @@ import {
|
||||
hddStack,
|
||||
house,
|
||||
infoCircle,
|
||||
journals,
|
||||
link,
|
||||
listTask,
|
||||
listUl,
|
||||
@@ -188,6 +194,7 @@ import {
|
||||
plus,
|
||||
plusCircle,
|
||||
questionCircle,
|
||||
scissors,
|
||||
search,
|
||||
slashCircle,
|
||||
sliders2Vertical,
|
||||
@@ -209,6 +216,7 @@ import {
|
||||
const icons = {
|
||||
airplane,
|
||||
archive,
|
||||
arrowClockwise,
|
||||
arrowCounterclockwise,
|
||||
arrowDown,
|
||||
arrowLeft,
|
||||
@@ -217,6 +225,7 @@ const icons = {
|
||||
arrowRightShort,
|
||||
arrowUpRight,
|
||||
asterisk,
|
||||
bodyText,
|
||||
boxArrowUp,
|
||||
boxArrowUpRight,
|
||||
boxes,
|
||||
@@ -264,6 +273,7 @@ const icons = {
|
||||
hddStack,
|
||||
house,
|
||||
infoCircle,
|
||||
journals,
|
||||
link,
|
||||
listTask,
|
||||
listUl,
|
||||
@@ -278,6 +288,7 @@ const icons = {
|
||||
plus,
|
||||
plusCircle,
|
||||
questionCircle,
|
||||
scissors,
|
||||
search,
|
||||
slashCircle,
|
||||
sliders2Vertical,
|
||||
@@ -458,6 +469,9 @@ function initializeApp(settings: SettingsService) {
|
||||
ConfirmButtonComponent,
|
||||
MonetaryComponent,
|
||||
SystemStatusDialogComponent,
|
||||
RotateConfirmDialogComponent,
|
||||
MergeConfirmDialogComponent,
|
||||
SplitConfirmDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -7,29 +7,30 @@
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
||||
<i-bs class="me-1" name="airplane"></i-bs> <ng-container i18n>Start tour</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||
[disabled]="!systemStatus"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
@if (!systemStatus) {
|
||||
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
|
||||
} @else {
|
||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||
@if (systemStatusHasErrors) {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
@if (permissionsService.isAdmin()) {
|
||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||
[disabled]="!systemStatus">
|
||||
@if (!systemStatus) {
|
||||
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
|
||||
} @else {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||
@if (systemStatusHasErrors) {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
} @else {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
<ng-container i18n>System Status</ng-container>
|
||||
</button>
|
||||
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<i-bs name="arrow-up-right"></i-bs>
|
||||
</a>
|
||||
<ng-container i18n>System Status</ng-container>
|
||||
</button>
|
||||
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<i-bs name="arrow-up-right"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</pngx-page-header>
|
||||
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
||||
|
||||
@@ -418,6 +418,7 @@ describe('SettingsComponent', () => {
|
||||
},
|
||||
}
|
||||
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||
jest.spyOn(permissionsService, 'isAdmin').mockReturnValue(true)
|
||||
completeSetup()
|
||||
expect(component['systemStatus']).toEqual(status) // private
|
||||
expect(component.systemStatusHasErrors).toBeTruthy()
|
||||
|
||||
@@ -121,7 +121,7 @@ export class SettingsComponent
|
||||
users: User[]
|
||||
groups: Group[]
|
||||
|
||||
private systemStatus: SystemStatus
|
||||
public systemStatus: SystemStatus
|
||||
|
||||
get systemStatusHasErrors(): boolean {
|
||||
return (
|
||||
@@ -385,12 +385,7 @@ export class SettingsComponent
|
||||
this.settingsForm.patchValue(currentFormValue)
|
||||
}
|
||||
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Admin
|
||||
)
|
||||
) {
|
||||
if (this.permissionsService.isAdmin()) {
|
||||
this.systemStatusService.get().subscribe((status) => {
|
||||
this.systemStatus = status
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" [(ngModel)]="togggleAll" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-tasks"></label>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon
|
||||
import { TasksComponent } from './tasks.component'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
const tasks: PaperlessTask[] = [
|
||||
{
|
||||
@@ -140,6 +141,7 @@ describe('TasksComponent', () => {
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
FormsModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export class TasksComponent
|
||||
{
|
||||
public activeTab: string
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
public expandedTask: number
|
||||
|
||||
public pageSize: number = 25
|
||||
@@ -120,6 +121,7 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.togggleAll = false
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
@for (user of users; track user) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
||||
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
||||
<div class="col">
|
||||
@@ -52,40 +52,38 @@
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
@if (groups.length > 0) {
|
||||
<ul class="list-group">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
@for (group of groups; track group) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
@for (group of groups; track group) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (groups.length === 0) {
|
||||
<li class="list-group-item" i18n>No groups defined</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (groups.length === 0) {
|
||||
<li class="list-group-item" i18n>No groups defined</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (!users || !groups) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<a class="navbar-brand d-flex align-items-center me-0 px-3 py-3 order-sm-0"
|
||||
[ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
|
||||
[ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2 col-xxxl-1' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
|
||||
routerLink="/dashboard"
|
||||
tourAnchor="tour.intro">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" height="1.5em" fill="currentColor">
|
||||
@@ -85,14 +85,14 @@
|
||||
</button>
|
||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item app-link">
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="house"></i-bs><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
@@ -100,9 +100,10 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
|
||||
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
@if (savedViewService.loading || savedViewService.sidebarViews?.length > 0) {
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
|
||||
<h6 class="sidebar-heading px-3 text-muted">
|
||||
<span i18n>Saved views</span>
|
||||
@if (savedViewService.loading) {
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
@@ -111,7 +112,7 @@
|
||||
}
|
||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||
@for (view of savedViewService.sidebarViews; track view) {
|
||||
<li class="nav-item w-100" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||
(cdkDragEnded)="onDragEnd($event)">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}"
|
||||
@@ -128,18 +129,18 @@
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
@if (openDocuments.length > 0) {
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
|
||||
<h6 class="sidebar-heading px-3 text-muted">
|
||||
<span i18n>Open documents</span>
|
||||
</h6>
|
||||
}
|
||||
<ul class="nav flex-column mb-2">
|
||||
@for (d of openDocuments; track d) {
|
||||
<li class="nav-item w-100">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}"
|
||||
<li class="nav-item w-100 app-link">
|
||||
<a class="nav-link app-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}"
|
||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||
popoverClass="popover-slim">
|
||||
@@ -151,8 +152,8 @@
|
||||
</li>
|
||||
}
|
||||
@if (openDocuments.length >= 1) {
|
||||
<li class="nav-item w-100">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
||||
<li class="nav-item w-100 app-link">
|
||||
<a class="nav-link app-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
||||
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="x"></i-bs><span> <ng-container i18n>Close all</ng-container></span>
|
||||
@@ -160,178 +161,184 @@
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
|
||||
<span i18n>Manage</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
||||
tourAnchor="tour.tags">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
|
||||
tourAnchor="tour.workflows">
|
||||
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="boxes"></i-bs><span> <ng-container i18n>Workflows</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
|
||||
tourAnchor="tour.mail">
|
||||
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="envelope"></i-bs><span> <ng-container i18n>Mail</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="nav-group mt-3 mb-1">
|
||||
<h6 class="sidebar-heading px-3 text-muted">
|
||||
<span i18n>Manage</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item app-link"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
||||
tourAnchor="tour.tags">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
|
||||
tourAnchor="tour.workflows">
|
||||
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="boxes"></i-bs><span> <ng-container i18n>Workflows</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
|
||||
tourAnchor="tour.mail">
|
||||
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="envelope"></i-bs><span> <ng-container i18n>Mail</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
|
||||
<span i18n>Administration</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }"
|
||||
tourAnchor="tour.settings">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="gear"></i-bs><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="sliders2-vertical"></i-bs><span> <ng-container i18n>Configuration</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="people"></i-bs><span> <ng-container i18n>Users & Groups</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }"
|
||||
tourAnchor="tour.file-tasks">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="list-task"></i-bs><span> <ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
|
||||
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.failedFileTasks.length}}</span></span>
|
||||
}</span>
|
||||
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
|
||||
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.failedFileTasks.length}}</span>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="text-left"></i-bs><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mt-2" tourAnchor="tour.outro">
|
||||
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none"
|
||||
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1"> <ng-container i18n>Documentation</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||
<div class="px-3 py-0 text-muted small d-flex align-items-center flex-wrap">
|
||||
<div class="me-3">
|
||||
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
|
||||
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
<div class="nav-group mt-auto mb-1">
|
||||
<h6 class="sidebar-heading px-3 pt-4 text-muted">
|
||||
<span i18n>Administration</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }"
|
||||
tourAnchor="tour.settings">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="gear"></i-bs><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="sliders2-vertical"></i-bs><span> <ng-container i18n>Configuration</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="people"></i-bs><span> <ng-container i18n>Users & Groups</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }"
|
||||
tourAnchor="tour.file-tasks">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="list-task"></i-bs><span> <ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
|
||||
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.failedFileTasks.length}}</span></span>
|
||||
}</span>
|
||||
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
|
||||
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.failedFileTasks.length}}</span>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
@if (permissionsService.isAdmin()) {
|
||||
<li class="nav-item app-link">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
{{ versionString }}
|
||||
<i-bs class="me-1" name="text-left"></i-bs><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</div>
|
||||
@if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
|
||||
<div class="version-check">
|
||||
<ng-template #updateAvailablePopContent>
|
||||
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is
|
||||
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
|
||||
</ng-template>
|
||||
<ng-template #updateCheckingNotEnabledPopContent>
|
||||
<p class="small mb-2">
|
||||
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
|
||||
</p>
|
||||
<div class="btn-group btn-group-xs flex-fill w-100">
|
||||
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
|
||||
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
|
||||
</div>
|
||||
<p class="small mb-0 mt-2">
|
||||
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
|
||||
How does this work?
|
||||
</a>
|
||||
</p>
|
||||
</ng-template>
|
||||
@if (settingsService.updateCheckingIsSet) {
|
||||
@if (appRemoteVersion.update_available) {
|
||||
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer"
|
||||
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item mt-2" tourAnchor="tour.outro">
|
||||
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none"
|
||||
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1"> <ng-container i18n>Documentation</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||
<div class="px-3 py-0 text-muted small d-flex align-items-center flex-wrap">
|
||||
<div class="me-3">
|
||||
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
|
||||
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
{{ versionString }}
|
||||
</a>
|
||||
</div>
|
||||
@if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
|
||||
<div class="version-check">
|
||||
<ng-template #updateAvailablePopContent>
|
||||
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is
|
||||
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
|
||||
</ng-template>
|
||||
<ng-template #updateCheckingNotEnabledPopContent>
|
||||
<p class="small mb-2">
|
||||
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
|
||||
</p>
|
||||
<div class="btn-group btn-group-xs flex-fill w-100">
|
||||
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
|
||||
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
|
||||
</div>
|
||||
<p class="small mb-0 mt-2">
|
||||
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
|
||||
How does this work?
|
||||
</a>
|
||||
</p>
|
||||
</ng-template>
|
||||
@if (settingsService.updateCheckingIsSet) {
|
||||
@if (appRemoteVersion.update_available) {
|
||||
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer"
|
||||
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
||||
container="body">
|
||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||
@if (appRemoteVersion?.update_available) {
|
||||
<ng-container i18n>Update available</ng-container>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
} @else {
|
||||
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
||||
container="body">
|
||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||
@if (appRemoteVersion?.update_available) {
|
||||
<ng-container i18n>Update available</ng-container>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
} @else {
|
||||
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
||||
container="body">
|
||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
height: 0.8em;
|
||||
}
|
||||
|
||||
.nav-group:not(:has(.app-link)) .sidebar-heading {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// These come from the col-* classes for non-slim sidebar, needed for animation
|
||||
@media (min-width: 768px) {
|
||||
max-width: 25%;
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{message}}</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="metadataDocumentID" i18n>Documents:</label>
|
||||
<ul class="list-group"
|
||||
cdkDropList
|
||||
(cdkDropListDropped)="onDrop($event)">
|
||||
@for (documentID of documentIDs; track documentID) {
|
||||
<li class="list-group-item" cdkDrag>
|
||||
<i-bs name="grip-vertical" class="me-2"></i-bs>
|
||||
{{getDocument(documentID)?.title}}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<label class="form-label" for="metadataDocumentID" i18n>Use metadata from:</label>
|
||||
<select class="form-select" [(ngModel)]="metadataDocumentID">
|
||||
<option [ngValue]="-1" i18n>Regenerate all metadata</option>
|
||||
@for (document of documents; track document.id) {
|
||||
<option [ngValue]="document.id">{{document.title}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||
{{btnCaption}}
|
||||
</button>
|
||||
</div>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.list-group-item {
|
||||
cursor: move;
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of } from 'rxjs'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
|
||||
describe('MergeConfirmDialogComponent', () => {
|
||||
let component: MergeConfirmDialogComponent
|
||||
let fixture: ComponentFixture<MergeConfirmDialogComponent>
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [MergeConfirmDialogComponent],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MergeConfirmDialogComponent)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should fetch documents on ngOnInit', () => {
|
||||
const documents = [
|
||||
{ id: 1, name: 'Document 1' },
|
||||
{ id: 2, name: 'Document 2' },
|
||||
{ id: 3, name: 'Document 3' },
|
||||
]
|
||||
jest.spyOn(documentService, 'getCachedMany').mockReturnValue(of(documents))
|
||||
|
||||
component.ngOnInit()
|
||||
|
||||
expect(component.documents).toEqual(documents)
|
||||
expect(documentService.getCachedMany).toHaveBeenCalledWith(
|
||||
component.documentIDs
|
||||
)
|
||||
})
|
||||
|
||||
it('should move documentIDs on drop', () => {
|
||||
component.documentIDs = [1, 2, 3]
|
||||
const event = {
|
||||
previousIndex: 1,
|
||||
currentIndex: 2,
|
||||
}
|
||||
|
||||
component.onDrop(event as any)
|
||||
|
||||
expect(component.documentIDs).toEqual([1, 3, 2])
|
||||
})
|
||||
|
||||
it('should get document by ID', () => {
|
||||
const documents = [
|
||||
{ id: 1, name: 'Document 1' },
|
||||
{ id: 2, name: 'Document 2' },
|
||||
{ id: 3, name: 'Document 3' },
|
||||
]
|
||||
jest.spyOn(documentService, 'getCachedMany').mockReturnValue(of(documents))
|
||||
|
||||
component.ngOnInit()
|
||||
|
||||
expect(component.getDocument(2)).toEqual({ id: 2, name: 'Document 2' })
|
||||
})
|
||||
})
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-merge-confirm-dialog',
|
||||
templateUrl: './merge-confirm-dialog.component.html',
|
||||
styleUrl: './merge-confirm-dialog.component.scss',
|
||||
})
|
||||
export class MergeConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
public documentIDs: number[] = []
|
||||
private _documents: Document[] = []
|
||||
get documents(): Document[] {
|
||||
return this._documents
|
||||
}
|
||||
|
||||
public metadataDocumentID: number = -1
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
) {
|
||||
super(activeModal)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.documentService
|
||||
.getCachedMany(this.documentIDs)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((documents) => {
|
||||
this._documents = documents
|
||||
})
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<number[]>) {
|
||||
moveItemInArray(this.documentIDs, event.previousIndex, event.currentIndex)
|
||||
}
|
||||
|
||||
getDocument(documentID: number): Document {
|
||||
return this.documents.find((d) => d.id === documentID)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-2 d-flex justify-content-end">
|
||||
<button class="btn btn-secondary mt-auto" (click)="rotate(false)">
|
||||
<i-bs name="arrow-counterclockwise"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-8 d-flex align-items-center">
|
||||
@if (documentID) {
|
||||
<img class="w-50 m-auto" [ngStyle]="{'transform': 'rotate('+rotation+'deg)'}" [src]="documentService.getThumbUrl(documentID)" />
|
||||
}
|
||||
</div>
|
||||
<div class="col-2 d-flex">
|
||||
<button class="btn btn-secondary mt-auto" (click)="rotate()">
|
||||
<i-bs name="arrow-clockwise"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
@if (messageBold) {
|
||||
<p><b>{{messageBold}}</b></p>
|
||||
}
|
||||
@if (message) {
|
||||
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (showPDFNote) {
|
||||
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be rotated.</p>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled || degrees === 0">
|
||||
{{btnCaption}}
|
||||
@if (!confirmButtonEnabled) {
|
||||
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
img {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('RotateConfirmDialogComponent', () => {
|
||||
let component: RotateConfirmDialogComponent
|
||||
let fixture: ComponentFixture<RotateConfirmDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RotateConfirmDialogComponent, SafeHtmlPipe],
|
||||
providers: [NgbActiveModal, SafeHtmlPipe],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(RotateConfirmDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support rotating the image', () => {
|
||||
component.documentID = 1
|
||||
fixture.detectChanges()
|
||||
component.rotate()
|
||||
fixture.detectChanges()
|
||||
expect(component.degrees).toBe(90)
|
||||
expect(fixture.nativeElement.querySelector('img').style.transform).toBe(
|
||||
'rotate(90deg)'
|
||||
)
|
||||
component.rotate()
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.querySelector('img').style.transform).toBe(
|
||||
'rotate(180deg)'
|
||||
)
|
||||
})
|
||||
|
||||
it('should normalize degrees', () => {
|
||||
expect(component.degrees).toBe(0)
|
||||
component.rotate()
|
||||
expect(component.degrees).toBe(90)
|
||||
component.rotate()
|
||||
expect(component.degrees).toBe(180)
|
||||
component.rotate()
|
||||
expect(component.degrees).toBe(270)
|
||||
component.rotate()
|
||||
expect(component.degrees).toBe(0)
|
||||
component.rotate()
|
||||
expect(component.degrees).toBe(90)
|
||||
component.rotate(false)
|
||||
expect(component.degrees).toBe(0)
|
||||
component.rotate(false)
|
||||
expect(component.degrees).toBe(270)
|
||||
})
|
||||
})
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-rotate-confirm-dialog',
|
||||
templateUrl: './rotate-confirm-dialog.component.html',
|
||||
styleUrl: './rotate-confirm-dialog.component.scss',
|
||||
})
|
||||
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
public documentID: number
|
||||
public showPDFNote: boolean = true
|
||||
|
||||
// animation is better if we dont normalize yet
|
||||
public rotation: number = 0
|
||||
|
||||
public get degrees(): number {
|
||||
let degrees = this.rotation % 360
|
||||
if (degrees < 0) degrees += 360
|
||||
return degrees
|
||||
}
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
public documentService: DocumentService
|
||||
) {
|
||||
super(activeModal)
|
||||
}
|
||||
|
||||
rotate(clockwise: boolean = true) {
|
||||
this.rotation += clockwise ? 90 : -90
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{message}}</p>
|
||||
<div class="row mb-2">
|
||||
<div class="col-6">
|
||||
<div class="input-group input-group-sm">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
|
||||
<div class="input-group-text" i18n>of {{totalPages}}</div>
|
||||
</div>
|
||||
<div class="pdf-viewer-container w-100 mt-3">
|
||||
<pngx-pdf-viewer [src]="pdfSrc" [(page)]="page"
|
||||
[original-size]="false"
|
||||
[zoom]="1"
|
||||
zoom-scale="page-fit"
|
||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
||||
</pngx-pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-sm btn-primary" (click)="addSplit()">
|
||||
<i-bs name="plus-circle"></i-bs>
|
||||
<span i18n>Add Split</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="list-group mt-3">
|
||||
@for (pageStr of pagesString.split(','); track pageStr; let i = $index) {
|
||||
<li class="list-group-item">
|
||||
{{pageStr}}
|
||||
@if (pagesString.split(',').length > 1) {
|
||||
|
||||
<button class="btn btn-sm btn-danger" (click)="removeSplit(i)">
|
||||
<i-bs name="trash"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||
{{btnCaption}}
|
||||
</button>
|
||||
</div>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.pdf-viewer-container {
|
||||
background-color: gray;
|
||||
height: 300px;
|
||||
|
||||
pngx-pdf-viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PdfViewerComponent } from '../../pdf-viewer/pdf-viewer.component'
|
||||
|
||||
describe('SplitConfirmDialogComponent', () => {
|
||||
let component: SplitConfirmDialogComponent
|
||||
let fixture: ComponentFixture<SplitConfirmDialogComponent>
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SplitConfirmDialogComponent, PdfViewerComponent],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(SplitConfirmDialogComponent)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should update pagesString when pages are added', () => {
|
||||
component.totalPages = 5
|
||||
component.page = 2
|
||||
component.addSplit()
|
||||
expect(component.pagesString).toEqual('1-2,3-5')
|
||||
component.page = 4
|
||||
component.addSplit()
|
||||
expect(component.pagesString).toEqual('1-2,3-4,5')
|
||||
})
|
||||
|
||||
it('should update pagesString when pages are removed', () => {
|
||||
component.totalPages = 5
|
||||
component.page = 2
|
||||
component.addSplit()
|
||||
component.page = 4
|
||||
component.addSplit()
|
||||
expect(component.pagesString).toEqual('1-2,3-4,5')
|
||||
component.removeSplit(0)
|
||||
expect(component.pagesString).toEqual('1-4,5')
|
||||
})
|
||||
|
||||
it('should enable confirm button when pages are added', () => {
|
||||
component.totalPages = 5
|
||||
component.page = 2
|
||||
component.addSplit()
|
||||
expect(component.confirmButtonEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should disable confirm button when all pages are removed', () => {
|
||||
component.totalPages = 5
|
||||
component.page = 2
|
||||
component.addSplit()
|
||||
component.removeSplit(0)
|
||||
expect(component.confirmButtonEnabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should not add split if page is the last page', () => {
|
||||
component.totalPages = 5
|
||||
component.page = 5
|
||||
component.addSplit()
|
||||
expect(component.pagesString).toEqual('1-5')
|
||||
})
|
||||
|
||||
it('should update totalPages when pdf is loaded', () => {
|
||||
component.pdfPreviewLoaded({ numPages: 5 } as any)
|
||||
expect(component.totalPages).toEqual(5)
|
||||
})
|
||||
})
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PDFDocumentProxy } from '../../pdf-viewer/typings'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-split-confirm-dialog',
|
||||
templateUrl: './split-confirm-dialog.component.html',
|
||||
styleUrl: './split-confirm-dialog.component.scss',
|
||||
})
|
||||
export class SplitConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
public get pagesString(): string {
|
||||
let pagesStr = ''
|
||||
|
||||
let lastPage = 1
|
||||
for (let i = 1; i <= this.totalPages; i++) {
|
||||
if (this.pages.has(i) || i === this.totalPages) {
|
||||
if (lastPage === i) {
|
||||
pagesStr += `${i},`
|
||||
lastPage = Math.min(i + 1, this.totalPages)
|
||||
} else {
|
||||
pagesStr += `${lastPage}-${i},`
|
||||
lastPage = Math.min(i + 1, this.totalPages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pagesStr.replace(/,$/, '')
|
||||
}
|
||||
|
||||
private pages: Set<number> = new Set()
|
||||
|
||||
public documentID: number
|
||||
public page: number = 1
|
||||
public totalPages: number
|
||||
|
||||
public get pdfSrc(): string {
|
||||
return this.documentService.getPreviewUrl(this.documentID)
|
||||
}
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
) {
|
||||
super(activeModal)
|
||||
this.confirmButtonEnabled = this.pages.size > 0
|
||||
}
|
||||
|
||||
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
||||
this.totalPages = pdf.numPages
|
||||
}
|
||||
|
||||
addSplit() {
|
||||
if (this.page === this.totalPages) return
|
||||
this.pages.add(this.page)
|
||||
this.pages = new Set(Array.from(this.pages).sort())
|
||||
this.confirmButtonEnabled = this.pages.size > 0
|
||||
}
|
||||
|
||||
removeSplit(i: number) {
|
||||
let page = Array.from(this.pages)[Math.min(i, this.pages.size - 1)]
|
||||
this.pages.delete(page)
|
||||
this.confirmButtonEnabled = this.pages.size > 0
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -9,16 +9,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .ng-select .ng-select-container .ng-value-container .ng-placeholder,
|
||||
::ng-deep .ng-select .ng-option,
|
||||
::ng-deep .ng-select .ng-select-container .ng-value-container .ng-value {
|
||||
::ng-deep .custom-fields-dropdown .ng-select .ng-select-container .ng-value-container .ng-placeholder,
|
||||
::ng-deep .custom-fields-dropdown .ng-select .ng-option,
|
||||
::ng-deep .custom-fields-dropdown .ng-select .ng-select-container .ng-value-container .ng-value {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
::ng-deep .paperless-input-select .ng-select {
|
||||
min-height: calc(1em + 0.75rem + 5px);
|
||||
}
|
||||
|
||||
::ng-deep .paperless-input-select .ng-select .ng-select-container .ng-value-container .ng-input {
|
||||
::ng-deep .custom-fields-dropdown .paperless-input-select .ng-select .ng-select-container .ng-value-container .ng-input {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
+5
-1
@@ -16,11 +16,15 @@
|
||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="mb-2 d-flex flex-column">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active">
|
||||
<label class="form-check-label" for="is_active" i18n>Active</label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_staff" formControlName="is_staff">
|
||||
<label class="form-check-label" for="is_staff"><ng-container i18n>Admin</ng-container> <small class="form-text text-muted ms-1" i18n>Access logs, Django backend</small></label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser" (change)="onToggleSuperUser()">
|
||||
<label class="form-check-label" for="is_superuser"><ng-container i18n>Superuser</ng-container> <small class="form-text text-muted ms-1" i18n>(Grants all permissions and can view objects)</small></label>
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ export class UserEditDialogComponent
|
||||
first_name: new FormControl(''),
|
||||
last_name: new FormControl(''),
|
||||
is_active: new FormControl(true),
|
||||
is_staff: new FormControl(true),
|
||||
is_superuser: new FormControl(false),
|
||||
groups: new FormControl([]),
|
||||
user_permissions: new FormControl([]),
|
||||
|
||||
@@ -35,7 +35,8 @@ export class MonetaryComponent extends AbstractInputComponent<string> {
|
||||
|
||||
get currencyCode(): string {
|
||||
const focused = document.activeElement === this.currencyField?.nativeElement
|
||||
if (focused && this.value) return this.value.match(/^([A-Z]{0,3})/)?.[0]
|
||||
if (focused && this.value)
|
||||
return this.value.toUpperCase().match(/^([A-Z]{0,3})/)?.[0]
|
||||
return (
|
||||
this.value
|
||||
?.toString()
|
||||
@@ -45,13 +46,13 @@ export class MonetaryComponent extends AbstractInputComponent<string> {
|
||||
}
|
||||
|
||||
set currencyCode(value: string) {
|
||||
this.value = value + this.monetaryValue?.toString()
|
||||
this.value = value.toUpperCase() + this.monetaryValue?.toString()
|
||||
}
|
||||
|
||||
get monetaryValue(): string {
|
||||
if (!this.value) return null
|
||||
const focused = document.activeElement === this.inputField?.nativeElement
|
||||
const val = parseFloat(this.value.toString().replace(/[^0-9.,]+/g, ''))
|
||||
const val = parseFloat(this.value.toString().replace(/[^0-9.,-]+/g, ''))
|
||||
return focused ? val.toString() : val.toFixed(2)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<input type="file" class="visually-hidden" (change)="onFileSelected($event)" multiple #fileUpload>
|
||||
</form>
|
||||
@if (getStatus().length > 0) {
|
||||
<div class="fixed-bottom p-2 p-md-4 d-flex justify-content-end pe-none" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
|
||||
<div class="fixed-bottom p-2 p-md-4 d-flex justify-content-end pe-none max-vh100-40 overflow-y-scroll" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
|
||||
<div class="col col-lg-4 col-xl-3 ps-0 pe-0 ps-lg-3 pe-lg-0 pe-auto">
|
||||
<div class="card shadow-sm consumer-status-card">
|
||||
<div class="card-body">
|
||||
|
||||
+4
@@ -39,3 +39,7 @@ form {
|
||||
.consumer-status-card {
|
||||
background-color: rgba(var(--bs-body-bg-rgb), .95) !important;
|
||||
}
|
||||
|
||||
.max-vh100-40 {
|
||||
max-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
+2
-2
@@ -149,7 +149,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow dismissing all alerts', fakeAsync(() => {
|
||||
it('should allow dismissing completed alerts', fakeAsync(() => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
component.alertsExpanded = true
|
||||
fixture.detectChanges()
|
||||
@@ -160,7 +160,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
component.dismissCompleted()
|
||||
tick(1000)
|
||||
fixture.detectChanges()
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(10)
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(4)
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
+3
-6
@@ -115,12 +115,9 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
dismissCompleted() {
|
||||
this.alerts.forEach((a) => a.close())
|
||||
if (this.alertsExpanded) {
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
|
||||
public onFileSelected(event: Event) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<pngx-page-header [(title)]="title">
|
||||
@if (contentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
|
||||
<div class="input-group input-group-sm d-none d-md-flex">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||
</div>
|
||||
@if (previewNumPages) {
|
||||
<div class="input-group input-group-sm d-none d-md-flex">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="input-group input-group-sm me-md-5 d-none d-md-flex">
|
||||
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
|
||||
<select class="form-select" (change)="onZoomSelect($event)">
|
||||
@@ -44,11 +46,19 @@
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
|
||||
<button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit">
|
||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs><span class="ps-1" i18n>Redo OCR</span>
|
||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <span i18n>Redo OCR</span>
|
||||
</button>
|
||||
|
||||
<button ngbDropdownItem (click)="moreLike()">
|
||||
<i-bs width="1em" height="1em" name="diagram-3"></i-bs><span class="ps-1" i18n>More like this</span>
|
||||
<i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span>
|
||||
</button>
|
||||
|
||||
<button ngbDropdownItem (click)="splitDocument()" [disabled]="contentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
|
||||
<i-bs width="1em" height="1em" name="scissors"></i-bs> <span i18n>Split</span>
|
||||
</button>
|
||||
|
||||
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || contentRenderType !== ContentRenderType.PDF">
|
||||
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,8 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
|
||||
import { PdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
||||
|
||||
const doc: Document = {
|
||||
id: 3,
|
||||
@@ -171,6 +173,8 @@ describe('DocumentDetailComponent', () => {
|
||||
ShareLinksDropdownComponent,
|
||||
CustomFieldsDropdownComponent,
|
||||
PdfViewerComponent,
|
||||
SplitConfirmDialogComponent,
|
||||
RotateConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
DocumentTitlePipe,
|
||||
@@ -1070,6 +1074,58 @@ describe('DocumentDetailComponent', () => {
|
||||
).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support split', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
initNormally()
|
||||
component.splitDocument()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.documentID = doc.id
|
||||
modal.componentInstance.totalPages = 5
|
||||
modal.componentInstance.page = 2
|
||||
modal.componentInstance.addSplit()
|
||||
modal.componentInstance.confirm()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [doc.id],
|
||||
method: 'split',
|
||||
parameters: { pages: '1-2,3-5' },
|
||||
})
|
||||
req.error(new ProgressEvent('failed'))
|
||||
modal.componentInstance.confirm()
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
})
|
||||
|
||||
it('should support rotate', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
initNormally()
|
||||
component.rotateDocument()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.documentID = doc.id
|
||||
modal.componentInstance.rotate()
|
||||
modal.componentInstance.confirm()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [doc.id],
|
||||
method: 'rotate',
|
||||
parameters: { degrees: 90 },
|
||||
})
|
||||
req.error(new ProgressEvent('failed'))
|
||||
modal.componentInstance.confirm()
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
})
|
||||
|
||||
function initNormally() {
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
|
||||
@@ -67,6 +67,8 @@ import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { PDFDocumentProxy } from '../common/pdf-viewer/typings'
|
||||
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
||||
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
|
||||
enum DocumentDetailNavIDs {
|
||||
Details = 1,
|
||||
@@ -147,7 +149,7 @@ export class DocumentDetailComponent
|
||||
})
|
||||
|
||||
previewCurrentPage: number = 1
|
||||
previewNumPages: number = 1
|
||||
previewNumPages: number
|
||||
previewZoomSetting: ZoomSetting = ZoomSetting.One
|
||||
previewZoomScale: ZoomSetting = ZoomSetting.PageWidth
|
||||
|
||||
@@ -1040,4 +1042,83 @@ export class DocumentDetailComponent
|
||||
this.updateFormForCustomFields(true)
|
||||
this.documentForm.updateValueAndValidity()
|
||||
}
|
||||
|
||||
splitDocument() {
|
||||
let modal = this.modalService.open(SplitConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Split confirm`
|
||||
modal.componentInstance.messageBold = $localize`This operation will split the selected document(s) into new documents.`
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.documentID = this.document.id
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.documentsService
|
||||
.bulkEdit([this.document.id], 'split', {
|
||||
pages: modal.componentInstance.pagesString,
|
||||
})
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Split operation will begin in the background.`
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
error: (error) => {
|
||||
if (modal) {
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
}
|
||||
this.toastService.showError(
|
||||
$localize`Error executing split operation`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
rotateDocument() {
|
||||
let modal = this.modalService.open(RotateConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Rotate confirm`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently rotate the original version of the current document.`
|
||||
modal.componentInstance.message = $localize`This will alter the original copy.`
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.documentID = this.document.id
|
||||
modal.componentInstance.showPDFNote = false
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.documentsService
|
||||
.bulkEdit([this.document.id], 'rotate', {
|
||||
degrees: modal.componentInstance.degrees,
|
||||
})
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.show({
|
||||
content: $localize`Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
|
||||
delay: 8000,
|
||||
action: this.close.bind(this),
|
||||
actionName: $localize`Close`,
|
||||
})
|
||||
modal.close()
|
||||
},
|
||||
error: (error) => {
|
||||
if (modal) {
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
}
|
||||
this.toastService.showError(
|
||||
$localize`Error executing rotate operation`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,18 +80,26 @@
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
|
||||
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline"> <ng-container i18n>Permissions</ng-container></div>
|
||||
</button>
|
||||
</button>
|
||||
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<i-bs name="three-dots"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<i-bs name="three-dots"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll">
|
||||
<i-bs name="body-text"></i-bs> <ng-container i18n>Redo OCR</ng-container>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll">
|
||||
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanEditAll || list.selected.size < 2">
|
||||
<i-bs name="journals"></i-bs> <ng-container i18n>Merge</ng-container>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
|
||||
@@ -113,22 +121,16 @@
|
||||
<div class="form-group ps-3 mb-2">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
|
||||
<label class="form-check-label" for="downloadFileType_archive" i18n>
|
||||
Archived files
|
||||
</label>
|
||||
<label class="form-check-label" for="downloadFileType_archive" i18n>Archived files</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
|
||||
<label class="form-check-label" for="downloadFileType_originals" i18n>
|
||||
Original files
|
||||
</label>
|
||||
<label class="form-check-label" for="downloadFileType_originals" i18n>Original files</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
|
||||
<label class="form-check-label" for="downloadUseFormatting" i18n>
|
||||
Use formatted filename
|
||||
</label>
|
||||
<label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,9 @@ import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||
|
||||
const selectionData: SelectionData = {
|
||||
selected_tags: [
|
||||
@@ -97,6 +100,9 @@ describe('BulkEditorComponent', () => {
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
SwitchComponent,
|
||||
RotateConfirmDialogComponent,
|
||||
IsNumberPipe,
|
||||
MergeConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
PermissionsService,
|
||||
@@ -818,6 +824,79 @@ describe('BulkEditorComponent', () => {
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should support rotate', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
component.rotateSelected()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.rotate()
|
||||
modal.componentInstance.confirm()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'rotate',
|
||||
parameters: { degrees: 90 },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should support merge', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentService, 'getCachedMany')
|
||||
.mockReturnValue(of([{ id: 3 }, { id: 4 }]))
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
component.mergeSelected()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.metadataDocumentID = 3
|
||||
modal.componentInstance.confirm()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'merge',
|
||||
parameters: { metadata_document_id: 3 },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should support bulk download with archive, originals or both and file formatting', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
DocumentService,
|
||||
SelectionDataItem,
|
||||
@@ -39,6 +39,8 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-bulk-editor',
|
||||
@@ -192,12 +194,21 @@ export class BulkEditorComponent
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
private executeBulkOperation(modal, method: string, args) {
|
||||
private executeBulkOperation(
|
||||
modal: NgbModalRef,
|
||||
method: string,
|
||||
args: any,
|
||||
overrideDocumentIDs?: number[]
|
||||
) {
|
||||
if (modal) {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
}
|
||||
this.documentService
|
||||
.bulkEdit(Array.from(this.list.selected), method, args)
|
||||
.bulkEdit(
|
||||
overrideDocumentIDs ?? Array.from(this.list.selected),
|
||||
method,
|
||||
args
|
||||
)
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -641,4 +652,49 @@ export class BulkEditorComponent
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
rotateSelected() {
|
||||
let modal = this.modalService.open(RotateConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent
|
||||
rotateDialog.title = $localize`Rotate confirm`
|
||||
rotateDialog.messageBold = $localize`This operation will permanently rotate the original version of ${this.list.selected.size} document(s).`
|
||||
rotateDialog.message = $localize`This will alter the original copy.`
|
||||
rotateDialog.btnClass = 'btn-danger'
|
||||
rotateDialog.btnCaption = $localize`Proceed`
|
||||
rotateDialog.documentID = Array.from(this.list.selected)[0]
|
||||
rotateDialog.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
rotateDialog.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'rotate', {
|
||||
degrees: rotateDialog.degrees,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
mergeSelected() {
|
||||
let modal = this.modalService.open(MergeConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
const mergeDialog = modal.componentInstance as MergeConfirmDialogComponent
|
||||
mergeDialog.title = $localize`Merge confirm`
|
||||
mergeDialog.messageBold = $localize`This operation will merge ${this.list.selected.size} selected documents into a new document.`
|
||||
mergeDialog.btnCaption = $localize`Proceed`
|
||||
mergeDialog.documentIDs = Array.from(this.list.selected)
|
||||
mergeDialog.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
const args = {}
|
||||
if (mergeDialog.metadataDocumentID > -1) {
|
||||
args['metadata_document_id'] = mergeDialog.metadataDocumentID
|
||||
}
|
||||
mergeDialog.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'merge', args, mergeDialog.documentIDs)
|
||||
this.toastService.showInfo(
|
||||
$localize`Merged document will be queued for consumption.`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
@if (list.selected.size === 0) {
|
||||
<span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>
|
||||
} @if (isFiltered) {
|
||||
<span i18n>(filtered)</span>
|
||||
<span i18n>(filtered)</span>
|
||||
}
|
||||
}
|
||||
@if (!list.isReloading && isFiltered) {
|
||||
|
||||
@@ -86,7 +86,7 @@ export class DocumentListComponent
|
||||
}
|
||||
|
||||
get isFiltered() {
|
||||
return this.list.filterRules?.length > 0
|
||||
return !!this.filterEditor?.rulesModified
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
@for (field of fields; track field) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
@for (account of mailAccounts; track account) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
@@ -70,7 +70,7 @@
|
||||
@for (rule of mailRules; track rule) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-objects"></label>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
@@ -85,6 +85,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
private _nameFilter: string
|
||||
|
||||
public selectedObjects: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reloadData()
|
||||
@@ -268,6 +269,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.togggleAll = false
|
||||
this.selectedObjects.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
@for (workflow of workflows; track workflow.id) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><code>{{workflow.order}}</code></div>
|
||||
<div class="col d-flex align-items-center"><code> @if(workflow.enabled) { <ng-container i18n>Enabled</ng-container> } @else { <span i18n class="text-muted">Disabled</span> }</code></div>
|
||||
<div class="col d-flex align-items-center">{{getTypesList(workflow)}}</div>
|
||||
|
||||
@@ -23,10 +23,12 @@ export class PermissionsGuard {
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree {
|
||||
if (
|
||||
!this.permissionsService.currentUserCan(
|
||||
route.data.requiredPermission.action,
|
||||
route.data.requiredPermission.type
|
||||
)
|
||||
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
||||
(route.data.requiredPermission &&
|
||||
!this.permissionsService.currentUserCan(
|
||||
route.data.requiredPermission.action,
|
||||
route.data.requiredPermission.type
|
||||
))
|
||||
) {
|
||||
// Check if tour is running 1 = TourState.ON
|
||||
if (this.tourService.getStatus() !== 1) {
|
||||
|
||||
@@ -5,10 +5,11 @@ import {
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Subscription, throwError } from 'rxjs'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
|
||||
import { OPEN_DOCUMENT_SERVICE } from '../data/storage-keys'
|
||||
import { wind } from 'ngx-bootstrap-icons'
|
||||
|
||||
const documents = [
|
||||
{
|
||||
@@ -233,4 +234,12 @@ describe('OpenDocumentsService', () => {
|
||||
req.error(new ErrorEvent('timeout'))
|
||||
expect(openDocumentsService.getOpenDocuments()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should log error on sessionStorage save', () => {
|
||||
const doc = { ...documents[0] }
|
||||
doc.content = 'a'.repeat(1000000)
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
openDocumentsService.openDocument(doc)
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -152,9 +152,13 @@ export class OpenDocumentsService {
|
||||
}
|
||||
|
||||
save() {
|
||||
sessionStorage.setItem(
|
||||
OPEN_DOCUMENT_SERVICE.DOCUMENTS,
|
||||
JSON.stringify(this.openDocuments)
|
||||
)
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
OPEN_DOCUMENT_SERVICE.DOCUMENTS,
|
||||
JSON.stringify(this.openDocuments)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('Error saving open documents to session storage', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,4 +418,25 @@ describe('PermissionsService', () => {
|
||||
)
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('correctly checks admin status', () => {
|
||||
permissionsService.initialize([], {
|
||||
username: 'testuser',
|
||||
last_name: 'User',
|
||||
first_name: 'Test',
|
||||
id: 1,
|
||||
is_staff: true,
|
||||
})
|
||||
|
||||
expect(permissionsService.isAdmin()).toBeTruthy()
|
||||
|
||||
permissionsService.initialize([], {
|
||||
username: 'testuser',
|
||||
last_name: 'User',
|
||||
first_name: 'Test',
|
||||
id: 1,
|
||||
})
|
||||
|
||||
expect(permissionsService.isAdmin()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,7 +24,6 @@ export enum PermissionType {
|
||||
MailRule = '%s_mailrule',
|
||||
User = '%s_user',
|
||||
Group = '%s_group',
|
||||
Admin = '%s_logentry',
|
||||
ShareLink = '%s_sharelink',
|
||||
CustomField = '%s_customfield',
|
||||
Workflow = '%s_workflow',
|
||||
@@ -52,6 +51,10 @@ export class PermissionsService {
|
||||
)
|
||||
}
|
||||
|
||||
public isAdmin(): boolean {
|
||||
return this.currentUser?.is_staff
|
||||
}
|
||||
|
||||
public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
|
||||
return (
|
||||
!object ||
|
||||
|
||||
@@ -5,7 +5,7 @@ export const environment = {
|
||||
apiBaseUrl: document.baseURI + 'api/',
|
||||
apiVersion: '5',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: '2.6.3',
|
||||
version: '2.7.2-dev',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user