mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Compare commits
46 Commits
v2.15.0-be
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2e593a0022 | ||
![]() |
3526a4cf23 | ||
![]() |
f4791cac2d | ||
![]() |
fdafd4eefb | ||
![]() |
87a8847a8d | ||
![]() |
7c31c79bbc | ||
![]() |
bf2a9b02c6 | ||
![]() |
348858780c | ||
![]() |
eb481ac1c0 | ||
![]() |
9a2d7a64ac | ||
![]() |
32a7f9cd5a | ||
![]() |
92431b2f4b | ||
![]() |
b4b2a92225 | ||
![]() |
fd45e81a83 | ||
![]() |
97d59dce9c | ||
![]() |
f3479d982c | ||
![]() |
b3ba673d9a | ||
![]() |
68b7427640 | ||
![]() |
9c68100dc0 | ||
![]() |
6e694ad9ff | ||
![]() |
5db511afdf | ||
![]() |
a8de26f88a | ||
![]() |
7a07f1e81d | ||
![]() |
92524ae97a | ||
![]() |
1c89f6da24 | ||
![]() |
d1a3e3b859 | ||
![]() |
79ae594d54 | ||
![]() |
f753f6dc46 | ||
![]() |
97fe5c4176 | ||
![]() |
1f5086164b | ||
![]() |
e60bd3a132 | ||
![]() |
b4047e73bb | ||
![]() |
4263d2196c | ||
![]() |
ac780134fb | ||
![]() |
5d6cfa7349 | ||
![]() |
3105317137 | ||
![]() |
1d9482acc3 | ||
![]() |
1456169d7f | ||
![]() |
22a6fe5e10 | ||
![]() |
caa3c13edd | ||
![]() |
24e863b298 | ||
![]() |
0c9d615f56 | ||
![]() |
90561857e8 | ||
![]() |
fc68f55d1a | ||
![]() |
6a8ec182fa | ||
![]() |
69541546ea |
@ -32,7 +32,7 @@ RUN set -eux \
|
||||
# Purpose: Installs s6-overlay and rootfs
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here either
|
||||
FROM ghcr.io/astral-sh/uv:0.6.5-python3.12-bookworm-slim AS s6-overlay-base
|
||||
FROM ghcr.io/astral-sh/uv:0.6.9-python3.12-bookworm-slim AS s6-overlay-base
|
||||
|
||||
WORKDIR /usr/src/s6
|
||||
|
||||
@ -239,6 +239,7 @@ COPY --from=compile-frontend --chown=1000:1000 /src/src/documents/static/fronten
|
||||
# add users, setup scripts
|
||||
# Mount the compiled frontend to expected location
|
||||
RUN set -eux \
|
||||
&& sed -i '1s|^#!/usr/bin/env python3|#!/command/with-contenv python3|' manage.py \
|
||||
&& echo "Setting up user/group" \
|
||||
&& addgroup --gid 1000 paperless \
|
||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||
|
@ -24,8 +24,8 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
@ -20,7 +20,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@ -22,10 +22,6 @@
|
||||
# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file'
|
||||
# - Modify the environment variables as needed
|
||||
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||
# - Open the list of containers, select paperless_webserver_1
|
||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||
# - Run 'python3 manage.py createsuperuser' to create a user
|
||||
# - Exit the console
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
@ -24,7 +24,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@ -20,7 +20,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@ -24,7 +24,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@ -17,7 +17,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@ -18,9 +18,10 @@ for command in decrypt_documents \
|
||||
document_fuzzy_match \
|
||||
manage_superuser \
|
||||
convert_mariadb_uuid \
|
||||
prune_audit_logs;
|
||||
prune_audit_logs \
|
||||
createsuperuser;
|
||||
do
|
||||
echo "installing $command..."
|
||||
sed "s/management_command/$command/g" management_script.sh >"$PWD/rootfs/usr/local/bin/$command"
|
||||
chmod +x "$PWD/rootfs/usr/local/bin/$command"
|
||||
chmod u=rwx,g=rwx,o=rx "$PWD/rootfs/usr/local/bin/$command"
|
||||
done
|
||||
|
@ -14,7 +14,7 @@ if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
|
||||
fi
|
||||
|
||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||
exec granian --interface asginl --ws "paperless.asgi:application"
|
||||
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
||||
else
|
||||
exec s6-setuidgid paperless granian --interface asginl --ws "paperless.asgi:application"
|
||||
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
||||
fi
|
||||
|
14
docker/rootfs/usr/local/bin/createsuperuser
Executable file
14
docker/rootfs/usr/local/bin/createsuperuser
Executable file
@ -0,0 +1,14 @@
|
||||
#!/command/with-contenv /usr/bin/bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "${PAPERLESS_SRC_DIR}"
|
||||
|
||||
if [[ $(id -u) == 0 ]]; then
|
||||
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
|
||||
elif [[ $(id -un) == "paperless" ]]; then
|
||||
python3 manage.py createsuperuser "$@"
|
||||
else
|
||||
echo "Unknown user."
|
||||
fi
|
@ -565,19 +565,15 @@ document.
|
||||
|
||||
### Managing encryption {#encryption}
|
||||
|
||||
Documents can be stored in Paperless using GnuPG encryption.
|
||||
|
||||
!!! warning
|
||||
|
||||
Encryption is deprecated since [paperless-ng 0.9](changelog.md#paperless-ng-090) and doesn't really
|
||||
provide any additional security, since you have to store the passphrase
|
||||
in a configuration file on the same system as the encrypted documents
|
||||
for paperless to work. Furthermore, the entire text content of the
|
||||
documents is stored plain in the database, even if your documents are
|
||||
encrypted. Filenames are not encrypted as well.
|
||||
|
||||
Also, the web server provides transparent access to your encrypted
|
||||
documents.
|
||||
Encryption was removed in [paperless-ng 0.9](changelog.md#paperless-ng-090)
|
||||
because it did not really provide any additional security, the passphrase
|
||||
was stored in a configuration file on the same system as the documents.
|
||||
Furthermore, the entire text content of the documents is stored plain in
|
||||
the database, even if your documents are encrypted. Filenames are not
|
||||
encrypted as well. Finally, the web server provides transparent access to
|
||||
your encrypted documents.
|
||||
|
||||
Consider running paperless on an encrypted filesystem instead, which
|
||||
will then at least provide security against physical hardware theft.
|
||||
@ -633,3 +629,11 @@ entries created prior to this are not removed. This command allows you to prune
|
||||
```shell
|
||||
prune_audit_logs
|
||||
```
|
||||
|
||||
### Create superuser {#create-superuser}
|
||||
|
||||
If you need to create a superuser, use the following command:
|
||||
|
||||
```shell
|
||||
createsuperuser
|
||||
```
|
||||
|
@ -270,7 +270,7 @@ The following methods are supported:
|
||||
- `remove_tag`
|
||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||
- `modify_tags`
|
||||
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||
- `delete`
|
||||
- No `parameters` required
|
||||
- `reprocess`
|
||||
|
@ -404,7 +404,7 @@ set this value to /paperless. No trailing slash!
|
||||
#### [`PAPERLESS_STATIC_URL=<path>`](#PAPERLESS_STATIC_URL) {#PAPERLESS_STATIC_URL}
|
||||
|
||||
: Override the STATIC_URL here. Unless you're hosting Paperless off a
|
||||
subdomain like /paperless/, you probably don't need to change this.
|
||||
specific path like /paperless/, you probably don't need to change this.
|
||||
If you do change it, be sure to include the trailing slash.
|
||||
|
||||
Defaults to "/static/".
|
||||
|
@ -84,7 +84,7 @@ first-time setup.
|
||||
$ uv run pre-commit install
|
||||
```
|
||||
|
||||
6. Apply migrations and create a superuser for your development instance:
|
||||
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
|
||||
|
||||
```bash
|
||||
# src/
|
||||
|
@ -131,26 +131,11 @@ account. The script essentially automatically performs the steps described in [D
|
||||
by default but you can change the image to pull from Docker Hub by changing the `image`
|
||||
line to `image: paperlessngx/paperless-ngx:latest`.
|
||||
|
||||
6. To be able to login, you will need a "superuser". To create it,
|
||||
execute the following command:
|
||||
6. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||
|
||||
```shell-session
|
||||
docker compose run --rm webserver createsuperuser
|
||||
```
|
||||
|
||||
or using docker exec from within the container:
|
||||
|
||||
```shell-session
|
||||
python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
This will guide you through the superuser setup.
|
||||
|
||||
7. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||
|
||||
8. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
||||
(or similar, depending on your configuration). Use the superuser credentials you have
|
||||
created in the previous step to login.
|
||||
7. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
||||
(or similar, depending on your configuration). When you first access the web interface, you will be
|
||||
prompted to create a superuser account.
|
||||
|
||||
### Build the Docker image yourself {#docker_build}
|
||||
|
||||
@ -386,16 +371,15 @@ are released, dependency support is confirmed, etc.
|
||||
dependencies for Postgres or Mariadb. You can select those extras with `--extra <EXTRA>`
|
||||
or all with `--all-extras`
|
||||
|
||||
9. Go to `/opt/paperless/src`, and execute the following commands:
|
||||
9. Go to `/opt/paperless/src`, and execute the following command:
|
||||
|
||||
```bash
|
||||
# This creates the database schema.
|
||||
sudo -Hu paperless python3 manage.py migrate
|
||||
|
||||
# This creates your first paperless user
|
||||
sudo -Hu paperless python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
When you first access the web interface you will be prompted to create a superuser account.
|
||||
|
||||
10. Optional: Test that paperless is working by executing
|
||||
|
||||
```bash
|
||||
@ -708,7 +692,8 @@ Paperless runs on Raspberry Pi. However, some things are rather slow on
|
||||
the Pi and configuring some options in paperless can help improve
|
||||
performance immensely:
|
||||
|
||||
- Stick with SQLite to save some resources.
|
||||
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
|
||||
if you encounter issues with SQLite locking.
|
||||
- If you do not need the filesystem-based consumer, consider disabling it
|
||||
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
|
||||
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
|
||||
|
@ -292,7 +292,9 @@ many workers attempting to access the database simultaneously.
|
||||
Consider changing to the PostgreSQL database if you will be processing
|
||||
many documents at once often. Otherwise, try tweaking the
|
||||
[`PAPERLESS_DB_TIMEOUT`](configuration.md#PAPERLESS_DB_TIMEOUT) setting to allow more time for the database to
|
||||
unlock. This may have minor performance implications.
|
||||
unlock. Additionally, you can change your SQLite database to use ["Write-Ahead Logging"](https://sqlite.org/wal.html).
|
||||
These changes may have minor performance implications but can help
|
||||
prevent database locking issues.
|
||||
|
||||
## granian fails to start with "is not a valid port number"
|
||||
|
||||
|
@ -23,7 +23,7 @@ dependencies = [
|
||||
"dateparser~=1.2",
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
"django~=5.1.6",
|
||||
"django~=5.1.7",
|
||||
"django-allauth[socialaccount,mfa]~=65.4.0",
|
||||
"django-auditlog~=3.0.0",
|
||||
"django-celery-results~=2.5.1",
|
||||
@ -78,7 +78,7 @@ optional-dependencies.postgres = [
|
||||
"psycopg-c==3.2.5",
|
||||
]
|
||||
optional-dependencies.webserver = [
|
||||
"granian~=2.0.1",
|
||||
"granian[uvloop]~=2.2.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,17 +12,17 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.2.2",
|
||||
"@angular/common": "~19.2.1",
|
||||
"@angular/compiler": "~19.2.1",
|
||||
"@angular/core": "~19.2.1",
|
||||
"@angular/forms": "~19.2.1",
|
||||
"@angular/localize": "~19.2.1",
|
||||
"@angular/platform-browser": "~19.2.1",
|
||||
"@angular/platform-browser-dynamic": "~19.2.1",
|
||||
"@angular/router": "~19.2.1",
|
||||
"@angular/cdk": "^19.2.7",
|
||||
"@angular/common": "~19.2.4",
|
||||
"@angular/compiler": "~19.2.4",
|
||||
"@angular/core": "~19.2.4",
|
||||
"@angular/forms": "~19.2.4",
|
||||
"@angular/localize": "~19.2.4",
|
||||
"@angular/platform-browser": "~19.2.4",
|
||||
"@angular/platform-browser-dynamic": "~19.2.4",
|
||||
"@angular/router": "~19.2.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||
"@ng-select/ng-select": "^14.2.3",
|
||||
"@ng-select/ng-select": "^14.2.6",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
@ -44,28 +44,28 @@
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^19.0.0",
|
||||
"@angular-builders/jest": "^19.0.0",
|
||||
"@angular-devkit/build-angular": "^19.2.1",
|
||||
"@angular-devkit/core": "^19.2.1",
|
||||
"@angular-devkit/schematics": "^19.2.1",
|
||||
"@angular-eslint/builder": "19.2.1",
|
||||
"@angular-eslint/eslint-plugin": "19.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "19.2.1",
|
||||
"@angular-eslint/schematics": "19.2.1",
|
||||
"@angular-eslint/template-parser": "19.2.1",
|
||||
"@angular/cli": "~19.2.1",
|
||||
"@angular/compiler-cli": "~19.2.1",
|
||||
"@angular-devkit/build-angular": "^19.2.5",
|
||||
"@angular-devkit/core": "^19.2.5",
|
||||
"@angular-devkit/schematics": "^19.2.5",
|
||||
"@angular-eslint/builder": "19.3.0",
|
||||
"@angular-eslint/eslint-plugin": "19.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.3.0",
|
||||
"@angular-eslint/schematics": "19.3.0",
|
||||
"@angular-eslint/template-parser": "19.3.0",
|
||||
"@angular/cli": "~19.2.5",
|
||||
"@angular/compiler-cli": "~19.2.4",
|
||||
"@codecov/webpack-plugin": "^1.9.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"@typescript-eslint/utils": "^8.26.1",
|
||||
"eslint": "^9.22.0",
|
||||
"@types/node": "^22.13.17",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"@typescript-eslint/utils": "^8.29.0",
|
||||
"eslint": "^9.23.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "^14.5.3",
|
||||
"jest-preset-angular": "^14.5.4",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
|
2387
src-ui/pnpm-lock.yaml
generated
2387
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@
|
||||
</svg>
|
||||
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
|
||||
@if (customAppTitle?.length) {
|
||||
<div class="d-flex flex-column align-items-start">
|
||||
<div class="d-flex flex-column align-items-start custom-title">
|
||||
<span class="title">{{customAppTitle}}</span>
|
||||
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
||||
</div>
|
||||
|
@ -244,7 +244,7 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (min-width: 366px) and (max-width: 768px) {
|
||||
.navbar-toggler {
|
||||
// compensate for 2 buttons on the right
|
||||
margin-right: 45px;
|
||||
@ -257,6 +257,13 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 345px) {
|
||||
.custom-title {
|
||||
max-width: 110px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .dropdown.show .dropdown-toggle,
|
||||
:host ::ng-deep .dropdown-toggle:hover {
|
||||
opacity: 0.7;
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)">
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions" placement="bottom-end">
|
||||
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||
<i-bs name="ui-radios"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
|
||||
@ -36,6 +37,8 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
|
||||
],
|
||||
})
|
||||
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
@Input()
|
||||
documentId: number
|
||||
|
||||
|
@ -62,6 +62,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
this.emailAddress = ''
|
||||
this.emailSubject = ''
|
||||
this.emailMessage = ''
|
||||
this.close()
|
||||
this.toastService.showInfo($localize`Email sent`)
|
||||
},
|
||||
error: (e) => {
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
@ -44,6 +45,11 @@ const nullItem = {
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
const negativeNullItem = {
|
||||
id: NEGATIVE_NULL_FILTER_VALUE,
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
let selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||
@ -64,6 +70,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
hotkeyService = TestBed.inject(HotKeyService)
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.selectionModel = new FilterableDropdownSelectionModel()
|
||||
selectionModel = new FilterableDropdownSelectionModel()
|
||||
})
|
||||
|
||||
@ -74,7 +81,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(1)
|
||||
@ -96,7 +103,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items selected', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -110,11 +117,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
|
||||
expect(component.items).toEqual([nullItem, ...items])
|
||||
expect(component.selectionModel.items).toEqual([nullItem, ...items])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -124,7 +131,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -139,8 +146,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should exclude items when excluded and not editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
component.excludeClicked(items[0].id)
|
||||
@ -149,8 +156,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should toggle when items excluded and editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
@ -160,8 +167,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should hide count for item if adding will increase size of set', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.hideCount(items[0])).toBeFalsy()
|
||||
selectionModel.logicalOperator = LogicalOperator.Or
|
||||
@ -170,7 +177,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should enforce single select when editing', () => {
|
||||
component.editing = true
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -182,11 +189,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support manyToOne selecting', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
selectionModel.manyToOne = false
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
expect(component.manyToOne).toBeTruthy()
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.selectionModel.manyToOne).toBeTruthy()
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
@ -197,12 +204,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should dynamically enable / disable modifier toggle', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
selectionModel.toggle(null)
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
@ -210,7 +215,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
@ -232,7 +237,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
@ -250,7 +255,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -277,7 +282,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
@ -297,7 +302,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
@ -319,7 +324,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -364,7 +369,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -400,7 +405,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -425,9 +430,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
@ -454,7 +459,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
@ -483,22 +488,53 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
expect(changedResult.getExcludedItems()).toEqual(items)
|
||||
}))
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.items = items.concat([{ id: null, name: 'Null B' }])
|
||||
it('should update null item selection on toggleIntersection', () => {
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.set(null, ToggleableItemState.Selected)
|
||||
component.selectionModel.intersection = Intersection.Exclude
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getExcludedItems()).toEqual([
|
||||
negativeNullItem,
|
||||
])
|
||||
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
|
||||
})
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
|
||||
selectionModel.toggle(items[1].id)
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items.length).toEqual(4)
|
||||
expect(selectionModel.items).toEqual([
|
||||
nullItem,
|
||||
{ id: null, name: 'Null B' },
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
selectionModel.intersection = Intersection.Exclude
|
||||
selectionModel.toggleIntersection()
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items).toEqual([
|
||||
negativeNullItem,
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
// coverage
|
||||
selectionModel.items = selectionModel.items.reverse()
|
||||
selectionModel.apply()
|
||||
})
|
||||
|
||||
it('selection model should sort items by state and document counts = 0, if set', () => {
|
||||
const tagA = { id: 4, name: 'Tag A' }
|
||||
component.items = items.concat([tagA])
|
||||
component.selectionModel.items = items.concat([tagA])
|
||||
component.selectionModel = selectionModel
|
||||
component.documentCounts = [
|
||||
{ id: 1, document_count: 0 }, // Tag1
|
||||
@ -529,7 +565,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
@ -549,7 +585,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.createRef = jest.fn()
|
||||
@ -569,7 +605,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
const id = 1
|
||||
const state = ToggleableItemState.Selected
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel.singleSelect = true
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel['temporarySelectionStates'].set(id, state)
|
||||
@ -596,7 +632,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support shortcut keys', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.shortcutKey = 't'
|
||||
fixture.detectChanges()
|
||||
@ -606,7 +642,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support an extra button and not apply changes when clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.extraButtonTitle = 'Extra'
|
||||
component.selectionModel = selectionModel
|
||||
|
@ -12,6 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, filter, takeUntil } from 'rxjs'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
@ -61,15 +62,56 @@ export class FilterableDropdownSelectionModel {
|
||||
}
|
||||
|
||||
set items(items: MatchingModel[]) {
|
||||
this._items = items
|
||||
this.sortItems()
|
||||
if (items) {
|
||||
this._items = Array.from(items)
|
||||
this.sortItems()
|
||||
this.setNullItem()
|
||||
}
|
||||
}
|
||||
|
||||
private setNullItem() {
|
||||
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
||||
if (this._items[0]?.id === null) {
|
||||
this._items.shift()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const item = {
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id:
|
||||
this.manyToOne || this.intersection === Intersection.Include
|
||||
? null
|
||||
: NEGATIVE_NULL_FILTER_VALUE,
|
||||
}
|
||||
|
||||
if (
|
||||
this._items[0]?.id === null ||
|
||||
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
||||
) {
|
||||
this._items[0] = item
|
||||
} else if (this._items) {
|
||||
this._items.unshift(item)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(manyToOne: boolean = false) {
|
||||
this.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
private sortItems() {
|
||||
this._items.sort((a, b) => {
|
||||
if (a.id == null && b.id != null) {
|
||||
if (
|
||||
(a.id == null && b.id != null) ||
|
||||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id != NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return -1
|
||||
} else if (a.id != null && b.id == null) {
|
||||
} else if (
|
||||
(a.id != null && b.id == null) ||
|
||||
(a.id != NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id == NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return 1
|
||||
} else if (
|
||||
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||
@ -230,6 +272,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set logicalOperator(operator: LogicalOperator) {
|
||||
this.temporaryLogicalOperator = operator
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleOperator() {
|
||||
@ -242,6 +285,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set intersection(intersection: Intersection) {
|
||||
this.temporaryIntersection = intersection
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleIntersection() {
|
||||
@ -250,9 +294,20 @@ export class FilterableDropdownSelectionModel {
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
|
||||
this.temporarySelectionStates.forEach((state, key) => {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
if (key === null && this.intersection === Intersection.Exclude) {
|
||||
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
||||
} else if (
|
||||
key === NEGATIVE_NULL_FILTER_VALUE &&
|
||||
this.intersection === Intersection.Include
|
||||
) {
|
||||
this.temporarySelectionStates.set(null, newState)
|
||||
} else {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
}
|
||||
})
|
||||
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
@ -274,6 +329,7 @@ export class FilterableDropdownSelectionModel {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||
this.setNullItem()
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
@ -305,8 +361,10 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
isNoneSelected() {
|
||||
return (
|
||||
this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected
|
||||
(this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected) ||
|
||||
(this.intersection == Intersection.Exclude &&
|
||||
this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
|
||||
)
|
||||
}
|
||||
|
||||
@ -384,25 +442,13 @@ export class FilterableDropdownComponent
|
||||
|
||||
filterText: string
|
||||
|
||||
@Input()
|
||||
set items(items: MatchingModel[]) {
|
||||
if (items) {
|
||||
this._selectionModel.items = Array.from(items)
|
||||
this._selectionModel.items.unshift({
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
_selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
get items(): MatchingModel[] {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel: FilterableDropdownSelectionModel =
|
||||
new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
@ -423,11 +469,6 @@ export class FilterableDropdownComponent
|
||||
@Output()
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set manyToOne(manyToOne: boolean) {
|
||||
this.selectionModel.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
get manyToOne() {
|
||||
return this.selectionModel.manyToOne
|
||||
}
|
||||
@ -484,7 +525,7 @@ export class FilterableDropdownComponent
|
||||
return this.manyToOne
|
||||
? this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
: !this.selectionModel.isNoneSelected()
|
||||
: true
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
|
@ -30,25 +30,24 @@
|
||||
[placeholder]="placeholder"
|
||||
[notFoundText]="notFoundText"
|
||||
[multiple]="true"
|
||||
bindValue="id"
|
||||
[compareWith]="compareDocuments"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="loading"
|
||||
[typeahead]="documentsInput$"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
(change)="onChange(selectedDocuments)">
|
||||
(change)="onChange(selectedDocumentIDs)">
|
||||
<ng-template ng-label-tmp let-document="item">
|
||||
<div class="d-flex align-items-center">
|
||||
@if (!disabled) {
|
||||
<button class="btn p-0 lh-1" (click)="unselect(document)" title="Remove link" i18n-title><i-bs name="x"></i-bs></button>
|
||||
<button class="btn p-0 lh-1" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title><i-bs name="x"></i-bs></button>
|
||||
}
|
||||
@if (document.title) {
|
||||
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();" title="Open link" i18n-title>
|
||||
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{document.title}}</span>
|
||||
</a>
|
||||
} @else {
|
||||
<span class="badge bg-light text-muted" (click)="unselect(document)" title="Remove link" i18n-title>
|
||||
<span class="badge bg-light text-muted" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title>
|
||||
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle-fill"></i-bs> <span i18n>Not found</span>
|
||||
</span>
|
||||
}
|
||||
|
@ -74,6 +74,11 @@ describe('DocumentLinkComponent', () => {
|
||||
expect(component.selectedDocuments).toEqual([documents[1], documents[0]])
|
||||
})
|
||||
|
||||
it('should retrieve document IDs from selected documents', () => {
|
||||
component.selectedDocuments = documents
|
||||
expect(component.selectedDocumentIDs).toEqual([1, 12, 16, 23])
|
||||
})
|
||||
|
||||
it('should search API on select text input', () => {
|
||||
const listSpy = jest.spyOn(documentService, 'listFiltered')
|
||||
listSpy.mockImplementation(
|
||||
|
@ -71,6 +71,10 @@ export class DocumentLinkComponent
|
||||
@Input()
|
||||
placeholder: string = $localize`Search for documents`
|
||||
|
||||
get selectedDocumentIDs(): number[] {
|
||||
return this.selectedDocuments.map((d) => d.id)
|
||||
}
|
||||
|
||||
constructor(private documentsService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
(change)="onChange(value)">
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
|
||||
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag(item.id)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove tag" i18n-title>
|
||||
<i-bs name="x" style="margin-inline-end: 1px;"></i-bs>
|
||||
@if (item.id && tags) {
|
||||
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
|
||||
|
@ -154,11 +154,11 @@ describe('TagsComponent', () => {
|
||||
it('support remove tags', () => {
|
||||
component.tags = tags
|
||||
component.value = [1, 2]
|
||||
component.removeTag(new PointerEvent('point'), 2)
|
||||
component.removeTag(2)
|
||||
expect(component.value).toEqual([1])
|
||||
|
||||
component.disabled = true
|
||||
component.removeTag(new PointerEvent('point'), 1)
|
||||
component.removeTag(1)
|
||||
expect(component.value).toEqual([1])
|
||||
})
|
||||
|
||||
|
@ -118,13 +118,10 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(event: PointerEvent, id: number) {
|
||||
removeTag(tagID: number) {
|
||||
if (this.disabled) return
|
||||
|
||||
// prevent opening dropdown
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
let index = this.value.indexOf(id)
|
||||
let index = this.value.indexOf(tagID)
|
||||
if (index > -1) {
|
||||
let oldValue = this.value
|
||||
oldValue.splice(index, 1)
|
||||
|
@ -824,11 +824,18 @@ export class DocumentDetailComponent
|
||||
},
|
||||
error: (error) => {
|
||||
this.networkActive = false
|
||||
if (!this.userCanEdit) {
|
||||
const canEdit =
|
||||
this.permissionsService.currentUserHasObjectPermissions(
|
||||
PermissionAction.Change,
|
||||
this.document
|
||||
)
|
||||
if (!canEdit) {
|
||||
// document was 'given away'
|
||||
this.openDocumentService.setDirty(this.document, false)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${this.document.title}" saved successfully.`
|
||||
)
|
||||
close && this.close()
|
||||
this.close()
|
||||
} else {
|
||||
this.error = error.error
|
||||
this.toastService.showError(
|
||||
@ -1279,9 +1286,7 @@ export class DocumentDetailComponent
|
||||
this.document.custom_fields?.forEach((fieldInstance) => {
|
||||
this.customFieldFormFields.push(
|
||||
new FormGroup({
|
||||
field: new FormControl(
|
||||
this.getCustomFieldFromInstance(fieldInstance)?.id
|
||||
),
|
||||
field: new FormControl(fieldInstance.field),
|
||||
value: new FormControl(fieldInstance.value),
|
||||
}),
|
||||
{ emitEvent }
|
||||
|
@ -20,10 +20,8 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createTag.bind(this)"
|
||||
(opened)="openTagsDropdown()"
|
||||
@ -36,7 +34,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@ -51,7 +48,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@ -66,7 +62,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@ -81,10 +76,8 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
|
||||
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
|
||||
[items]="customFields"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createCustomField.bind(this)"
|
||||
(opened)="openCustomFieldsDropdown()"
|
||||
|
@ -1150,10 +1150,10 @@ describe('BulkEditorComponent', () => {
|
||||
|
||||
it('should not attempt to retrieve objects if user does not have permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.tags).toBeUndefined()
|
||||
expect(component.correspondents).toBeUndefined()
|
||||
expect(component.documentTypes).toBeUndefined()
|
||||
expect(component.storagePaths).toBeUndefined()
|
||||
expect(component.tagSelectionModel.items.length).toEqual(0)
|
||||
expect(component.correspondentSelectionModel.items.length).toEqual(0)
|
||||
expect(component.documentTypeSelectionModel.items.length).toEqual(0)
|
||||
expect(component.storagePathsSelectionModel.items.length).toEqual(0)
|
||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/correspondents/`
|
||||
@ -1204,7 +1204,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(tagListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
|
||||
expect(component.tags).toEqual(tags.results)
|
||||
expect(component.tagSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(tags.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new correspondent', () => {
|
||||
@ -1251,7 +1253,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCorrespondent.id
|
||||
)
|
||||
expect(component.correspondents).toEqual(correspondents.results)
|
||||
expect(component.correspondentSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(correspondents.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new document type', () => {
|
||||
@ -1295,7 +1299,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newDocumentType.id
|
||||
)
|
||||
expect(component.documentTypes).toEqual(documentTypes.results)
|
||||
expect(component.documentTypeSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(documentTypes.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new storage path', () => {
|
||||
@ -1339,7 +1345,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newStoragePath.id
|
||||
)
|
||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||
expect(component.storagePathsSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(storagePaths.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new custom field', () => {
|
||||
@ -1391,7 +1399,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCustomField.id
|
||||
)
|
||||
expect(component.customFields).toEqual(customFields.results)
|
||||
expect(component.customFieldsSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(customFields.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the bulk edit custom field values dialog with correct parameters', () => {
|
||||
@ -1416,17 +1426,17 @@ describe('BulkEditorComponent', () => {
|
||||
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||
|
||||
component.customFields = [
|
||||
component.customFieldsSelectionModel.items = [
|
||||
{ id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String },
|
||||
{ id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String },
|
||||
]
|
||||
] as any
|
||||
|
||||
component.setCustomFieldValues({
|
||||
itemsToAdd: [{ id: 1 }, { id: 2 }],
|
||||
itemsToRemove: [1],
|
||||
} as any)
|
||||
|
||||
expect(modal.componentInstance.customFields).toEqual(component.customFields)
|
||||
expect(modal.componentInstance.customFields.length).toEqual(2)
|
||||
expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2])
|
||||
expect(modal.componentInstance.documents).toEqual([3, 4])
|
||||
|
||||
|
@ -14,12 +14,8 @@ import { saveAs } from 'file-saver'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
@ -75,17 +71,11 @@ export class BulkEditorComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tags: Tag[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
customFields: CustomField[]
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
correspondentDocumentCounts: SelectionDataItem[]
|
||||
documentTypeDocumentCounts: SelectionDataItem[]
|
||||
@ -176,7 +166,7 @@ export class BulkEditorComponent
|
||||
this.tagService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.tags = result.results))
|
||||
.subscribe((result) => (this.tagSelectionModel.items = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -187,7 +177,9 @@ export class BulkEditorComponent
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.correspondentSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -198,7 +190,9 @@ export class BulkEditorComponent
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.documentTypeSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -209,7 +203,9 @@ export class BulkEditorComponent
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.storagePathsSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -220,7 +216,9 @@ export class BulkEditorComponent
|
||||
this.customFieldService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.customFieldsSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
|
||||
this.downloadForm
|
||||
@ -651,7 +649,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newTag, tags }) => {
|
||||
this.tags = tags.results
|
||||
this.tagSelectionModel.items = tags.results
|
||||
this.tagSelectionModel.toggle(newTag.id)
|
||||
})
|
||||
}
|
||||
@ -674,7 +672,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCorrespondent, correspondents }) => {
|
||||
this.correspondents = correspondents.results
|
||||
this.correspondentSelectionModel.items = correspondents.results
|
||||
this.correspondentSelectionModel.toggle(newCorrespondent.id)
|
||||
})
|
||||
}
|
||||
@ -695,7 +693,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newDocumentType, documentTypes }) => {
|
||||
this.documentTypes = documentTypes.results
|
||||
this.documentTypeSelectionModel.items = documentTypes.results
|
||||
this.documentTypeSelectionModel.toggle(newDocumentType.id)
|
||||
})
|
||||
}
|
||||
@ -716,7 +714,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newStoragePath, storagePaths }) => {
|
||||
this.storagePaths = storagePaths.results
|
||||
this.storagePathsSelectionModel.items = storagePaths.results
|
||||
this.storagePathsSelectionModel.toggle(newStoragePath.id)
|
||||
})
|
||||
}
|
||||
@ -737,7 +735,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCustomField, customFields }) => {
|
||||
this.customFields = customFields.results
|
||||
this.customFieldsSelectionModel.items = customFields.results
|
||||
this.customFieldsSelectionModel.toggle(newCustomField.id)
|
||||
})
|
||||
}
|
||||
@ -875,7 +873,9 @@ export class BulkEditorComponent
|
||||
})
|
||||
const dialog =
|
||||
modal.componentInstance as CustomFieldsBulkEditDialogComponent
|
||||
dialog.customFields = this.customFields
|
||||
dialog.customFields = (
|
||||
this.customFieldsSelectionModel.items as CustomField[]
|
||||
).filter((f) => f.id !== null)
|
||||
dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map(
|
||||
(item) => item.id
|
||||
)
|
||||
|
@ -35,11 +35,9 @@
|
||||
<div class="col-auto">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[manyToOne]="true"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onTagsDropdownOpen()"
|
||||
@ -48,10 +46,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="t"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onCorrespondentDropdownOpen()"
|
||||
@ -60,10 +57,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="y"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onDocumentTypeDropdownOpen()"
|
||||
@ -72,10 +68,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="u"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[(selectionModel)]="storagePathSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onStoragePathDropdownOpen()"
|
||||
|
@ -69,6 +69,7 @@ import {
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_TITLE,
|
||||
FILTER_TITLE_CONTENT,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
@ -671,9 +672,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '12',
|
||||
},
|
||||
]
|
||||
expect(component.correspondentSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.correspondentSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@ -681,6 +679,19 @@ describe('FilterEditorComponent', () => {
|
||||
correspondents[0],
|
||||
])
|
||||
component.toggleCorrespondent(12) // coverage
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
]
|
||||
expect(component.correspondentSelectionModel.intersection).toEqual(
|
||||
Intersection.Exclude
|
||||
)
|
||||
expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
|
||||
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
|
||||
])
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
|
||||
@ -754,9 +765,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '22',
|
||||
},
|
||||
]
|
||||
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.documentTypeSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@ -764,6 +772,19 @@ describe('FilterEditorComponent', () => {
|
||||
document_types[0],
|
||||
])
|
||||
component.toggleDocumentType(22) // coverage
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
]
|
||||
expect(component.documentTypeSelectionModel.intersection).toEqual(
|
||||
Intersection.Exclude
|
||||
)
|
||||
expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
|
||||
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
|
||||
])
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any of document types', fakeAsync(() => {
|
||||
@ -780,9 +801,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '23',
|
||||
},
|
||||
]
|
||||
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.documentTypeSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@ -837,9 +855,6 @@ describe('FilterEditorComponent', () => {
|
||||
value: '32',
|
||||
},
|
||||
]
|
||||
expect(component.storagePathSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.storagePathSelectionModel.intersection).toEqual(
|
||||
Intersection.Include
|
||||
)
|
||||
@ -847,6 +862,19 @@ describe('FilterEditorComponent', () => {
|
||||
storage_paths[0],
|
||||
])
|
||||
component.toggleStoragePath(32) // coverage
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
]
|
||||
expect(component.storagePathSelectionModel.intersection).toEqual(
|
||||
Intersection.Exclude
|
||||
)
|
||||
expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
|
||||
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
|
||||
])
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
|
||||
@ -1398,6 +1426,19 @@ describe('FilterEditorComponent', () => {
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
|
||||
const excludeButton = correspondentsFilterableDropdown.queryAll(
|
||||
By.css('input[value=exclude]')
|
||||
)[0]
|
||||
excludeButton.nativeElement.checked = true
|
||||
excludeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
|
||||
@ -1455,6 +1496,19 @@ describe('FilterEditorComponent', () => {
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
|
||||
const excludeButton = docTypesFilterableDropdown.queryAll(
|
||||
By.css('input[value=exclude]')
|
||||
)[0]
|
||||
excludeButton.nativeElement.checked = true
|
||||
excludeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
|
||||
@ -1512,6 +1566,19 @@ describe('FilterEditorComponent', () => {
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
|
||||
const excludeButton = storagePathsFilterableDropdown.queryAll(
|
||||
By.css('input[value=exclude]')
|
||||
)[0]
|
||||
excludeButton.nativeElement.checked = true
|
||||
excludeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
|
||||
|
@ -26,14 +26,12 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from 'rxjs/operators'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import {
|
||||
CustomFieldQueryLogicalOperator,
|
||||
CustomFieldQueryOperator,
|
||||
} from 'src/app/data/custom-field-query'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import {
|
||||
FILTER_ADDED_AFTER,
|
||||
@ -75,9 +73,8 @@ import {
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_TITLE,
|
||||
FILTER_TITLE_CONTENT,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
@ -251,7 +248,9 @@ export class FilterEditorComponent
|
||||
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Correspondent: ${
|
||||
this.correspondents.find((c) => c.id == +rule.value)?.name
|
||||
this.correspondentSelectionModel.items.find(
|
||||
(c) => c.id == +rule.value
|
||||
)?.name
|
||||
}`
|
||||
} else {
|
||||
return $localize`Without correspondent`
|
||||
@ -261,7 +260,9 @@ export class FilterEditorComponent
|
||||
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Document type: ${
|
||||
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
|
||||
this.documentTypeSelectionModel.items.find(
|
||||
(dt) => dt.id == +rule.value
|
||||
)?.name
|
||||
}`
|
||||
} else {
|
||||
return $localize`Without document type`
|
||||
@ -271,7 +272,9 @@ export class FilterEditorComponent
|
||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Storage path: ${
|
||||
this.storagePaths.find((sp) => sp.id == +rule.value)?.name
|
||||
this.storagePathSelectionModel.items.find(
|
||||
(sp) => sp.id == +rule.value
|
||||
)?.name
|
||||
}`
|
||||
} else {
|
||||
return $localize`Without storage path`
|
||||
@ -279,7 +282,7 @@ export class FilterEditorComponent
|
||||
|
||||
case FILTER_HAS_TAGS_ALL:
|
||||
return $localize`Tag: ${
|
||||
this.tags.find((t) => t.id == +rule.value)?.name
|
||||
this.tagSelectionModel.items.find((t) => t.id == +rule.value)?.name
|
||||
}`
|
||||
|
||||
case FILTER_HAS_ANY_TAG:
|
||||
@ -326,10 +329,6 @@ export class FilterEditorComponent
|
||||
@ViewChild('textFilterInput')
|
||||
textFilterInput: ElementRef
|
||||
|
||||
tags: Tag[] = []
|
||||
correspondents: Correspondent[] = []
|
||||
documentTypes: DocumentType[] = []
|
||||
storagePaths: StoragePath[] = []
|
||||
customFields: CustomField[] = []
|
||||
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
@ -370,7 +369,7 @@ export class FilterEditorComponent
|
||||
)
|
||||
}
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||
@ -551,6 +550,19 @@ export class FilterEditorComponent
|
||||
)
|
||||
break
|
||||
case FILTER_CORRESPONDENT:
|
||||
this.correspondentSelectionModel.intersection =
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
? Intersection.Exclude
|
||||
: Intersection.Include
|
||||
this.correspondentSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
this.correspondentSelectionModel.intersection ==
|
||||
Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.correspondentSelectionModel.intersection = Intersection.Include
|
||||
@ -569,6 +581,18 @@ export class FilterEditorComponent
|
||||
)
|
||||
break
|
||||
case FILTER_DOCUMENT_TYPE:
|
||||
this.documentTypeSelectionModel.intersection =
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
? Intersection.Exclude
|
||||
: Intersection.Include
|
||||
this.documentTypeSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
this.documentTypeSelectionModel.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.documentTypeSelectionModel.intersection = Intersection.Include
|
||||
@ -587,6 +611,18 @@ export class FilterEditorComponent
|
||||
)
|
||||
break
|
||||
case FILTER_STORAGE_PATH:
|
||||
this.storagePathSelectionModel.intersection =
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
? Intersection.Exclude
|
||||
: Intersection.Include
|
||||
this.storagePathSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
this.storagePathSelectionModel.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.storagePathSelectionModel.intersection = Intersection.Include
|
||||
@ -809,9 +845,21 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.correspondentSelectionModel.isNoneSelected()) {
|
||||
if (
|
||||
this.correspondentSelectionModel.isNoneSelected() &&
|
||||
this.correspondentSelectionModel.intersection == Intersection.Include
|
||||
) {
|
||||
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
|
||||
} else {
|
||||
if (
|
||||
this.correspondentSelectionModel.isNoneSelected() &&
|
||||
this.correspondentSelectionModel.intersection == Intersection.Exclude
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
})
|
||||
}
|
||||
this.correspondentSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((correspondent) => {
|
||||
@ -822,6 +870,7 @@ export class FilterEditorComponent
|
||||
})
|
||||
this.correspondentSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((correspondent) => correspondent.id > 0)
|
||||
.forEach((correspondent) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||
@ -829,9 +878,21 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.documentTypeSelectionModel.isNoneSelected()) {
|
||||
if (
|
||||
this.documentTypeSelectionModel.isNoneSelected() &&
|
||||
this.documentTypeSelectionModel.intersection === Intersection.Include
|
||||
) {
|
||||
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
|
||||
} else {
|
||||
if (
|
||||
this.documentTypeSelectionModel.isNoneSelected() &&
|
||||
this.documentTypeSelectionModel.intersection == Intersection.Exclude
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
})
|
||||
}
|
||||
this.documentTypeSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((documentType) => {
|
||||
@ -842,6 +903,7 @@ export class FilterEditorComponent
|
||||
})
|
||||
this.documentTypeSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((documentType) => documentType.id > 0)
|
||||
.forEach((documentType) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||
@ -849,9 +911,21 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.storagePathSelectionModel.isNoneSelected()) {
|
||||
if (
|
||||
this.storagePathSelectionModel.isNoneSelected() &&
|
||||
this.storagePathSelectionModel.intersection == Intersection.Include
|
||||
) {
|
||||
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
||||
} else {
|
||||
if (
|
||||
this.storagePathSelectionModel.isNoneSelected() &&
|
||||
this.storagePathSelectionModel.intersection == Intersection.Exclude
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
})
|
||||
}
|
||||
this.storagePathSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((storagePath) => {
|
||||
@ -862,6 +936,7 @@ export class FilterEditorComponent
|
||||
})
|
||||
this.storagePathSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((storagePath) => storagePath.id > 0)
|
||||
.forEach((storagePath) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||
@ -1062,7 +1137,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.tagService.listAll().subscribe((result) => {
|
||||
this.tags = result.results
|
||||
this.tagSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
@ -1074,7 +1149,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.correspondentService.listAll().subscribe((result) => {
|
||||
this.correspondents = result.results
|
||||
this.correspondentSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
@ -1086,7 +1161,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.documentTypeService.listAll().subscribe((result) => {
|
||||
this.documentTypes = result.results
|
||||
this.documentTypeSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
@ -1098,7 +1173,7 @@ export class FilterEditorComponent
|
||||
) {
|
||||
this.loadingCountTotal++
|
||||
this.storagePathService.listAll().subscribe((result) => {
|
||||
this.storagePaths = result.results
|
||||
this.storagePathSelectionModel.items = result.results
|
||||
this.maybeCompleteLoading()
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { DataType } from './datatype'
|
||||
|
||||
export const NEGATIVE_NULL_FILTER_VALUE = -1
|
||||
|
||||
// These correspond to src/documents/models.py and changes here require a DB migration (and vice versa)
|
||||
export const FILTER_TITLE = 0
|
||||
export const FILTER_CONTENT = 1
|
||||
|
@ -50,7 +50,7 @@ describe('ObjectNamePipe', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should return empty string if object not found', (done) => {
|
||||
it('should return Private string if object not found', (done) => {
|
||||
const mockObjects = {
|
||||
results: [{ id: 2, name: 'Object 2' }],
|
||||
count: 1,
|
||||
@ -60,7 +60,7 @@ describe('ObjectNamePipe', () => {
|
||||
jest.spyOn(objectService, 'listAll').mockReturnValue(of(mockObjects))
|
||||
|
||||
pipe.transform(1).subscribe((result) => {
|
||||
expect(result).toBe('')
|
||||
expect(result).toBe('Private')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
@ -35,7 +35,10 @@ export abstract class ObjectNamePipe implements PipeTransform {
|
||||
return this.objectService.listAll().pipe(
|
||||
map((objects) => {
|
||||
this.objects = objects.results
|
||||
return this.objects.find((o) => o.id === obejctId)?.name || ''
|
||||
return (
|
||||
this.objects.find((o) => o.id === obejctId)?.name ||
|
||||
$localize`Private`
|
||||
)
|
||||
}),
|
||||
catchError(() => of(''))
|
||||
)
|
||||
|
@ -156,6 +156,72 @@ describe(`Additional service tests for SavedViewService`, () => {
|
||||
httpTestingController.verify() // no reload
|
||||
})
|
||||
|
||||
it('should reload after create, delete, patch and patchMany', () => {
|
||||
const reloadSpy = jest.spyOn(service, 'reload')
|
||||
service
|
||||
.create({
|
||||
name: 'New Saved View',
|
||||
show_on_dashboard: true,
|
||||
show_in_sidebar: true,
|
||||
sort_field: 'name',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
})
|
||||
.subscribe()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}${endpoint}/`)
|
||||
.flush({})
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
reloadSpy.mockClear()
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
|
||||
)
|
||||
.flush({
|
||||
results: saved_views,
|
||||
})
|
||||
service.delete(saved_views[0]).subscribe()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}${endpoint}/1/`)
|
||||
.flush({})
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
reloadSpy.mockClear()
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
|
||||
)
|
||||
.flush({
|
||||
results: saved_views,
|
||||
})
|
||||
service.patch(saved_views[0], true).subscribe()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}${endpoint}/1/`)
|
||||
.flush({})
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
|
||||
)
|
||||
.flush({
|
||||
results: saved_views,
|
||||
})
|
||||
service.patchMany(saved_views).subscribe()
|
||||
saved_views.forEach((saved_view) => {
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/${saved_view.id}/`
|
||||
)
|
||||
req.flush({})
|
||||
})
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
|
||||
)
|
||||
.flush({
|
||||
results: saved_views,
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Dont need to setup again
|
||||
|
||||
|
@ -602,7 +602,6 @@ export class SettingsService {
|
||||
)
|
||||
} catch (error) {
|
||||
this.toastService.showError(errorMessage)
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
this.storeSettings()
|
||||
@ -614,7 +613,6 @@ export class SettingsService {
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(errorMessage)
|
||||
console.log(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -636,7 +634,6 @@ export class SettingsService {
|
||||
this.toastService.showError(
|
||||
'Error migrating update checking setting'
|
||||
)
|
||||
console.log(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from '../data/filter-rule-type'
|
||||
import {
|
||||
filterRulesFromQueryParams,
|
||||
@ -97,6 +98,16 @@ describe('QueryParams Utils', () => {
|
||||
correspondent__isnull: 1,
|
||||
})
|
||||
|
||||
params = queryParamsFromFilterRules([
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
|
||||
},
|
||||
])
|
||||
expect(params).toEqual({
|
||||
correspondent__isnull: 0,
|
||||
})
|
||||
|
||||
params = queryParamsFromFilterRules([
|
||||
{
|
||||
rule_type: FILTER_HAS_ANY_TAG,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
FILTER_RULE_TYPES,
|
||||
FilterRuleType,
|
||||
NEGATIVE_NULL_FILTER_VALUE,
|
||||
} from '../data/filter-rule-type'
|
||||
import { ListViewState } from '../services/document-list-view.service'
|
||||
|
||||
@ -113,6 +114,10 @@ export function filterRulesFromQueryParams(
|
||||
rt.isnull_filtervar == filterQueryParamName
|
||||
)
|
||||
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
|
||||
const nullRuleValue =
|
||||
queryParams.get(filterQueryParamName) == '1'
|
||||
? null
|
||||
: NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
const valueURIComponent: string = queryParams.get(filterQueryParamName)
|
||||
const filterQueryParamValues: string[] = rule_type.multi
|
||||
? valueURIComponent.split(',')
|
||||
@ -125,7 +130,7 @@ export function filterRulesFromQueryParams(
|
||||
val = val.replace('1', 'true').replace('0', 'false')
|
||||
return {
|
||||
rule_type: rule_type.id,
|
||||
value: isNullRuleType ? null : val,
|
||||
value: isNullRuleType ? nullRuleValue : val,
|
||||
}
|
||||
})
|
||||
)
|
||||
@ -143,6 +148,11 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
|
||||
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
||||
if (ruleType.isnull_filtervar && rule.value == null) {
|
||||
params[ruleType.isnull_filtervar] = 1
|
||||
} else if (
|
||||
ruleType.isnull_filtervar &&
|
||||
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
|
||||
) {
|
||||
params[ruleType.isnull_filtervar] = 0
|
||||
} else if (ruleType.multi) {
|
||||
params[ruleType.filtervar] = params[ruleType.filtervar]
|
||||
? params[ruleType.filtervar] + ',' + rule.value
|
||||
|
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
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
@ -24,7 +24,7 @@
|
||||
--pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
|
||||
--pngx-bg-disabled: #f7f7f7;
|
||||
--pngx-focus-alpha: 0.3;
|
||||
--pngx-toast-max-width: 360px;
|
||||
--pngx-toast-max-width: 340px;
|
||||
--bs-info: var(--pngx-bg-alt2);
|
||||
--bs-info-rgb: 233, 236, 239;
|
||||
@media screen and (min-width: 1024px) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from documents.models import Document
|
||||
from paperless.config import GeneralConfig
|
||||
|
||||
|
||||
@ -25,4 +27,9 @@ def settings(request):
|
||||
"domain": getattr(django_settings, "PAPERLESS_URL", request.get_host()),
|
||||
"APP_TITLE": app_title,
|
||||
"APP_LOGO": app_logo,
|
||||
"FIRST_INSTALL": User.objects.exclude(
|
||||
username__in=["consumer", "AnonymousUser"],
|
||||
).count()
|
||||
== 0
|
||||
and Document.global_objects.count() == 0,
|
||||
}
|
||||
|
@ -870,7 +870,7 @@ class BasicUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class NotesSerializer(serializers.ModelSerializer):
|
||||
user = BasicUserSerializer()
|
||||
user = BasicUserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
@ -893,7 +893,7 @@ class DocumentSerializer(
|
||||
created_date = serializers.DateField(required=False)
|
||||
page_count = SerializerMethodField()
|
||||
|
||||
notes = NotesSerializer(many=True, required=False)
|
||||
notes = NotesSerializer(many=True, required=False, read_only=True)
|
||||
|
||||
custom_fields = CustomFieldInstanceSerializer(
|
||||
many=True,
|
||||
|
@ -784,10 +784,10 @@ def run_workflows(
|
||||
field=field,
|
||||
document=document,
|
||||
).first()
|
||||
if instance:
|
||||
if instance and args[value_field_name] is not None:
|
||||
setattr(instance, value_field_name, args[value_field_name])
|
||||
instance.save()
|
||||
else:
|
||||
elif not instance:
|
||||
CustomFieldInstance.objects.create(
|
||||
**args,
|
||||
field=field,
|
||||
|
@ -15,6 +15,12 @@
|
||||
{% endblock form_top_content %}
|
||||
|
||||
{% block form_content %}
|
||||
{% if FIRST_INSTALL %}
|
||||
<script type="text/javascript">
|
||||
// forward to the signup page if no users exist
|
||||
window.location.href = "{{ signup_url }}";
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if not DISABLE_REGULAR_LOGIN %}
|
||||
{% translate "Username" as i18n_username %}
|
||||
{% translate "Password" as i18n_password %}
|
||||
|
@ -6,12 +6,19 @@
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block form_top_content %}
|
||||
<p>
|
||||
{% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %}
|
||||
</p>
|
||||
{% if not FIRST_INSTALL %}
|
||||
<p>
|
||||
{% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock form_top_content %}
|
||||
|
||||
{% block form_content %}
|
||||
{% if FIRST_INSTALL %}
|
||||
<p>
|
||||
{% blocktrans %}Note: This is the first user account for this installation and will be granted superuser privileges.{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% translate "Username" as i18n_username %}
|
||||
{% translate "Email (optional)" as i18n_email %}
|
||||
{% translate "Password" as i18n_password1 %}
|
||||
@ -42,29 +49,31 @@
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block after_form_content %}
|
||||
{% load allauth socialaccount %}
|
||||
{% get_providers as socialaccount_providers %}
|
||||
{% if socialaccount_providers %}
|
||||
{% if not DISABLE_REGULAR_LOGIN %}
|
||||
<p class="mt-3">{% translate "or sign in via" %}</p>
|
||||
{% endif %}
|
||||
<ul class="m-0 p-0">
|
||||
{% for provider in socialaccount_providers %}
|
||||
{% if provider.id == "openid" %}
|
||||
{% for brand in provider.get_brands %}
|
||||
{% provider_login_url provider openid=brand.openid_url process=process as href %}
|
||||
<li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
|
||||
<li class="d-grid mt-3">
|
||||
<form class="d-grid" method="POST" action="{{ href }}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-secondary">{{ provider.name }}</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if not FIRST_INSTALL %}
|
||||
{% load allauth socialaccount %}
|
||||
{% get_providers as socialaccount_providers %}
|
||||
{% if socialaccount_providers %}
|
||||
{% if not DISABLE_REGULAR_LOGIN %}
|
||||
<p class="mt-3">{% translate "or sign in via" %}</p>
|
||||
{% endif %}
|
||||
<ul class="m-0 p-0">
|
||||
{% for provider in socialaccount_providers %}
|
||||
{% if provider.id == "openid" %}
|
||||
{% for brand in provider.get_brands %}
|
||||
{% provider_login_url provider openid=brand.openid_url process=process as href %}
|
||||
<li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
|
||||
<li class="d-grid mt-3">
|
||||
<form class="d-grid" method="POST" action="{{ href }}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-secondary">{{ provider.name }}</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock after_form_content %}
|
||||
|
@ -211,7 +211,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
def test_api_modify_tags_not_provided(self, m):
|
||||
"""
|
||||
GIVEN:
|
||||
- API data to modify tags is missing modify_tags field
|
||||
- API data to modify tags is missing remove_tags field
|
||||
WHEN:
|
||||
- API to edit tags is called
|
||||
THEN:
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
|
||||
"PO-Revision-Date: 2025-03-14 17:27\n"
|
||||
"PO-Revision-Date: 2025-03-20 12:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca_ES\n"
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
|
||||
"PO-Revision-Date: 2025-03-11 20:35\n"
|
||||
"PO-Revision-Date: 2025-03-18 12:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
|
@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
|
||||
"POT-Creation-Date: 2025-03-26 21:04-0700\n"
|
||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@ -1235,28 +1235,28 @@ msgstr ""
|
||||
msgid "Don't have an account yet? <a href=\"%(signup_url)s\">Sign up</a>"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/login.html:19
|
||||
#: documents/templates/account/signup.html:15
|
||||
#: documents/templates/account/login.html:25
|
||||
#: documents/templates/account/signup.html:22
|
||||
#: documents/templates/socialaccount/signup.html:13
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/login.html:20
|
||||
#: documents/templates/account/signup.html:17
|
||||
#: documents/templates/account/login.html:26
|
||||
#: documents/templates/account/signup.html:24
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/login.html:30
|
||||
#: documents/templates/account/login.html:36
|
||||
#: documents/templates/mfa/authenticate.html:23
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/login.html:34
|
||||
#: documents/templates/account/login.html:40
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/login.html:45
|
||||
#: documents/templates/account/signup.html:49
|
||||
#: documents/templates/account/login.html:51
|
||||
#: documents/templates/account/signup.html:57
|
||||
msgid "or sign in via"
|
||||
msgstr ""
|
||||
|
||||
@ -1335,21 +1335,27 @@ msgstr ""
|
||||
msgid "Paperless-ngx sign up"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/signup.html:10
|
||||
#: documents/templates/account/signup.html:11
|
||||
#, python-format
|
||||
msgid "Already have an account? <a href=\"%(login_url)s\">Sign in</a>"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/signup.html:16
|
||||
#: documents/templates/account/signup.html:19
|
||||
msgid ""
|
||||
"Note: This is the first user account for this installation and will be "
|
||||
"granted superuser privileges."
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/signup.html:23
|
||||
#: documents/templates/socialaccount/signup.html:14
|
||||
msgid "Email (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/signup.html:18
|
||||
#: documents/templates/account/signup.html:25
|
||||
msgid "Password (again)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/templates/account/signup.html:36
|
||||
#: documents/templates/account/signup.html:43
|
||||
#: documents/templates/socialaccount/signup.html:27
|
||||
msgid "Sign up"
|
||||
msgstr ""
|
||||
@ -1578,139 +1584,139 @@ msgstr ""
|
||||
msgid "paperless application settings"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:723
|
||||
#: paperless/settings.py:722
|
||||
msgid "English (US)"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:724
|
||||
#: paperless/settings.py:723
|
||||
msgid "Arabic"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:725
|
||||
#: paperless/settings.py:724
|
||||
msgid "Afrikaans"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:726
|
||||
#: paperless/settings.py:725
|
||||
msgid "Belarusian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:727
|
||||
#: paperless/settings.py:726
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:728
|
||||
#: paperless/settings.py:727
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:729
|
||||
#: paperless/settings.py:728
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:730
|
||||
#: paperless/settings.py:729
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:731
|
||||
#: paperless/settings.py:730
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:732
|
||||
#: paperless/settings.py:731
|
||||
msgid "Greek"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:733
|
||||
#: paperless/settings.py:732
|
||||
msgid "English (GB)"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:734
|
||||
#: paperless/settings.py:733
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:735
|
||||
#: paperless/settings.py:734
|
||||
msgid "Finnish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:736
|
||||
#: paperless/settings.py:735
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:737
|
||||
#: paperless/settings.py:736
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:738
|
||||
#: paperless/settings.py:737
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:739
|
||||
#: paperless/settings.py:738
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:740
|
||||
#: paperless/settings.py:739
|
||||
msgid "Korean"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:741
|
||||
#: paperless/settings.py:740
|
||||
msgid "Luxembourgish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:742
|
||||
#: paperless/settings.py:741
|
||||
msgid "Norwegian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:743
|
||||
#: paperless/settings.py:742
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:744
|
||||
#: paperless/settings.py:743
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:745
|
||||
#: paperless/settings.py:744
|
||||
msgid "Portuguese (Brazil)"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:746
|
||||
#: paperless/settings.py:745
|
||||
msgid "Portuguese"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:747
|
||||
#: paperless/settings.py:746
|
||||
msgid "Romanian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:748
|
||||
#: paperless/settings.py:747
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:749
|
||||
#: paperless/settings.py:748
|
||||
msgid "Slovak"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:750
|
||||
#: paperless/settings.py:749
|
||||
msgid "Slovenian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:751
|
||||
#: paperless/settings.py:750
|
||||
msgid "Serbian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:752
|
||||
#: paperless/settings.py:751
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:753
|
||||
#: paperless/settings.py:752
|
||||
msgid "Turkish"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:754
|
||||
#: paperless/settings.py:753
|
||||
msgid "Ukrainian"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:755
|
||||
#: paperless/settings.py:754
|
||||
msgid "Chinese Simplified"
|
||||
msgstr ""
|
||||
|
||||
#: paperless/settings.py:756
|
||||
#: paperless/settings.py:755
|
||||
msgid "Chinese Traditional"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
|
||||
"PO-Revision-Date: 2025-03-11 20:35\n"
|
||||
"PO-Revision-Date: 2025-03-18 00:32\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Indonesian\n"
|
||||
"Language: id_ID\n"
|
||||
@ -274,51 +274,51 @@ msgstr "Kartu Besar"
|
||||
|
||||
#: documents/models.py:384
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "Judul"
|
||||
|
||||
#: documents/models.py:385 documents/models.py:942
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
msgstr "Dibuat"
|
||||
|
||||
#: documents/models.py:386 documents/models.py:941
|
||||
msgid "Added"
|
||||
msgstr ""
|
||||
msgstr "Ditambahkan"
|
||||
|
||||
#: documents/models.py:387
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
msgstr "Label"
|
||||
|
||||
#: documents/models.py:388
|
||||
msgid "Correspondent"
|
||||
msgstr ""
|
||||
msgstr "Koresponden"
|
||||
|
||||
#: documents/models.py:389
|
||||
msgid "Document Type"
|
||||
msgstr ""
|
||||
msgstr "Tipe Dokumen"
|
||||
|
||||
#: documents/models.py:390
|
||||
msgid "Storage Path"
|
||||
msgstr ""
|
||||
msgstr "Lokasi Penyimpanan"
|
||||
|
||||
#: documents/models.py:391
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
msgstr "Catatan"
|
||||
|
||||
#: documents/models.py:392
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
msgstr "Pemilik"
|
||||
|
||||
#: documents/models.py:393
|
||||
msgid "Shared"
|
||||
msgstr ""
|
||||
msgstr "Dibagikan"
|
||||
|
||||
#: documents/models.py:394
|
||||
msgid "ASN"
|
||||
msgstr ""
|
||||
msgstr "ASN"
|
||||
|
||||
#: documents/models.py:395
|
||||
msgid "Pages"
|
||||
msgstr ""
|
||||
msgstr "Halaman"
|
||||
|
||||
#: documents/models.py:401
|
||||
msgid "show on dashboard"
|
||||
@ -526,7 +526,7 @@ msgstr "tidak memiliki area khusus"
|
||||
|
||||
#: documents/models.py:489
|
||||
msgid "custom fields query"
|
||||
msgstr ""
|
||||
msgstr "kueri bidang khusus"
|
||||
|
||||
#: documents/models.py:490
|
||||
msgid "created to"
|
||||
@ -566,15 +566,15 @@ msgstr "saring aturan"
|
||||
|
||||
#: documents/models.py:534
|
||||
msgid "Auto Task"
|
||||
msgstr ""
|
||||
msgstr "Tugas Otomatis"
|
||||
|
||||
#: documents/models.py:535
|
||||
msgid "Scheduled Task"
|
||||
msgstr ""
|
||||
msgstr "Tugas Terjadwal"
|
||||
|
||||
#: documents/models.py:536
|
||||
msgid "Manual Task"
|
||||
msgstr ""
|
||||
msgstr "Tugas Manual"
|
||||
|
||||
#: documents/models.py:539
|
||||
msgid "Consume File"
|
||||
@ -666,7 +666,7 @@ msgstr "Data yang dikembalikan dari tugas"
|
||||
|
||||
#: documents/models.py:614
|
||||
msgid "Task Type"
|
||||
msgstr ""
|
||||
msgstr "Tipe Tugas"
|
||||
|
||||
#: documents/models.py:615
|
||||
msgid "The type of task that was run"
|
||||
@ -746,7 +746,7 @@ msgstr "Tautan Dokumen"
|
||||
|
||||
#: documents/models.py:736
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
msgstr "Pilih"
|
||||
|
||||
#: documents/models.py:748
|
||||
msgid "data type"
|
||||
@ -754,7 +754,7 @@ msgstr "jenis data"
|
||||
|
||||
#: documents/models.py:755
|
||||
msgid "extra data"
|
||||
msgstr ""
|
||||
msgstr "data ekstra"
|
||||
|
||||
#: documents/models.py:759
|
||||
msgid "Extra data for the custom field, such as select options"
|
||||
@ -790,7 +790,7 @@ msgstr "Dokumen diperbarui"
|
||||
|
||||
#: documents/models.py:932
|
||||
msgid "Scheduled"
|
||||
msgstr ""
|
||||
msgstr "Dijadwalkan"
|
||||
|
||||
#: documents/models.py:935
|
||||
msgid "Consume Folder"
|
||||
@ -806,15 +806,15 @@ msgstr "Pengambilan Surat"
|
||||
|
||||
#: documents/models.py:938
|
||||
msgid "Web UI"
|
||||
msgstr ""
|
||||
msgstr "Antarmuka Web"
|
||||
|
||||
#: documents/models.py:943
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
msgstr "Dimodifikasi"
|
||||
|
||||
#: documents/models.py:944
|
||||
msgid "Custom Field"
|
||||
msgstr ""
|
||||
msgstr "Kolom Khusus"
|
||||
|
||||
#: documents/models.py:947
|
||||
msgid "Workflow Trigger Type"
|
||||
@ -982,7 +982,7 @@ msgstr "Surel"
|
||||
|
||||
#: documents/models.py:1176
|
||||
msgid "Webhook"
|
||||
msgstr ""
|
||||
msgstr "Webhook"
|
||||
|
||||
#: documents/models.py:1180
|
||||
msgid "Workflow Action Type"
|
||||
@ -1114,11 +1114,11 @@ msgstr "hapus semua kolom khusus"
|
||||
|
||||
#: documents/models.py:1395
|
||||
msgid "email"
|
||||
msgstr ""
|
||||
msgstr "surel"
|
||||
|
||||
#: documents/models.py:1404
|
||||
msgid "webhook"
|
||||
msgstr ""
|
||||
msgstr "webhook"
|
||||
|
||||
#: documents/models.py:1408
|
||||
msgid "workflow action"
|
||||
@ -1146,7 +1146,7 @@ msgstr "diaktifkan"
|
||||
|
||||
#: documents/models.py:1445
|
||||
msgid "workflow"
|
||||
msgstr ""
|
||||
msgstr "alur kerja"
|
||||
|
||||
#: documents/models.py:1449
|
||||
msgid "workflow trigger type"
|
||||
@ -1188,15 +1188,15 @@ msgstr ""
|
||||
|
||||
#: documents/templates/account/account_inactive.html:9
|
||||
msgid "Account inactive."
|
||||
msgstr ""
|
||||
msgstr "Akun tidak aktif."
|
||||
|
||||
#: documents/templates/account/account_inactive.html:14
|
||||
msgid "This account is inactive."
|
||||
msgstr ""
|
||||
msgstr "Akun ini tidak aktif."
|
||||
|
||||
#: documents/templates/account/account_inactive.html:16
|
||||
msgid "Return to login"
|
||||
msgstr ""
|
||||
msgstr "Kembali ke halaman masuk"
|
||||
|
||||
#: documents/templates/account/email/base_message.txt:1
|
||||
#, python-format
|
||||
@ -1358,11 +1358,11 @@ msgstr ""
|
||||
|
||||
#: documents/templates/mfa/authenticate.html:17
|
||||
msgid "Code"
|
||||
msgstr ""
|
||||
msgstr "Kode"
|
||||
|
||||
#: documents/templates/mfa/authenticate.html:24
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
msgstr "Batal"
|
||||
|
||||
#: documents/templates/paperless-ngx/base.html:58
|
||||
msgid "Share link was not found."
|
||||
@ -1470,7 +1470,7 @@ msgstr ""
|
||||
|
||||
#: paperless/models.py:62
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
msgstr "tidak ada"
|
||||
|
||||
#: paperless/models.py:70
|
||||
msgid "LeaveColorUnchanged"
|
||||
@ -1486,7 +1486,7 @@ msgstr ""
|
||||
|
||||
#: paperless/models.py:73
|
||||
msgid "Gray"
|
||||
msgstr ""
|
||||
msgstr "Abu-abu"
|
||||
|
||||
#: paperless/models.py:74
|
||||
msgid "CMYK"
|
||||
@ -1566,7 +1566,7 @@ msgstr "Arab"
|
||||
|
||||
#: paperless/settings.py:725
|
||||
msgid "Afrikaans"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Afrika"
|
||||
|
||||
#: paperless/settings.py:726
|
||||
msgid "Belarusian"
|
||||
@ -1574,7 +1574,7 @@ msgstr "Belarusia"
|
||||
|
||||
#: paperless/settings.py:727
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Bulgaria"
|
||||
|
||||
#: paperless/settings.py:728
|
||||
msgid "Catalan"
|
||||
@ -1622,11 +1622,11 @@ msgstr "Italia"
|
||||
|
||||
#: paperless/settings.py:739
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Jepang"
|
||||
|
||||
#: paperless/settings.py:740
|
||||
msgid "Korean"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Korea"
|
||||
|
||||
#: paperless/settings.py:741
|
||||
msgid "Luxembourgish"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user