mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-03 18:54:40 -05:00
Compare commits
240 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
28261ac51c | ||
![]() |
2f96cc0050 | ||
![]() |
d36e8254f3 | ||
![]() |
405fab8514 | ||
![]() |
ee4f62a1b3 | ||
![]() |
d97e4a9a95 | ||
![]() |
1cfba87114 | ||
![]() |
b145ed315a | ||
![]() |
1f47b8c090 | ||
![]() |
1e3f2a1438 | ||
![]() |
d61b2bbfc6 | ||
![]() |
e1d6b4a9ac | ||
![]() |
9f398337c6 | ||
![]() |
765bf1d11c | ||
![]() |
49a96ccee0 | ||
![]() |
c45bbcaea2 | ||
![]() |
ab87aedfc7 | ||
![]() |
18e3ad8b22 | ||
![]() |
c342eafa8d | ||
![]() |
290c44f5d5 | ||
![]() |
02015ec404 | ||
![]() |
98b4f447d8 | ||
![]() |
272691e386 | ||
![]() |
1012cee39a | ||
![]() |
19a5733d0d | ||
![]() |
86788f1445 | ||
![]() |
1d5e7e930e | ||
![]() |
da6d568906 | ||
![]() |
abf05d5ebe | ||
![]() |
5821033e3d | ||
![]() |
5c1039d07d | ||
![]() |
b874d6d1a4 | ||
![]() |
68a47cbf0b | ||
![]() |
ccc8917706 | ||
![]() |
4c4d3a45c2 | ||
![]() |
6007b252d7 | ||
![]() |
3f05b9f921 | ||
![]() |
67ec1aea42 | ||
![]() |
f5a0b9c174 | ||
![]() |
7035445d6a | ||
![]() |
a899ff16e3 | ||
![]() |
d77c9e3bd5 | ||
![]() |
0ab21b6fc5 | ||
![]() |
485237caf1 | ||
![]() |
9181aebc8c | ||
![]() |
eec3f13610 | ||
![]() |
935d077836 | ||
![]() |
d06aac947d | ||
![]() |
1856837d21 | ||
![]() |
bac6a013ed | ||
![]() |
aef68f0b41 | ||
![]() |
7fbe4d0aad | ||
![]() |
2a1c6bf0ca | ||
![]() |
8e6de2790e | ||
![]() |
94a20b4510 | ||
![]() |
21558dcf8b | ||
![]() |
85d913520b | ||
![]() |
f89b6281da | ||
![]() |
75de53eb83 | ||
![]() |
b0dd77bfd8 | ||
![]() |
51b0f6e325 | ||
![]() |
9153be489c | ||
![]() |
90731e05f5 | ||
![]() |
9270f7290e | ||
![]() |
4e3d25c714 | ||
![]() |
e44cfef662 | ||
![]() |
6c138a21d4 | ||
![]() |
b5a1dc86a5 | ||
![]() |
3520a83c2f | ||
![]() |
93f2ef45f5 | ||
![]() |
8291ec17d4 | ||
![]() |
5007855904 | ||
![]() |
754cf17c90 | ||
![]() |
6972d0337f | ||
![]() |
452ea2ccf9 | ||
![]() |
2bcbed31e9 | ||
![]() |
20b7ff9f9f | ||
![]() |
3f7a0802a4 | ||
![]() |
ea514a7ed8 | ||
![]() |
af67dbe523 | ||
![]() |
8cd09ba10d | ||
![]() |
9e4bc05a24 | ||
![]() |
dafb0b1f21 | ||
![]() |
2ac2a6dec6 | ||
![]() |
beb8ed8313 | ||
![]() |
997db0fea1 | ||
![]() |
64d40a7c37 | ||
![]() |
dc422b5dac | ||
![]() |
f56ab150b0 | ||
![]() |
e3fa3fe818 | ||
![]() |
0a7c296194 | ||
![]() |
c2e34b36ce | ||
![]() |
084f2f2822 | ||
![]() |
e4f69dc945 | ||
![]() |
8574d28c6f | ||
![]() |
27f575c2d1 | ||
![]() |
8e3a021b0f | ||
![]() |
20e45407f5 | ||
![]() |
f836c5ce3e | ||
![]() |
476d753720 | ||
![]() |
65b48952cd | ||
![]() |
0647812699 | ||
![]() |
8722ff481c | ||
![]() |
0c883d064e | ||
![]() |
df2c139721 | ||
![]() |
24b678b7e6 | ||
![]() |
1f85da64e4 | ||
![]() |
a14b9127b6 | ||
![]() |
7d182ab894 | ||
![]() |
51c339d1b7 | ||
![]() |
ab548e36c7 | ||
![]() |
ae4e8808b0 | ||
![]() |
b4e369b556 | ||
![]() |
5f7c60d9a1 | ||
![]() |
fca10227bc | ||
![]() |
007e5a5473 | ||
![]() |
70d9a6fd36 | ||
![]() |
0406fca59b | ||
![]() |
1d65628132 | ||
![]() |
81a5baa451 | ||
![]() |
e9254d4eef | ||
![]() |
0fc1860d4c | ||
![]() |
00485138f9 | ||
![]() |
79345f0a69 | ||
![]() |
3aec0b3372 | ||
![]() |
dbccd13915 | ||
![]() |
fbeaed930e | ||
![]() |
5a74a92b74 | ||
![]() |
d1a4c1f2eb | ||
![]() |
9614528033 | ||
![]() |
548a7f05d8 | ||
![]() |
771b6606e0 | ||
![]() |
c08ca6e3e5 | ||
![]() |
a186527f07 | ||
![]() |
ecb300845c | ||
![]() |
fcf532f13e | ||
![]() |
cc93bc41df | ||
![]() |
c6fdf4409b | ||
![]() |
37f8a77516 | ||
![]() |
2b29233a1e | ||
![]() |
d5572137de | ||
![]() |
d370704286 | ||
![]() |
be684c9a11 | ||
![]() |
447b4cdb98 | ||
![]() |
f6cc2f9fc3 | ||
![]() |
beb69ae01e | ||
![]() |
8bfe68743d | ||
![]() |
9c1561adfb | ||
![]() |
d7d3fed833 | ||
![]() |
827121808a | ||
![]() |
f0e71330ac | ||
![]() |
e4578b4589 | ||
![]() |
e94a92ed59 | ||
![]() |
6c3d6d562d | ||
![]() |
c324a71e91 | ||
![]() |
2e6f85cc3a | ||
![]() |
56ef263540 | ||
![]() |
36ecb8587d | ||
![]() |
ad9bdaf1b8 | ||
![]() |
22f29b3659 | ||
![]() |
0dc47d156f | ||
![]() |
3633fb382f | ||
![]() |
a283a65813 | ||
![]() |
74d0c9fda5 | ||
![]() |
d1f255a22e | ||
![]() |
ceb834bdef | ||
![]() |
6c230f098a | ||
![]() |
da40d03be6 | ||
![]() |
a6f4c75a72 | ||
![]() |
c22a80abd3 | ||
![]() |
2806b1820e | ||
![]() |
177cc9d985 | ||
![]() |
da85b05ea4 | ||
![]() |
82fd706dca | ||
![]() |
2f06680f76 | ||
![]() |
64095a710a | ||
![]() |
005150c84f | ||
![]() |
c57cd7e298 | ||
![]() |
ef329fc687 | ||
![]() |
695967cbb2 | ||
![]() |
c028910cdd | ||
![]() |
37f836db2c | ||
![]() |
d01192b81e | ||
![]() |
9655c89b69 | ||
![]() |
ccf1430f82 | ||
![]() |
c2509ea439 | ||
![]() |
3cd9bcebe6 | ||
![]() |
2368fd15cb | ||
![]() |
cc35b321c5 | ||
![]() |
1ddbc31c59 | ||
![]() |
6ae8ab4af3 | ||
![]() |
37dc791301 | ||
![]() |
5fed311ffc | ||
![]() |
e74f182662 | ||
![]() |
3f094c88bd | ||
![]() |
631eaa2109 | ||
![]() |
27fadf6963 | ||
![]() |
d53b300a7f | ||
![]() |
6cc626fd86 | ||
![]() |
8b8f1af513 | ||
![]() |
4226a1bddc | ||
![]() |
289a7a2348 | ||
![]() |
702ab1b77a | ||
![]() |
e22ccbda26 | ||
![]() |
56d296f04b | ||
![]() |
11cfa0871e | ||
![]() |
159344f033 | ||
![]() |
d8cfed5f5e | ||
![]() |
94fe7a9e3d | ||
![]() |
9c9b4effe2 | ||
![]() |
75f5007ede | ||
![]() |
d95baf4e6b | ||
![]() |
aac04e73b9 | ||
![]() |
3af3484a00 | ||
![]() |
90e68af6cf | ||
![]() |
eee08d389f | ||
![]() |
b3b0e95d2d | ||
![]() |
35907313e8 | ||
![]() |
d4a20c7e30 | ||
![]() |
3b1dffe0dc | ||
![]() |
fa0ab0de27 | ||
![]() |
3b2b4a9177 | ||
![]() |
3d030637ca | ||
![]() |
79092c27c5 | ||
![]() |
28fdb170bf | ||
![]() |
335c6c3820 | ||
![]() |
ad23cce2e6 | ||
![]() |
0d96cd03d5 | ||
![]() |
1888ee6a3f | ||
![]() |
605aa50b00 | ||
![]() |
149d770ad1 | ||
![]() |
b2e9f3195a | ||
![]() |
33e9990ed5 | ||
![]() |
e775b6346a | ||
![]() |
54e17f5b74 | ||
![]() |
ff1639d58b | ||
![]() |
7649903d3c | ||
![]() |
7a5d707fc0 | ||
![]() |
85e00aecb4 | ||
![]() |
53aa216a4a |
@@ -1,117 +0,0 @@
|
|||||||
# Paperless NGX Development Environment
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Welcome to the Paperless NGX development environment! This setup uses VSCode DevContainers to provide a consistent and seamless development experience.
|
|
||||||
|
|
||||||
### What are DevContainers?
|
|
||||||
|
|
||||||
DevContainers are a feature in VSCode that allows you to develop within a Docker container. This ensures that your development environment is consistent across different machines and setups. By defining a containerized environment, you can eliminate the "works on my machine" problem.
|
|
||||||
|
|
||||||
### Advantages of DevContainers
|
|
||||||
|
|
||||||
- **Consistency**: Same environment for all developers.
|
|
||||||
- **Isolation**: Separate development environment from your local machine.
|
|
||||||
- **Reproducibility**: Easily recreate the environment on any machine.
|
|
||||||
- **Pre-configured Tools**: Include all necessary tools and dependencies in the container.
|
|
||||||
|
|
||||||
## DevContainer Setup
|
|
||||||
|
|
||||||
The DevContainer configuration provides up all the necessary services for Paperless NGX, including:
|
|
||||||
|
|
||||||
- Redis
|
|
||||||
- Gotenberg
|
|
||||||
- Tika
|
|
||||||
|
|
||||||
Data is stored using Docker volumes to ensure persistence across container restarts.
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
The setup includes debugging configurations (`launch.json`) and tasks (`tasks.json`) to help you manage and debug various parts of the project:
|
|
||||||
|
|
||||||
- **Backend Debugging:**
|
|
||||||
- `manage.py runserver`
|
|
||||||
- `manage.py document-consumer`
|
|
||||||
- `celery`
|
|
||||||
- **Maintenance Tasks:**
|
|
||||||
- Create superuser
|
|
||||||
- Run migrations
|
|
||||||
- Recreate virtual environment (`.venv` with pipenv)
|
|
||||||
- Compile frontend assets
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Step 1: Running the DevContainer
|
|
||||||
|
|
||||||
To start the DevContainer:
|
|
||||||
|
|
||||||
1. Open VSCode.
|
|
||||||
2. Open the project folder.
|
|
||||||
3. Open the command palette:
|
|
||||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
|
||||||
- **Mac**: `Cmd+Shift+P`
|
|
||||||
4. Type and select `Dev Containers: Rebuild and Reopen in Container`.
|
|
||||||
|
|
||||||
VSCode will build and start the DevContainer environment.
|
|
||||||
|
|
||||||
### Step 2: Initial Setup
|
|
||||||
|
|
||||||
Once the DevContainer is up and running, perform the following steps:
|
|
||||||
|
|
||||||
1. **Compile Frontend Assets**:
|
|
||||||
|
|
||||||
- Open the command palette:
|
|
||||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
|
||||||
- **Mac**: `Cmd+Shift+P`
|
|
||||||
- Select `Tasks: Run Task`.
|
|
||||||
- Choose `Frontend Compile`.
|
|
||||||
|
|
||||||
2. **Run Database Migrations**:
|
|
||||||
|
|
||||||
- Open the command palette:
|
|
||||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
|
||||||
- **Mac**: `Cmd+Shift+P`
|
|
||||||
- Select `Tasks: Run Task`.
|
|
||||||
- Choose `Migrate Database`.
|
|
||||||
|
|
||||||
3. **Create Superuser**:
|
|
||||||
- Open the command palette:
|
|
||||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
|
||||||
- **Mac**: `Cmd+Shift+P`
|
|
||||||
- Select `Tasks: Run Task`.
|
|
||||||
- Choose `Create Superuser`.
|
|
||||||
|
|
||||||
### Debugging and Running Services
|
|
||||||
|
|
||||||
You can start and debug backend services either as debugging sessions via `launch.json` or as tasks.
|
|
||||||
|
|
||||||
#### Using `launch.json`:
|
|
||||||
|
|
||||||
1. Press `F5` or go to the **Run and Debug** view in VSCode.
|
|
||||||
2. Select the desired configuration:
|
|
||||||
- `Runserver`
|
|
||||||
- `Document Consumer`
|
|
||||||
- `Celery`
|
|
||||||
|
|
||||||
#### Using Tasks:
|
|
||||||
|
|
||||||
1. Open the command palette:
|
|
||||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
|
||||||
- **Mac**: `Cmd+Shift+P`
|
|
||||||
2. Select `Tasks: Run Task`.
|
|
||||||
3. Choose the desired task:
|
|
||||||
- `Runserver`
|
|
||||||
- `Document Consumer`
|
|
||||||
- `Celery`
|
|
||||||
|
|
||||||
### Additional Maintenance Tasks
|
|
||||||
|
|
||||||
Additional tasks are available for common maintenance operations:
|
|
||||||
|
|
||||||
- **Recreate .venv**: For setting up the virtual environment using pipenv.
|
|
||||||
- **Migrate Database**: To apply database migrations.
|
|
||||||
- **Create Superuser**: To create an admin user for the application.
|
|
||||||
|
|
||||||
## Let's Get Started!
|
|
||||||
|
|
||||||
Follow the steps above to get your development environment up and running. Happy coding!
|
|
@@ -3,14 +3,26 @@
|
|||||||
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
|
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
|
||||||
"service": "paperless-development",
|
"service": "paperless-development",
|
||||||
"workspaceFolder": "/usr/src/paperless/paperless-ngx",
|
"workspaceFolder": "/usr/src/paperless/paperless-ngx",
|
||||||
"postCreateCommand": "/bin/bash -c pre-commit install && pipenv install --dev",
|
"postCreateCommand": "pipenv install --dev && pipenv run pre-commit install",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"mhutchie.git-graph",
|
"mhutchie.git-graph",
|
||||||
"ms-python.python"
|
"ms-python.python",
|
||||||
]
|
"ms-vscode.js-debug-nightly",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"yzhang.markdown-all-in-one"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"python.defaultInterpreterPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python",
|
||||||
|
"python.pythonPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python",
|
||||||
|
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||||
|
"editor.formatOnPaste": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"files.trimTrailingWhitespace": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remoteUser": "paperless"
|
"remoteUser": "paperless"
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ services:
|
|||||||
image: docker.io/library/redis:7
|
image: docker.io/library/redis:7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- redisdata:/data
|
- ./redisdata:/data
|
||||||
|
|
||||||
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
|
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
|
||||||
paperless-development:
|
paperless-development:
|
||||||
@@ -43,14 +43,16 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/paperless/paperless-ngx:delegated
|
- ..:/usr/src/paperless/paperless-ngx:delegated
|
||||||
- ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files
|
- ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files
|
||||||
- pipenv:/usr/src/paperless/paperless-ngx/.venv # Pipenv environment persisted in volume
|
- pipenv:/usr/src/paperless/paperless-ngx/.venv
|
||||||
- /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container
|
- /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container
|
||||||
- /usr/src/paperless/paperless-ngx/src/.pytest_cache
|
- /usr/src/paperless/paperless-ngx/src/.pytest_cache
|
||||||
- /usr/src/paperless/paperless-ngx/.ruff_cache
|
- /usr/src/paperless/paperless-ngx/.ruff_cache
|
||||||
- /usr/src/paperless/paperless-ngx/htmlcov
|
- /usr/src/paperless/paperless-ngx/htmlcov
|
||||||
- /usr/src/paperless/paperless-ngx/.coverage
|
- /usr/src/paperless/paperless-ngx/.coverage
|
||||||
- data:/usr/src/paperless/paperless-ngx/data
|
- ./data:/usr/src/paperless/paperless-ngx/data
|
||||||
- media:/usr/src/paperless/paperless-ngx/media
|
- ./media:/usr/src/paperless/paperless-ngx/media
|
||||||
|
- ./consume:/usr/src/paperless/paperless-ngx/consume
|
||||||
|
- ~/.gitconfig:/usr/src/paperless/.gitconfig:ro
|
||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_TIKA_ENABLED: 1
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
@@ -78,7 +80,4 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
|
||||||
media:
|
|
||||||
redisdata:
|
|
||||||
pipenv:
|
pipenv:
|
||||||
|
@@ -2,42 +2,57 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "manage.py runserver",
|
"name": "Chrome: Debug Angular Frontend",
|
||||||
|
"description": "Debug the Angular Dev Frontend in Chrome",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:4200",
|
||||||
|
"webRoot": "${workspaceFolder}/src-ui",
|
||||||
|
"preLaunchTask": "Start: Frontend Angular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Backend Server (manage.py runserver)",
|
||||||
|
"description": "Debug the Django Backend Server",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/src/manage.py",
|
"program": "${workspaceFolder}/src/manage.py",
|
||||||
"console": "integratedTerminal",
|
"args": [
|
||||||
"justMyCode": true,
|
"runserver"
|
||||||
"args": ["runserver"],
|
],
|
||||||
"django": true
|
"django": true,
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "manage.py document_consumer",
|
|
||||||
"type": "python",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder}/src/manage.py",
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"justMyCode": true,
|
|
||||||
"args": ["document_consumer"],
|
|
||||||
"django": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "celery",
|
|
||||||
"type": "python",
|
|
||||||
"cwd": "${workspaceFolder}/src",
|
|
||||||
"request": "launch",
|
|
||||||
"module": "celery",
|
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"env": {
|
"env": {
|
||||||
"PYTHONPATH": "${workspaceFolder}/src"
|
"PYTHONPATH": "${workspaceFolder}/src"
|
||||||
},
|
},
|
||||||
|
"python": "${workspaceFolder}/.venv/bin/python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Consumer Service (manage.py document_consumer)",
|
||||||
|
"description": "Debug the Consumer Service which processes files from a directory",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/src/manage.py",
|
||||||
"args": [
|
"args": [
|
||||||
"-A",
|
"document_consumer"
|
||||||
"paperless",
|
],
|
||||||
"worker",
|
"django": true,
|
||||||
"-l",
|
"console": "integratedTerminal",
|
||||||
"DEBUG"
|
"env": {
|
||||||
]
|
"PYTHONPATH": "${workspaceFolder}/src"
|
||||||
|
},
|
||||||
|
"python": "${workspaceFolder}/.venv/bin/python"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Debug: FullStack",
|
||||||
|
"description": "Debug run the Angular dev frontend, Django backend, and consumer service",
|
||||||
|
"configurations": [
|
||||||
|
"Chrome: Debug Angular Frontend",
|
||||||
|
"Debug: Backend Server (manage.py runserver)",
|
||||||
|
"Debug: Consumer Service (manage.py document_consumer)"
|
||||||
|
],
|
||||||
|
"preLaunchTask": "Start: Celery Worker"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +1,84 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "manage.py document_consumer",
|
"label": "Start: Celery Worker",
|
||||||
"type": "shell",
|
"description": "Start the Celery Worker which processes background and consume tasks",
|
||||||
"command": "pipenv run python manage.py document_consumer",
|
"type": "shell",
|
||||||
"group": "build",
|
"command": "pipenv run celery --app paperless worker -l DEBUG",
|
||||||
"presentation": {
|
"isBackground": true,
|
||||||
"echo": true,
|
"options": {
|
||||||
"reveal": "always",
|
"cwd": "${workspaceFolder}/src"
|
||||||
"focus": false,
|
},
|
||||||
"panel": "shared",
|
"problemMatcher": [
|
||||||
"showReuseMessage": false,
|
{
|
||||||
"clear": true,
|
"owner": "custom",
|
||||||
"revealProblems": "onProblem"
|
"pattern": [
|
||||||
},
|
{
|
||||||
"options": {
|
"regexp": ".",
|
||||||
"cwd": "${workspaceFolder}/src"
|
"file": 1,
|
||||||
}
|
"location": 2,
|
||||||
|
"message": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": "celery.*",
|
||||||
|
"endsPattern": "ready"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "manage.py runserver",
|
"label": "Start: Frontend Angular",
|
||||||
|
"description": "Start the Frontend Angular Dev Server",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm start",
|
||||||
|
"isBackground": true,
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src-ui"
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "custom",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": ".",
|
||||||
|
"file": 1,
|
||||||
|
"location": 2,
|
||||||
|
"message": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": ".*",
|
||||||
|
"endsPattern": "Compiled successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Start: Consumer Service (manage.py document_consumer)",
|
||||||
|
"description": "Start the Consumer Service which processes files from a directory",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pipenv run python manage.py document_consumer",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"revealProblems": "onProblem"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Start: Backend Server (manage.py runserver)",
|
||||||
|
"description": "Start the Backend Server which serves the Django API and the compiled Angular frontend",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pipenv run python manage.py runserver",
|
"command": "pipenv run python manage.py runserver",
|
||||||
"group": "build",
|
"group": "build",
|
||||||
@@ -37,100 +94,130 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/src"
|
"cwd": "${workspaceFolder}/src"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maintenance: manage.py migrate",
|
||||||
|
"description": "Apply database migrations",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pipenv run python manage.py migrate",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"revealProblems": "onProblem"
|
||||||
},
|
},
|
||||||
{
|
"options": {
|
||||||
"label": "Maintenance: manage.py migrate",
|
"cwd": "${workspaceFolder}/src"
|
||||||
"type": "shell",
|
}
|
||||||
"command": "pipenv run python manage.py migrate",
|
|
||||||
"group": "none",
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": true,
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": false,
|
|
||||||
"clear": true,
|
|
||||||
"revealProblems": "onProblem"
|
|
||||||
},
|
},
|
||||||
"options": {
|
{
|
||||||
"cwd": "${workspaceFolder}/src"
|
"label": "Maintenance: Build Documentation",
|
||||||
|
"description": "Build the documentation with MkDocs",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pipenv run mkdocs build --config-file mkdocs.yml && pipenv run mkdocs serve",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"revealProblems": "onProblem"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maintenance: manage.py createsuperuser",
|
||||||
|
"description": "Create a superuser",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pipenv run python manage.py createsuperuser",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"revealProblems": "onProblem"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maintenance: recreate .venv",
|
||||||
|
"description": "Recreate the python virtual environment and install python dependencies",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "rm -R -v .venv/* || pipenv install --dev",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"revealProblems": "onProblem"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maintenance: Install Frontend Dependencies",
|
||||||
|
"description": "Install frontend (npm) dependencies",
|
||||||
|
"type": "npm",
|
||||||
|
"script": "install",
|
||||||
|
"path": "src-ui",
|
||||||
|
"group": "clean",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"detail": "install dependencies from package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Clean install frontend dependencies and build the frontend for production",
|
||||||
|
"label": "Maintenance: Compile frontend for production",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm ci && ./node_modules/.bin/ng build --configuration production",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true,
|
||||||
|
"revealProblems": "onProblem"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src-ui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Project Setup: Run all Init Tasks",
|
||||||
|
"description": "Runs all init tasks to setup the project including migrate the database, create a superuser and compile the frontend for production",
|
||||||
|
"dependsOrder": "sequence",
|
||||||
|
"dependsOn": [
|
||||||
|
"Maintenance: manage.py migrate",
|
||||||
|
"Maintenance: manage.py createsuperuser",
|
||||||
|
"Maintenance: Compile frontend for production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Project Start: Run all Services",
|
||||||
|
"description": "Runs all services required to start the project including the Celery Worker, the Consumer Service and the Backend Server",
|
||||||
|
"dependsOn": [
|
||||||
|
"Start: Celery Worker",
|
||||||
|
"Start: Consumer Service (manage.py document_consumer)",
|
||||||
|
"Start: Backend Server (manage.py runserver)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Maintenance: manage.py createsuperuser",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "pipenv run python manage.py createsuperuser",
|
|
||||||
"group": "none",
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": true,
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": false,
|
|
||||||
"clear": true,
|
|
||||||
"revealProblems": "onProblem"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "compile frontend",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm ci && ./node_modules/.bin/ng build --configuration production",
|
|
||||||
"group": "none",
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": true,
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": false,
|
|
||||||
"clear": true,
|
|
||||||
"revealProblems": "onProblem"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/src-ui"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Maintenance: recreate .venv",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "rm -R -v .venv/* || pipenv install --dev",
|
|
||||||
"group": "none",
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": true,
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": false,
|
|
||||||
"clear": true,
|
|
||||||
"revealProblems": "onProblem"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Celery Worker",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "pipenv run celery --app paperless worker -l DEBUG",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": true,
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": false,
|
|
||||||
"clear": true,
|
|
||||||
"revealProblems": "onProblem"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -98,7 +98,7 @@ body:
|
|||||||
label: Browser
|
label: Browser
|
||||||
description: Which browser you are using, if relevant.
|
description: Which browser you are using, if relevant.
|
||||||
placeholder: e.g. Chrome, Safari
|
placeholder: e.g. Chrome, Safari
|
||||||
- type: input
|
- type: textarea
|
||||||
id: config-changes
|
id: config-changes
|
||||||
attributes:
|
attributes:
|
||||||
label: Configuration changes
|
label: Configuration changes
|
||||||
|
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
# This is the version of pipenv all the steps will use
|
# This is the version of pipenv all the steps will use
|
||||||
# If changing this, change Dockerfile
|
# If changing this, change Dockerfile
|
||||||
DEFAULT_PIP_ENV_VERSION: "2024.0.3"
|
DEFAULT_PIP_ENV_VERSION: "2024.4.0"
|
||||||
# This is the default version of Python to use in most steps which aren't specific
|
# This is the default version of Python to use in most steps which aren't specific
|
||||||
DEFAULT_PYTHON_VERSION: "3.11"
|
DEFAULT_PYTHON_VERSION: "3.11"
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
github.repository
|
github.repository
|
||||||
|
|
||||||
name: Linting Checks
|
name: Linting Checks
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
name: "Build & Deploy Documentation"
|
name: "Build & Deploy Documentation"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
|
|
||||||
tests-backend:
|
tests-backend:
|
||||||
name: "Backend Tests (Python ${{ matrix.python-version }})"
|
name: "Backend Tests (Python ${{ matrix.python-version }})"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
strategy:
|
strategy:
|
||||||
@@ -170,7 +170,7 @@ jobs:
|
|||||||
|
|
||||||
install-frontend-depedendencies:
|
install-frontend-depedendencies:
|
||||||
name: "Install Frontend Dependencies"
|
name: "Install Frontend Dependencies"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
|
|
||||||
tests-frontend:
|
tests-frontend:
|
||||||
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- install-frontend-depedendencies
|
- install-frontend-depedendencies
|
||||||
strategy:
|
strategy:
|
||||||
@@ -261,7 +261,7 @@ jobs:
|
|||||||
|
|
||||||
tests-coverage-upload:
|
tests-coverage-upload:
|
||||||
name: "Upload to Codecov"
|
name: "Upload to Codecov"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- tests-backend
|
- tests-backend
|
||||||
- tests-frontend
|
- tests-frontend
|
||||||
@@ -283,7 +283,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
-
|
-
|
||||||
name: Upload frontend coverage to Codecov
|
name: Upload frontend coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
# not required for public repos, but intermittently fails otherwise
|
# not required for public repos, but intermittently fails otherwise
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -299,7 +299,7 @@ jobs:
|
|||||||
path: src/
|
path: src/
|
||||||
-
|
-
|
||||||
name: Upload coverage to Codecov
|
name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
# not required for public repos, but intermittently fails otherwise
|
# not required for public repos, but intermittently fails otherwise
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -333,7 +333,7 @@ jobs:
|
|||||||
|
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
name: Build Docker image for ${{ github.ref_name }}
|
name: Build Docker image for ${{ github.ref_name }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
||||||
@@ -406,7 +406,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Login to Docker Hub
|
name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
# Don't attempt to login is not pushing to Docker Hub
|
# Don't attempt to login if not pushing to Docker Hub
|
||||||
if: steps.push-other-places.outputs.enable == 'true'
|
if: steps.push-other-places.outputs.enable == 'true'
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -414,7 +414,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Login to Quay.io
|
name: Login to Quay.io
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
# Don't attempt to login is not pushing to Quay.io
|
# Don't attempt to login if not pushing to Quay.io
|
||||||
if: steps.push-other-places.outputs.enable == 'true'
|
if: steps.push-other-places.outputs.enable == 'true'
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
@@ -461,7 +461,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-docker-image
|
- build-docker-image
|
||||||
- documentation
|
- documentation
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -482,12 +482,6 @@ jobs:
|
|||||||
name: Install Python dependencies
|
name: Install Python dependencies
|
||||||
run: |
|
run: |
|
||||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
|
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
|
||||||
-
|
|
||||||
name: Patch whitenoise
|
|
||||||
run: |
|
|
||||||
curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch
|
|
||||||
patch -d $(pipenv --venv)/lib/python3.11/site-packages --verbose -p2 < 484.patch
|
|
||||||
rm 484.patch
|
|
||||||
-
|
-
|
||||||
name: Install system dependencies
|
name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -575,7 +569,7 @@ jobs:
|
|||||||
|
|
||||||
publish-release:
|
publish-release:
|
||||||
name: "Publish Release"
|
name: "Publish Release"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||||
changelog: ${{ steps.create-release.outputs.body }}
|
changelog: ${{ steps.create-release.outputs.body }}
|
||||||
@@ -625,7 +619,7 @@ jobs:
|
|||||||
|
|
||||||
append-changelog:
|
append-changelog:
|
||||||
name: "Append Changelog"
|
name: "Append Changelog"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- publish-release
|
- publish-release
|
||||||
if: needs.publish-release.outputs.prerelease == 'false'
|
if: needs.publish-release.outputs.prerelease == 'false'
|
||||||
@@ -655,7 +649,9 @@ jobs:
|
|||||||
git checkout ${{ needs.publish-release.outputs.version }}-changelog
|
git checkout ${{ needs.publish-release.outputs.version }}-changelog
|
||||||
echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md
|
echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md
|
||||||
echo "Manually linking usernames"
|
echo "Manually linking usernames"
|
||||||
sed -i -r 's|@(.+?) \(\[#|[@\1](https://github.com/\1) ([#|ig' changelog-new.md
|
sed -i -r 's|@([a-zA-Z0-9_]+) \(\[#|[@\1](https://github.com/\1) ([#|g' changelog-new.md
|
||||||
|
echo "Removing unneeded comment tags"
|
||||||
|
sed -i -r 's|@<!---->|@|g' changelog-new.md
|
||||||
CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
|
CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
|
||||||
echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
|
echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
|
||||||
mv changelog-new.md changelog.md
|
mv changelog-new.md changelog.md
|
||||||
|
8
.github/workflows/cleanup-tags.yml
vendored
8
.github/workflows/cleanup-tags.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
cleanup-images:
|
cleanup-images:
|
||||||
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Clean temporary images
|
name: Clean temporary images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
|
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "${{ github.repository_owner }}"
|
owner: "${{ github.repository_owner }}"
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
cleanup-untagged-images:
|
cleanup-untagged-images:
|
||||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- cleanup-images
|
- cleanup-images
|
||||||
strategy:
|
strategy:
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Clean untagged images
|
name: Clean untagged images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
|
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "${{ github.repository_owner }}"
|
owner: "${{ github.repository_owner }}"
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
2
.github/workflows/crowdin.yml
vendored
2
.github/workflows/crowdin.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
name: Crowdin Sync
|
name: Crowdin Sync
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
2
.github/workflows/project-actions.yml
vendored
2
.github/workflows/project-actions.yml
vendored
@@ -15,7 +15,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
pr_opened_or_reopened:
|
pr_opened_or_reopened:
|
||||||
name: pr_opened_or_reopened
|
name: pr_opened_or_reopened
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
# write permission is required for autolabeler
|
# write permission is required for autolabeler
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
10
.github/workflows/repo-maintenance.yml
vendored
10
.github/workflows/repo-maintenance.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
name: 'Stale'
|
name: 'Stale'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
lock-threads:
|
lock-threads:
|
||||||
name: 'Lock Old Threads'
|
name: 'Lock Old Threads'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5
|
- uses: dessant/lock-threads@v5
|
||||||
with:
|
with:
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
close-answered-discussions:
|
close-answered-discussions:
|
||||||
name: 'Close Answered Discussions'
|
name: 'Close Answered Discussions'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
close-outdated-discussions:
|
close-outdated-discussions:
|
||||||
name: 'Close Outdated Discussions'
|
name: 'Close Outdated Discussions'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
@@ -208,7 +208,7 @@ jobs:
|
|||||||
close-unsupported-feature-requests:
|
close-unsupported-feature-requests:
|
||||||
name: 'Close Unsupported Feature Requests'
|
name: 'Close Unsupported Feature Requests'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -100,3 +100,9 @@ scripts/nuke
|
|||||||
|
|
||||||
# celery schedule file
|
# celery schedule file
|
||||||
celerybeat-schedule*
|
celerybeat-schedule*
|
||||||
|
|
||||||
|
# ignore .devcontainer sub folders
|
||||||
|
/.devcontainer/consume/
|
||||||
|
/.devcontainer/data/
|
||||||
|
/.devcontainer/media/
|
||||||
|
/.devcontainer/redisdata/
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
repos:
|
repos:
|
||||||
# General hooks
|
# General hooks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-docstring-first
|
- id: check-docstring-first
|
||||||
- id: check-json
|
- id: check-json
|
||||||
@@ -46,9 +46,12 @@ repos:
|
|||||||
- ts
|
- ts
|
||||||
- markdown
|
- markdown
|
||||||
exclude: "(^Pipfile\\.lock$)"
|
exclude: "(^Pipfile\\.lock$)"
|
||||||
|
additional_dependencies:
|
||||||
|
- prettier@3.3.3
|
||||||
|
- 'prettier-plugin-organize-imports@4.1.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 'v0.6.8'
|
rev: v0.8.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
16
.prettierrc
16
.prettierrc
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
# https://prettier.io/docs/en/options.html#semicolons
|
|
||||||
"semi": false,
|
|
||||||
# https://prettier.io/docs/en/options.html#quotes
|
|
||||||
"singleQuote": true,
|
|
||||||
# https://prettier.io/docs/en/options.html#trailing-commas
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["index.md", "administration.md"],
|
|
||||||
"options": {
|
|
||||||
"tabWidth": 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
19
.prettierrc.js
Normal file
19
.prettierrc.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const config = {
|
||||||
|
// https://prettier.io/docs/en/options.html#semicolons
|
||||||
|
semi: false,
|
||||||
|
// https://prettier.io/docs/en/options.html#quotes
|
||||||
|
singleQuote: true,
|
||||||
|
// https://prettier.io/docs/en/options.html#trailing-commas
|
||||||
|
trailingComma: 'es5',
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['docs/*.md'],
|
||||||
|
options: {
|
||||||
|
tabWidth: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [require('prettier-plugin-organize-imports')],
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config
|
44
.ruff.toml
44
.ruff.toml
@@ -31,17 +31,55 @@ extend-select = [
|
|||||||
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||||
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
||||||
|
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
||||||
]
|
]
|
||||||
# TODO PTH https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
|
||||||
ignore = ["DJ001", "SIM105", "RUF012"]
|
ignore = ["DJ001", "SIM105", "RUF012"]
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
[lint.per-file-ignores]
|
||||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||||
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
||||||
|
"src/documents/consumer.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/management/commands/document_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/management/commands/document_exporter.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/migrations/0012_auto_20160305_0040.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/migrations/0014_document_checksum.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/migrations/1003_mime_types.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/migrations/1012_fix_archive_files.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/models.py" = ["SIM115", "PTH"] # TODO PTH Enable & remove
|
||||||
|
"src/documents/parsers.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/signals/handlers.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tasks.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_api_app_config.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_api_bulk_download.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_api_documents.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_classifier.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_management.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_management_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_management_exporter.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_management_thumbnails.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_migration_archive_files.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_migration_document_pages_count.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_migration_mime_type.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_sanity_check.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_tasks.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/tests/test_views.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/documents/views.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless/checks.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless/settings.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless/tests/test_checks.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless/urls.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless/views.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless_mail/mail.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless_mail/preprocessor.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless_tesseract/parsers.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001", "PTH"] # TODO PTH Enable & remove
|
||||||
|
"src/paperless_tika/tests/test_live_tika.py" = ["PTH"] # TODO Enable & remove
|
||||||
|
"src/paperless_tika/tests/test_tika_parser.py" = ["PTH"] # TODO Enable & remove
|
||||||
"*/tests/*.py" = ["E501", "SIM117"]
|
"*/tests/*.py" = ["E501", "SIM117"]
|
||||||
"*/migrations/*.py" = ["E501", "SIM", "T201"]
|
"*/migrations/*.py" = ["E501", "SIM", "T201"]
|
||||||
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001"]
|
|
||||||
"src/documents/models.py" = ["SIM115"]
|
|
||||||
|
|
||||||
[lint.isort]
|
[lint.isort]
|
||||||
force-single-line = true
|
force-single-line = true
|
||||||
|
14
Dockerfile
14
Dockerfile
@@ -39,7 +39,7 @@ COPY Pipfile* ./
|
|||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Installing pipenv" \
|
&& echo "Installing pipenv" \
|
||||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.0.3 \
|
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.4.0 \
|
||||||
&& echo "Generating requirement.txt" \
|
&& echo "Generating requirement.txt" \
|
||||||
&& pipenv requirements > requirements.txt
|
&& pipenv requirements > requirements.txt
|
||||||
|
|
||||||
@@ -233,16 +233,12 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
|||||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||||
&& echo "Installing Python requirements" \
|
&& echo "Installing Python requirements" \
|
||||||
&& curl --fail --silent --show-error --location \
|
&& curl --fail --silent --show-error --location \
|
||||||
--output psycopg_c-3.2.2-cp312-cp312-linux_x86_64.whl \
|
--output psycopg_c-3.2.3-cp312-cp312-linux_x86_64.whl \
|
||||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.2/psycopg_c-3.2.2-cp312-cp312-linux_x86_64.whl \
|
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.3/psycopg_c-3.2.3-cp312-cp312-linux_x86_64.whl \
|
||||||
&& curl --fail --silent --show-error --location \
|
&& curl --fail --silent --show-error --location \
|
||||||
--output psycopg_c-3.2.2-cp312-cp312-linux_aarch64.whl \
|
--output psycopg_c-3.2.3-cp312-cp312-linux_aarch64.whl \
|
||||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.2/psycopg_c-3.2.2-cp312-cp312-linux_aarch64.whl \
|
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.3/psycopg_c-3.2.3-cp312-cp312-linux_aarch64.whl \
|
||||||
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
|
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
|
||||||
&& echo "Patching whitenoise for compression speedup" \
|
|
||||||
&& curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \
|
|
||||||
&& patch -d /usr/local/lib/python3.12/site-packages --verbose -p2 < 484.patch \
|
|
||||||
&& rm 484.patch \
|
|
||||||
&& echo "Installing NLTK data" \
|
&& echo "Installing NLTK data" \
|
||||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||||
|
19
Pipfile
19
Pipfile
@@ -7,8 +7,8 @@ name = "pypi"
|
|||||||
dateparser = "~=1.2"
|
dateparser = "~=1.2"
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
django = "~=5.1.1"
|
django = "~=5.1.5"
|
||||||
django-allauth = {extras = ["socialaccount"], version = "*"}
|
django-allauth = {extras = ["mfa", "socialaccount"], version = "*"}
|
||||||
django-auditlog = "*"
|
django-auditlog = "*"
|
||||||
django-celery-results = "*"
|
django-celery-results = "*"
|
||||||
django-compression-middleware = "*"
|
django-compression-middleware = "*"
|
||||||
@@ -18,12 +18,12 @@ django-filter = "~=24.3"
|
|||||||
django-guardian = "*"
|
django-guardian = "*"
|
||||||
django-multiselectfield = "*"
|
django-multiselectfield = "*"
|
||||||
django-soft-delete = "*"
|
django-soft-delete = "*"
|
||||||
djangorestframework = "==3.15.2"
|
djangorestframework = "~=3.15.2"
|
||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
drf-writable-nested = "*"
|
drf-writable-nested = "*"
|
||||||
bleach = "*"
|
bleach = "*"
|
||||||
celery = {extras = ["redis"], version = "*"}
|
celery = {extras = ["redis"], version = "*"}
|
||||||
channels = "~=4.1"
|
channels = "~=4.2"
|
||||||
channels-redis = "*"
|
channels-redis = "*"
|
||||||
concurrent-log-handler = "*"
|
concurrent-log-handler = "*"
|
||||||
filelock = "*"
|
filelock = "*"
|
||||||
@@ -37,7 +37,7 @@ jinja2 = "~=3.1"
|
|||||||
langdetect = "*"
|
langdetect = "*"
|
||||||
mysqlclient = "*"
|
mysqlclient = "*"
|
||||||
nltk = "*"
|
nltk = "*"
|
||||||
ocrmypdf = "~=16.5"
|
ocrmypdf = "~=16.8"
|
||||||
pathvalidate = "*"
|
pathvalidate = "*"
|
||||||
pdf2image = "*"
|
pdf2image = "*"
|
||||||
psycopg = {version = "*", extras = ["c"]}
|
psycopg = {version = "*", extras = ["c"]}
|
||||||
@@ -49,23 +49,24 @@ python-magic = "*"
|
|||||||
pyzbar = "*"
|
pyzbar = "*"
|
||||||
rapidfuzz = "*"
|
rapidfuzz = "*"
|
||||||
redis = {extras = ["hiredis"], version = "*"}
|
redis = {extras = ["hiredis"], version = "*"}
|
||||||
scikit-learn = "~=1.5"
|
scikit-learn = "~=1.6"
|
||||||
setproctitle = "*"
|
setproctitle = "*"
|
||||||
tika-client = "*"
|
tika-client = "*"
|
||||||
tqdm = "*"
|
tqdm = "*"
|
||||||
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
|
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
|
||||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||||
watchdog = "~=4.0"
|
watchdog = "~=6.0"
|
||||||
whitenoise = "~=6.7"
|
whitenoise = "~=6.8"
|
||||||
whoosh = "~=2.7"
|
whoosh = "~=2.7"
|
||||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||||
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
# Linting
|
# Linting
|
||||||
pre-commit = "*"
|
pre-commit = "*"
|
||||||
ruff = "*"
|
ruff = "*"
|
||||||
# Testing
|
|
||||||
factory-boy = "*"
|
factory-boy = "*"
|
||||||
|
# Testing
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-cov = "*"
|
pytest-cov = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
|
3928
Pipfile.lock
generated
3928
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ A full list of [features](https://docs.paperless-ngx.com/#features) and [screens
|
|||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
The easiest way to deploy paperless is `docker compose`. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from GitHub Packages.
|
The easiest way to deploy paperless is `docker compose`. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from the GitHub container registry.
|
||||||
|
|
||||||
If you'd like to jump right in, you can configure a `docker compose` environment with our install script:
|
If you'd like to jump right in, you can configure a `docker compose` environment with our install script:
|
||||||
|
|
||||||
|
@@ -1,26 +1,17 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Paperless-ngx settings #
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# See http://docs.paperless-ngx.com/configuration/ for all available options.
|
||||||
|
|
||||||
# The UID and GID of the user used to run paperless in the container. Set this
|
# The UID and GID of the user used to run paperless in the container. Set this
|
||||||
# to your UID and GID on the host so that you have write access to the
|
# to your UID and GID on the host so that you have write access to the
|
||||||
# consumption directory.
|
# consumption directory.
|
||||||
#USERMAP_UID=1000
|
#USERMAP_UID=1000
|
||||||
#USERMAP_GID=1000
|
#USERMAP_GID=1000
|
||||||
|
|
||||||
# Additional languages to install for text recognition, separated by a
|
# See the documentation linked above for all options. A few commonly adjusted settings
|
||||||
# whitespace. Note that this is
|
# are provided below.
|
||||||
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
|
|
||||||
# language used for OCR.
|
|
||||||
# The container installs English, German, Italian, Spanish and French by
|
|
||||||
# default.
|
|
||||||
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
|
|
||||||
# for available languages.
|
|
||||||
#PAPERLESS_OCR_LANGUAGES=tur ces
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Paperless-specific settings #
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# All settings defined in the paperless.conf.example can be used here. The
|
|
||||||
# Docker setup does not use the configuration file.
|
|
||||||
# A few commonly adjusted settings are provided below.
|
|
||||||
|
|
||||||
# This is required if you will be exposing Paperless-ngx on a public domain
|
# This is required if you will be exposing Paperless-ngx on a public domain
|
||||||
# (if doing so please consider security measures such as reverse proxy)
|
# (if doing so please consider security measures such as reverse proxy)
|
||||||
@@ -30,13 +21,17 @@
|
|||||||
# be a very long sequence of random characters. You don't need to remember it.
|
# be a very long sequence of random characters. You don't need to remember it.
|
||||||
#PAPERLESS_SECRET_KEY=change-me
|
#PAPERLESS_SECRET_KEY=change-me
|
||||||
|
|
||||||
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
|
# Use this variable to set a timezone for the Paperless Docker containers. Defaults to UTC.
|
||||||
#PAPERLESS_TIME_ZONE=America/Los_Angeles
|
#PAPERLESS_TIME_ZONE=America/Los_Angeles
|
||||||
|
|
||||||
# The default language to use for OCR. Set this to the language most of your
|
# The default language to use for OCR. Set this to the language most of your
|
||||||
# documents are written in.
|
# documents are written in.
|
||||||
#PAPERLESS_OCR_LANGUAGE=eng
|
#PAPERLESS_OCR_LANGUAGE=eng
|
||||||
|
|
||||||
# Set if accessing paperless via a domain subpath e.g. https://domain.com/PATHPREFIX and using a reverse-proxy like traefik or nginx
|
# Additional languages to install for text recognition, separated by a whitespace.
|
||||||
#PAPERLESS_FORCE_SCRIPT_NAME=/PATHPREFIX
|
# Note that this is different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines
|
||||||
#PAPERLESS_STATIC_URL=/PATHPREFIX/static/ # trailing slash required
|
# the language used for OCR.
|
||||||
|
# The container installs English, German, Italian, Spanish and French by default.
|
||||||
|
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
|
||||||
|
# for available languages.
|
||||||
|
#PAPERLESS_OCR_LANGUAGES=tur ces
|
||||||
|
@@ -19,6 +19,8 @@
|
|||||||
#
|
#
|
||||||
# - Open portainer Stacks list and click 'Add stack'
|
# - Open portainer Stacks list and click 'Add stack'
|
||||||
# - Paste the contents of this file and assign a name, e.g. 'paperless'
|
# - Paste the contents of this file and assign a name, e.g. 'paperless'
|
||||||
|
# - 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
|
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||||
# - Open the list of containers, select paperless_webserver_1
|
# - Open the list of containers, select paperless_webserver_1
|
||||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||||
@@ -61,28 +63,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
# The UID and GID of the user used to run paperless in the container. Set this
|
env_file:
|
||||||
# to your UID and GID on the host so that you have write access to the
|
- stack.env
|
||||||
# consumption directory.
|
|
||||||
USERMAP_UID: 1000
|
|
||||||
USERMAP_GID: 100
|
|
||||||
# Additional languages to install for text recognition, separated by a
|
|
||||||
# whitespace. Note that this is
|
|
||||||
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
|
|
||||||
# language used for OCR.
|
|
||||||
# The container installs English, German, Italian, Spanish and French by
|
|
||||||
# default.
|
|
||||||
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
|
|
||||||
# for available languages.
|
|
||||||
#PAPERLESS_OCR_LANGUAGES: tur ces
|
|
||||||
# Adjust this key if you plan to make paperless available publicly. It should
|
|
||||||
# be a very long sequence of random characters. You don't need to remember it.
|
|
||||||
#PAPERLESS_SECRET_KEY: change-me
|
|
||||||
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
|
|
||||||
#PAPERLESS_TIME_ZONE: America/Los_Angeles
|
|
||||||
# The default language to use for OCR. Set this to the language most of your
|
|
||||||
# documents are written in.
|
|
||||||
#PAPERLESS_OCR_LANGUAGE: eng
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
|
@@ -16,7 +16,7 @@ do
|
|||||||
# Check if it starts with "PAPERLESS_" and ends in "_FILE"
|
# Check if it starts with "PAPERLESS_" and ends in "_FILE"
|
||||||
if [[ ${env_name} == PAPERLESS_*_FILE ]]; then
|
if [[ ${env_name} == PAPERLESS_*_FILE ]]; then
|
||||||
# This should have been named different..
|
# This should have been named different..
|
||||||
if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" ]]; then
|
if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${env_name} == "PAPERLESS_MODEL_FILE" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# Extract the value of the environment
|
# Extract the value of the environment
|
||||||
|
@@ -14,7 +14,9 @@ for command in decrypt_documents \
|
|||||||
document_thumbnails \
|
document_thumbnails \
|
||||||
document_sanity_checker \
|
document_sanity_checker \
|
||||||
document_fuzzy_match \
|
document_fuzzy_match \
|
||||||
manage_superuser;
|
manage_superuser \
|
||||||
|
convert_mariadb_uuid \
|
||||||
|
prune_audit_logs;
|
||||||
do
|
do
|
||||||
echo "installing $command..."
|
echo "installing $command..."
|
||||||
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
|
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
|
||||||
|
@@ -19,6 +19,8 @@ Options available to any installation of paperless:
|
|||||||
export. Therefore, incremental backups with `rsync` are entirely
|
export. Therefore, incremental backups with `rsync` are entirely
|
||||||
possible.
|
possible.
|
||||||
|
|
||||||
|
The exporter does not include API tokens and they will need to be re-generated after importing.
|
||||||
|
|
||||||
!!! caution
|
!!! caution
|
||||||
|
|
||||||
You cannot import the export generated with one version of paperless in
|
You cannot import the export generated with one version of paperless in
|
||||||
@@ -79,8 +81,8 @@ $ docker compose down
|
|||||||
1. If you pull the image from the docker hub, all you need to do is:
|
1. If you pull the image from the docker hub, all you need to do is:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ docker compose pull
|
docker compose pull
|
||||||
$ docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
The Docker Compose files refer to the `latest` version, which is
|
The Docker Compose files refer to the `latest` version, which is
|
||||||
@@ -89,9 +91,9 @@ $ docker compose down
|
|||||||
1. If you built the image yourself, do the following:
|
1. If you built the image yourself, do the following:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ git pull
|
git pull
|
||||||
$ docker compose build
|
docker compose build
|
||||||
$ docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
Running `docker compose up` will also apply any new database migrations.
|
Running `docker compose up` will also apply any new database migrations.
|
||||||
@@ -153,7 +155,7 @@ following:
|
|||||||
environment before that, if you use one.
|
environment before that, if you use one.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -166,8 +168,8 @@ following:
|
|||||||
3. Migrate the database.
|
3. Migrate the database.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ cd src
|
cd src
|
||||||
$ python3 manage.py migrate # (1)
|
python3 manage.py migrate # (1)
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Including `sudo -Hu <paperless_user>` may be required
|
1. Including `sudo -Hu <paperless_user>` may be required
|
||||||
@@ -239,6 +241,7 @@ document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z]
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-c, --compare-checksums
|
-c, --compare-checksums
|
||||||
|
-cj, --compare-json
|
||||||
-d, --delete
|
-d, --delete
|
||||||
-f, --use-filename-format
|
-f, --use-filename-format
|
||||||
-na, --no-archive
|
-na, --no-archive
|
||||||
@@ -267,7 +270,8 @@ only export changed and added files. Paperless determines whether a file
|
|||||||
has changed by inspecting the file attributes "date/time modified" and
|
has changed by inspecting the file attributes "date/time modified" and
|
||||||
"size". If that does not work out for you, specify `-c` or
|
"size". If that does not work out for you, specify `-c` or
|
||||||
`--compare-checksums` and paperless will attempt to compare file
|
`--compare-checksums` and paperless will attempt to compare file
|
||||||
checksums instead. This is slower.
|
checksums instead. This is slower. The manifest and metadata json files
|
||||||
|
are always updated, unless `cj` or `--compare-json` is specified.
|
||||||
|
|
||||||
Paperless will not remove any existing files in the export directory. If
|
Paperless will not remove any existing files in the export directory. If
|
||||||
you want paperless to also remove files that do not belong to the
|
you want paperless to also remove files that do not belong to the
|
||||||
@@ -620,3 +624,12 @@ document_fuzzy_match [--ratio] [--processes N]
|
|||||||
If providing the `--delete` option, it is highly recommended to have a backup.
|
If providing the `--delete` option, it is highly recommended to have a backup.
|
||||||
While every effort has been taken to ensure proper operation, there is always the
|
While every effort has been taken to ensure proper operation, there is always the
|
||||||
chance of deletion of a file you want to keep.
|
chance of deletion of a file you want to keep.
|
||||||
|
|
||||||
|
### Prune history (audit log) entries {#prune-history}
|
||||||
|
|
||||||
|
If the audit log is enabled Paperless-ngx keeps an audit log of all changes made to documents. Functionality to automatically remove entries for deleted documents was added but
|
||||||
|
entries created prior to this are not removed. This command allows you to prune the audit log of entries that are no longer needed.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
prune_audit_logs
|
||||||
|
```
|
||||||
|
@@ -25,20 +25,20 @@ documents.
|
|||||||
|
|
||||||
The following algorithms are available:
|
The following algorithms are available:
|
||||||
|
|
||||||
- **None:** No matching will be performed.
|
- **None:** No matching will be performed.
|
||||||
- **Any:** Looks for any occurrence of any word provided in match in
|
- **Any:** Looks for any occurrence of any word provided in match in
|
||||||
the PDF. If you define the match as `Bank1 Bank2`, it will match
|
the PDF. If you define the match as `Bank1 Bank2`, it will match
|
||||||
documents containing either of these terms.
|
documents containing either of these terms.
|
||||||
- **All:** Requires that every word provided appears in the PDF,
|
- **All:** Requires that every word provided appears in the PDF,
|
||||||
albeit not in the order provided.
|
albeit not in the order provided.
|
||||||
- **Exact:** Matches only if the match appears exactly as provided
|
- **Exact:** Matches only if the match appears exactly as provided
|
||||||
(i.e. preserve ordering) in the PDF.
|
(i.e. preserve ordering) in the PDF.
|
||||||
- **Regular expression:** Parses the match as a regular expression and
|
- **Regular expression:** Parses the match as a regular expression and
|
||||||
tries to find a match within the document.
|
tries to find a match within the document.
|
||||||
- **Fuzzy match:** Uses a partial matching based on locating the tag text
|
- **Fuzzy match:** Uses a partial matching based on locating the tag text
|
||||||
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
|
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
|
||||||
- **Auto:** Tries to automatically match new documents. This does not
|
- **Auto:** Tries to automatically match new documents. This does not
|
||||||
require you to set a match. See the [notes below](#automatic-matching).
|
require you to set a match. See the [notes below](#automatic-matching).
|
||||||
|
|
||||||
When using the _any_ or _all_ matching algorithms, you can search for
|
When using the _any_ or _all_ matching algorithms, you can search for
|
||||||
terms that consist of multiple words by enclosing them in double quotes.
|
terms that consist of multiple words by enclosing them in double quotes.
|
||||||
@@ -69,33 +69,33 @@ Paperless tries to hide much of the involved complexity with this
|
|||||||
approach. However, there are a couple caveats you need to keep in mind
|
approach. However, there are a couple caveats you need to keep in mind
|
||||||
when using this feature:
|
when using this feature:
|
||||||
|
|
||||||
- Changes to your documents are not immediately reflected by the
|
- Changes to your documents are not immediately reflected by the
|
||||||
matching algorithm. The neural network needs to be _trained_ on your
|
matching algorithm. The neural network needs to be _trained_ on your
|
||||||
documents after changes. Paperless periodically (default: once each
|
documents after changes. Paperless periodically (default: once each
|
||||||
hour) checks for changes and does this automatically for you.
|
hour) checks for changes and does this automatically for you.
|
||||||
- The Auto matching algorithm only takes documents into account which
|
- The Auto matching algorithm only takes documents into account which
|
||||||
are NOT placed in your inbox (i.e. have any inbox tags assigned to
|
are NOT placed in your inbox (i.e. have any inbox tags assigned to
|
||||||
them). This ensures that the neural network only learns from
|
them). This ensures that the neural network only learns from
|
||||||
documents which you have correctly tagged before.
|
documents which you have correctly tagged before.
|
||||||
- The matching algorithm can only work if there is a correlation
|
- The matching algorithm can only work if there is a correlation
|
||||||
between the tag, correspondent, document type, or storage path and
|
between the tag, correspondent, document type, or storage path and
|
||||||
the document itself. Your bank statements usually contain your bank
|
the document itself. Your bank statements usually contain your bank
|
||||||
account number and the name of the bank, so this works reasonably
|
account number and the name of the bank, so this works reasonably
|
||||||
well, However, tags such as "TODO" cannot be automatically
|
well, However, tags such as "TODO" cannot be automatically
|
||||||
assigned.
|
assigned.
|
||||||
- The matching algorithm needs a reasonable number of documents to
|
- The matching algorithm needs a reasonable number of documents to
|
||||||
identify when to assign tags, correspondents, storage paths, and
|
identify when to assign tags, correspondents, storage paths, and
|
||||||
types. If one out of a thousand documents has the correspondent
|
types. If one out of a thousand documents has the correspondent
|
||||||
"Very obscure web shop I bought something five years ago", it will
|
"Very obscure web shop I bought something five years ago", it will
|
||||||
probably not assign this correspondent automatically if you buy
|
probably not assign this correspondent automatically if you buy
|
||||||
something from them again. The more documents, the better.
|
something from them again. The more documents, the better.
|
||||||
- Paperless also needs a reasonable amount of negative examples to
|
- Paperless also needs a reasonable amount of negative examples to
|
||||||
decide when not to assign a certain tag, correspondent, document
|
decide when not to assign a certain tag, correspondent, document
|
||||||
type, or storage path. This will usually be the case as you start
|
type, or storage path. This will usually be the case as you start
|
||||||
filling up paperless with documents. Example: If all your documents
|
filling up paperless with documents. Example: If all your documents
|
||||||
are either from "Webshop" or "Bank", paperless will assign one
|
are either from "Webshop" or "Bank", paperless will assign one
|
||||||
of these correspondents to ANY new document, if both are set to
|
of these correspondents to ANY new document, if both are set to
|
||||||
automatic matching.
|
automatic matching.
|
||||||
|
|
||||||
## Hooking into the consumption process {#consume-hooks}
|
## Hooking into the consumption process {#consume-hooks}
|
||||||
|
|
||||||
@@ -242,12 +242,12 @@ webserver:
|
|||||||
|
|
||||||
Troubleshooting:
|
Troubleshooting:
|
||||||
|
|
||||||
- Monitor the Docker Compose log
|
- Monitor the Docker Compose log
|
||||||
`cd ~/paperless-ngx; docker compose logs -f`
|
`cd ~/paperless-ngx; docker compose logs -f`
|
||||||
- Check your script's permission e.g. in case of permission error
|
- Check your script's permission e.g. in case of permission error
|
||||||
`sudo chmod 755 post-consumption-example.sh`
|
`sudo chmod 755 post-consumption-example.sh`
|
||||||
- Pipe your scripts's output to a log file e.g.
|
- Pipe your scripts's output to a log file e.g.
|
||||||
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
|
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
|
||||||
|
|
||||||
## File name handling {#file-name-handling}
|
## File name handling {#file-name-handling}
|
||||||
|
|
||||||
@@ -302,35 +302,35 @@ will create a directory structure as follows:
|
|||||||
|
|
||||||
Paperless provides the following variables for use within filenames:
|
Paperless provides the following variables for use within filenames:
|
||||||
|
|
||||||
- `{{ asn }}`: The archive serial number of the document, or "none".
|
- `{{ asn }}`: The archive serial number of the document, or "none".
|
||||||
- `{{ correspondent }}`: The name of the correspondent, or "none".
|
- `{{ correspondent }}`: The name of the correspondent, or "none".
|
||||||
- `{{ document_type }}`: The name of the document type, or "none".
|
- `{{ document_type }}`: The name of the document type, or "none".
|
||||||
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
|
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
|
||||||
document.
|
document.
|
||||||
- `{{ title }}`: The title of the document.
|
- `{{ title }}`: The title of the document.
|
||||||
- `{{ created }}`: The full date (ISO format) the document was created.
|
- `{{ created }}`: The full date (ISO format) the document was created.
|
||||||
- `{{ created_year }}`: Year created only, formatted as the year with
|
- `{{ created_year }}`: Year created only, formatted as the year with
|
||||||
century.
|
century.
|
||||||
- `{{ created_year_short }}`: Year created only, formatted as the year
|
- `{{ created_year_short }}`: Year created only, formatted as the year
|
||||||
without century, zero padded.
|
without century, zero padded.
|
||||||
- `{{ created_month }}`: Month created only (number 01-12).
|
- `{{ created_month }}`: Month created only (number 01-12).
|
||||||
- `{{ created_month_name }}`: Month created name, as per locale
|
- `{{ created_month_name }}`: Month created name, as per locale
|
||||||
- `{{ created_month_name_short }}`: Month created abbreviated name, as per
|
- `{{ created_month_name_short }}`: Month created abbreviated name, as per
|
||||||
locale
|
locale
|
||||||
- `{{ created_day }}`: Day created only (number 01-31).
|
- `{{ created_day }}`: Day created only (number 01-31).
|
||||||
- `{{ added }}`: The full date (ISO format) the document was added to
|
- `{{ added }}`: The full date (ISO format) the document was added to
|
||||||
paperless.
|
paperless.
|
||||||
- `{{ added_year }}`: Year added only.
|
- `{{ added_year }}`: Year added only.
|
||||||
- `{{ added_year_short }}`: Year added only, formatted as the year without
|
- `{{ added_year_short }}`: Year added only, formatted as the year without
|
||||||
century, zero padded.
|
century, zero padded.
|
||||||
- `{{ added_month }}`: Month added only (number 01-12).
|
- `{{ added_month }}`: Month added only (number 01-12).
|
||||||
- `{{ added_month_name }}`: Month added name, as per locale
|
- `{{ added_month_name }}`: Month added name, as per locale
|
||||||
- `{{ added_month_name_short }}`: Month added abbreviated name, as per
|
- `{{ added_month_name_short }}`: Month added abbreviated name, as per
|
||||||
locale
|
locale
|
||||||
- `{{ added_day }}`: Day added only (number 01-31).
|
- `{{ added_day }}`: Day added only (number 01-31).
|
||||||
- `{{ owner_username }}`: Username of document owner, if any, or "none"
|
- `{{ owner_username }}`: Username of document owner, if any, or "none"
|
||||||
- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none"
|
- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none"
|
||||||
- `{{ doc_pk }}`: The paperless identifier (primary key) for the document.
|
- `{{ doc_pk }}`: The paperless identifier (primary key) for the document.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
@@ -381,10 +381,10 @@ before empty placeholders are removed as well, empty directories are omitted.
|
|||||||
When a single storage layout is not sufficient for your use case, storage paths allow for more complex
|
When a single storage layout is not sufficient for your use case, storage paths allow for more complex
|
||||||
structure to set precisely where each document is stored in the file system.
|
structure to set precisely where each document is stored in the file system.
|
||||||
|
|
||||||
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
|
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
|
||||||
follows the rules described above
|
follows the rules described above
|
||||||
- Each document is assigned a storage path using the matching algorithms described above, but can be
|
- Each document is assigned a storage path using the matching algorithms described above, but can be
|
||||||
overwritten at any time
|
overwritten at any time
|
||||||
|
|
||||||
For example, you could define the following two storage paths:
|
For example, you could define the following two storage paths:
|
||||||
|
|
||||||
@@ -435,8 +435,8 @@ with more complex logic.
|
|||||||
|
|
||||||
#### Additional Variables
|
#### Additional Variables
|
||||||
|
|
||||||
- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string
|
- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string
|
||||||
- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable.
|
- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
@@ -532,15 +532,15 @@ installation, you can use volumes to accomplish this:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
# ...
|
|
||||||
webserver:
|
|
||||||
environment:
|
|
||||||
- PAPERLESS_ENABLE_FLOWER
|
|
||||||
ports:
|
|
||||||
- 5555:5555 # (2)!
|
|
||||||
# ...
|
# ...
|
||||||
volumes:
|
webserver:
|
||||||
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
|
environment:
|
||||||
|
- PAPERLESS_ENABLE_FLOWER
|
||||||
|
ports:
|
||||||
|
- 5555:5555 # (2)!
|
||||||
|
# ...
|
||||||
|
volumes:
|
||||||
|
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Note the `:ro` tag means the file will be mounted as read only.
|
1. Note the `:ro` tag means the file will be mounted as read only.
|
||||||
@@ -571,11 +571,11 @@ For example, using Docker Compose:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
# ...
|
|
||||||
webserver:
|
|
||||||
# ...
|
# ...
|
||||||
volumes:
|
webserver:
|
||||||
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
|
# ...
|
||||||
|
volumes:
|
||||||
|
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Note the `:ro` tag means the folder will be mounted as read only. This is for extra security against changes
|
1. Note the `:ro` tag means the folder will be mounted as read only. This is for extra security against changes
|
||||||
@@ -585,10 +585,13 @@ services:
|
|||||||
### Case Sensitivity
|
### Case Sensitivity
|
||||||
|
|
||||||
The database interface does not provide a method to configure a MySQL
|
The database interface does not provide a method to configure a MySQL
|
||||||
database to be case sensitive. This would prevent a user from creating a
|
database to be case-sensitive. A case-**in**sensitive database prevents a user from creating a
|
||||||
tag `Name` and `NAME` as they are considered the same.
|
tag `Name` and `NAME` as they are considered the same.
|
||||||
|
|
||||||
Per Django documentation, to enable this requires manual intervention.
|
However, there is a downside to turning on case sensitivity, as it also makes searches case-sensitive,
|
||||||
|
so for example a document with the title `Invoice` won't be found when searching for `invoice`.
|
||||||
|
|
||||||
|
Per Django documentation, making a database case-sensitive requires manual intervention.
|
||||||
To enable case sensitive tables, you can execute the following command
|
To enable case sensitive tables, you can execute the following command
|
||||||
against each table:
|
against each table:
|
||||||
|
|
||||||
@@ -605,6 +608,8 @@ existing tables) with:
|
|||||||
an older system may fix issues that can arise while setting up Paperless-ngx but
|
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||||
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||||
|
|
||||||
|
For more information on this topic, you can refer to [this](https://code.djangoproject.com/ticket/9682) Django issue.
|
||||||
|
|
||||||
### Missing timezones
|
### Missing timezones
|
||||||
|
|
||||||
MySQL as well as MariaDB do not have any timezone information by default (though some
|
MySQL as well as MariaDB do not have any timezone information by default (though some
|
||||||
@@ -623,16 +628,16 @@ Paperless is able to utilize barcodes for automatically performing some tasks.
|
|||||||
|
|
||||||
At this time, the library utilized for detection of barcodes supports the following types:
|
At this time, the library utilized for detection of barcodes supports the following types:
|
||||||
|
|
||||||
- AN-13/UPC-A
|
- AN-13/UPC-A
|
||||||
- UPC-E
|
- UPC-E
|
||||||
- EAN-8
|
- EAN-8
|
||||||
- Code 128
|
- Code 128
|
||||||
- Code 93
|
- Code 93
|
||||||
- Code 39
|
- Code 39
|
||||||
- Codabar
|
- Codabar
|
||||||
- Interleaved 2 of 5
|
- Interleaved 2 of 5
|
||||||
- QR Code
|
- QR Code
|
||||||
- SQ Code
|
- SQ Code
|
||||||
|
|
||||||
You may check for updates on the [zbar library homepage](https://github.com/mchehab/zbar).
|
You may check for updates on the [zbar library homepage](https://github.com/mchehab/zbar).
|
||||||
For usage in Paperless, the type of barcode does not matter, only the contents of it.
|
For usage in Paperless, the type of barcode does not matter, only the contents of it.
|
||||||
@@ -806,22 +811,22 @@ gpg --decrypt name_of_file.asc
|
|||||||
|
|
||||||
First, enable the [PAPERLESS_ENABLE_GPG_DECRYPTOR environment variable](configuration.md#PAPERLESS_ENABLE_GPG_DECRYPTOR).
|
First, enable the [PAPERLESS_ENABLE_GPG_DECRYPTOR environment variable](configuration.md#PAPERLESS_ENABLE_GPG_DECRYPTOR).
|
||||||
|
|
||||||
Then determine your local `gpg-agent.extra` socket by invoking
|
Then determine your local `gpg-agent` socket by invoking
|
||||||
|
|
||||||
```
|
```
|
||||||
gpgconf --list-dir agent-extra-socket
|
gpgconf --list-dir agent-socket
|
||||||
```
|
```
|
||||||
|
|
||||||
on your host. A possible output is `~/.gnupg/S.gpg-agent.extra`.
|
on your host. A possible output is `~/.gnupg/S.gpg-agent`.
|
||||||
Also find the location of your public keyring.
|
Also find the location of your public keyring.
|
||||||
|
|
||||||
If using docker, you'll need to add the following volume mounts to your `docker-compose.yml` file:
|
If using docker, you'll need to add the following volume mounts to your `docker-compose.yml` file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webserver:
|
webserver:
|
||||||
volumes:
|
volumes:
|
||||||
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
|
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
|
||||||
- <path to gpg-agent.extra socket>:/usr/src/paperless/.gnupg/S.gpg-agent
|
- <path to gpg-agent socket>:/usr/src/paperless/.gnupg/S.gpg-agent
|
||||||
```
|
```
|
||||||
|
|
||||||
For a 'bare-metal' installation no further configuration is necessary. If you
|
For a 'bare-metal' installation no further configuration is necessary. If you
|
||||||
@@ -829,9 +834,9 @@ want to use a separate `GNUPG_HOME`, you can do so by configuring the [PAPERLESS
|
|||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
- Make sure, that `gpg-agent` is running on your host machine
|
- Make sure, that `gpg-agent` is running on your host machine
|
||||||
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
|
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
|
||||||
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
|
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
paperless@9da1865df327:~/.gnupg$ ls -al
|
paperless@9da1865df327:~/.gnupg$ ls -al
|
||||||
|
349
docs/api.md
349
docs/api.md
@@ -8,23 +8,23 @@ most of the available filters and ordering fields.
|
|||||||
|
|
||||||
The API provides the following main endpoints:
|
The API provides the following main endpoints:
|
||||||
|
|
||||||
- `/api/correspondents/`: Full CRUD support.
|
- `/api/correspondents/`: Full CRUD support.
|
||||||
- `/api/custom_fields/`: Full CRUD support.
|
- `/api/custom_fields/`: Full CRUD support.
|
||||||
- `/api/documents/`: Full CRUD support, except POSTing new documents.
|
- `/api/documents/`: Full CRUD support, except POSTing new documents.
|
||||||
See [below](#file-uploads).
|
See [below](#file-uploads).
|
||||||
- `/api/document_types/`: Full CRUD support.
|
- `/api/document_types/`: Full CRUD support.
|
||||||
- `/api/groups/`: Full CRUD support.
|
- `/api/groups/`: Full CRUD support.
|
||||||
- `/api/logs/`: Read-Only.
|
- `/api/logs/`: Read-Only.
|
||||||
- `/api/mail_accounts/`: Full CRUD support.
|
- `/api/mail_accounts/`: Full CRUD support.
|
||||||
- `/api/mail_rules/`: Full CRUD support.
|
- `/api/mail_rules/`: Full CRUD support.
|
||||||
- `/api/profile/`: GET, PATCH
|
- `/api/profile/`: GET, PATCH
|
||||||
- `/api/share_links/`: Full CRUD support.
|
- `/api/share_links/`: Full CRUD support.
|
||||||
- `/api/storage_paths/`: Full CRUD support.
|
- `/api/storage_paths/`: Full CRUD support.
|
||||||
- `/api/tags/`: Full CRUD support.
|
- `/api/tags/`: Full CRUD support.
|
||||||
- `/api/tasks/`: Read-only.
|
- `/api/tasks/`: Read-only.
|
||||||
- `/api/users/`: Full CRUD support.
|
- `/api/users/`: Full CRUD support.
|
||||||
- `/api/workflows/`: Full CRUD support.
|
- `/api/workflows/`: Full CRUD support.
|
||||||
- `/api/search/` GET, see [below](#global-search).
|
- `/api/search/` GET, see [below](#global-search).
|
||||||
|
|
||||||
All of these endpoints except for the logging endpoint allow you to
|
All of these endpoints except for the logging endpoint allow you to
|
||||||
fetch (and edit and delete where appropriate) individual objects by
|
fetch (and edit and delete where appropriate) individual objects by
|
||||||
@@ -33,32 +33,32 @@ appending their primary key to the path, e.g. `/api/documents/454/`.
|
|||||||
The objects served by the document endpoint contain the following
|
The objects served by the document endpoint contain the following
|
||||||
fields:
|
fields:
|
||||||
|
|
||||||
- `id`: ID of the document. Read-only.
|
- `id`: ID of the document. Read-only.
|
||||||
- `title`: Title of the document.
|
- `title`: Title of the document.
|
||||||
- `content`: Plain text content of the document.
|
- `content`: Plain text content of the document.
|
||||||
- `tags`: List of IDs of tags assigned to this document, or empty
|
- `tags`: List of IDs of tags assigned to this document, or empty
|
||||||
list.
|
list.
|
||||||
- `document_type`: Document type of this document, or null.
|
- `document_type`: Document type of this document, or null.
|
||||||
- `correspondent`: Correspondent of this document or null.
|
- `correspondent`: Correspondent of this document or null.
|
||||||
- `created`: The date time at which this document was created.
|
- `created`: The date time at which this document was created.
|
||||||
- `created_date`: The date (YYYY-MM-DD) at which this document was
|
- `created_date`: The date (YYYY-MM-DD) at which this document was
|
||||||
created. Optional. If also passed with created, this is ignored.
|
created. Optional. If also passed with created, this is ignored.
|
||||||
- `modified`: The date at which this document was last edited in
|
- `modified`: The date at which this document was last edited in
|
||||||
paperless. Read-only.
|
paperless. Read-only.
|
||||||
- `added`: The date at which this document was added to paperless.
|
- `added`: The date at which this document was added to paperless.
|
||||||
Read-only.
|
Read-only.
|
||||||
- `archive_serial_number`: The identifier of this document in a
|
- `archive_serial_number`: The identifier of this document in a
|
||||||
physical document archive.
|
physical document archive.
|
||||||
- `original_file_name`: Verbose filename of the original document.
|
- `original_file_name`: Verbose filename of the original document.
|
||||||
Read-only.
|
Read-only.
|
||||||
- `archived_file_name`: Verbose filename of the archived document.
|
- `archived_file_name`: Verbose filename of the archived document.
|
||||||
Read-only. Null if no archived document is available.
|
Read-only. Null if no archived document is available.
|
||||||
- `notes`: Array of notes associated with the document.
|
- `notes`: Array of notes associated with the document.
|
||||||
- `page_count`: Number of pages.
|
- `page_count`: Number of pages.
|
||||||
- `set_permissions`: Allows setting document permissions. Optional,
|
- `set_permissions`: Allows setting document permissions. Optional,
|
||||||
write-only. See [below](#permissions).
|
write-only. See [below](#permissions).
|
||||||
- `custom_fields`: Array of custom fields & values, specified as
|
- `custom_fields`: Array of custom fields & values, specified as
|
||||||
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
|
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -69,11 +69,11 @@ fields:
|
|||||||
In addition to that, the document endpoint offers these additional
|
In addition to that, the document endpoint offers these additional
|
||||||
actions on individual documents:
|
actions on individual documents:
|
||||||
|
|
||||||
- `/api/documents/<pk>/download/`: Download the document.
|
- `/api/documents/<pk>/download/`: Download the document.
|
||||||
- `/api/documents/<pk>/preview/`: Display the document inline, without
|
- `/api/documents/<pk>/preview/`: Display the document inline, without
|
||||||
downloading it.
|
downloading it.
|
||||||
- `/api/documents/<pk>/thumb/`: Download the PNG thumbnail of a
|
- `/api/documents/<pk>/thumb/`: Download the PNG thumbnail of a
|
||||||
document.
|
document.
|
||||||
|
|
||||||
Paperless generates archived PDF/A documents from consumed files and
|
Paperless generates archived PDF/A documents from consumed files and
|
||||||
stores both the original files as well as the archived files. By
|
stores both the original files as well as the archived files. By
|
||||||
@@ -107,30 +107,30 @@ Access the metadata of a document with an ID `id` at
|
|||||||
|
|
||||||
The endpoint reports the following data:
|
The endpoint reports the following data:
|
||||||
|
|
||||||
- `original_checksum`: MD5 checksum of the original document.
|
- `original_checksum`: MD5 checksum of the original document.
|
||||||
- `original_size`: Size of the original document, in bytes.
|
- `original_size`: Size of the original document, in bytes.
|
||||||
- `original_mime_type`: Mime type of the original document.
|
- `original_mime_type`: Mime type of the original document.
|
||||||
- `media_filename`: Current filename of the document, under which it
|
- `media_filename`: Current filename of the document, under which it
|
||||||
is stored inside the media directory.
|
is stored inside the media directory.
|
||||||
- `has_archive_version`: True, if this document is archived, false
|
- `has_archive_version`: True, if this document is archived, false
|
||||||
otherwise.
|
otherwise.
|
||||||
- `original_metadata`: A list of metadata associated with the original
|
- `original_metadata`: A list of metadata associated with the original
|
||||||
document. See below.
|
document. See below.
|
||||||
- `archive_checksum`: MD5 checksum of the archived document, or null.
|
- `archive_checksum`: MD5 checksum of the archived document, or null.
|
||||||
- `archive_size`: Size of the archived document in bytes, or null.
|
- `archive_size`: Size of the archived document in bytes, or null.
|
||||||
- `archive_metadata`: Metadata associated with the archived document,
|
- `archive_metadata`: Metadata associated with the archived document,
|
||||||
or null. See below.
|
or null. See below.
|
||||||
|
|
||||||
File metadata is reported as a list of objects in the following form:
|
File metadata is reported as a list of objects in the following form:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"namespace": "http://ns.adobe.com/pdf/1.3/",
|
"namespace": "http://ns.adobe.com/pdf/1.3/",
|
||||||
"prefix": "pdf",
|
"prefix": "pdf",
|
||||||
"key": "Producer",
|
"key": "Producer",
|
||||||
"value": "SparklePDF, Fancy edition"
|
"value": "SparklePDF, Fancy edition"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -140,9 +140,9 @@ document. Paperless only reports PDF metadata at this point.
|
|||||||
|
|
||||||
## Documents additional endpoints
|
## Documents additional endpoints
|
||||||
|
|
||||||
- `/api/documents/<id>/notes/`: Retrieve notes for a document.
|
- `/api/documents/<id>/notes/`: Retrieve notes for a document.
|
||||||
- `/api/documents/<id>/share_links/`: Retrieve share links for a document.
|
- `/api/documents/<id>/share_links/`: Retrieve share links for a document.
|
||||||
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
|
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
|
||||||
|
|
||||||
## Authorization
|
## Authorization
|
||||||
|
|
||||||
@@ -228,10 +228,10 @@ Full text searching is available on the `/api/documents/` endpoint. Two
|
|||||||
specific query parameters cause the API to return full text search
|
specific query parameters cause the API to return full text search
|
||||||
results:
|
results:
|
||||||
|
|
||||||
- `/api/documents/?query=your%20search%20query`: Search for a document
|
- `/api/documents/?query=your%20search%20query`: Search for a document
|
||||||
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
|
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
|
||||||
- `/api/documents/?more_like_id=1234`: Search for documents similar to
|
- `/api/documents/?more_like_id=1234`: Search for documents similar to
|
||||||
the document with id 1234.
|
the document with id 1234.
|
||||||
|
|
||||||
Pagination works exactly the same as it does for normal requests on this
|
Pagination works exactly the same as it does for normal requests on this
|
||||||
endpoint.
|
endpoint.
|
||||||
@@ -268,12 +268,12 @@ attribute with various information about the search results:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `score` is an indication how well this document matches the query
|
- `score` is an indication how well this document matches the query
|
||||||
relative to the other search results.
|
relative to the other search results.
|
||||||
- `highlights` is an excerpt from the document content and highlights
|
- `highlights` is an excerpt from the document content and highlights
|
||||||
the search terms with `<span>` tags as shown above.
|
the search terms with `<span>` tags as shown above.
|
||||||
- `rank` is the index of the search results. The first result will
|
- `rank` is the index of the search results. The first result will
|
||||||
have rank 0.
|
have rank 0.
|
||||||
|
|
||||||
### Filtering by custom fields
|
### Filtering by custom fields
|
||||||
|
|
||||||
@@ -284,33 +284,33 @@ use cases:
|
|||||||
1. Documents with a custom field "due" (date) between Aug 1, 2024 and
|
1. Documents with a custom field "due" (date) between Aug 1, 2024 and
|
||||||
Sept 1, 2024 (inclusive):
|
Sept 1, 2024 (inclusive):
|
||||||
|
|
||||||
`?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]`
|
`?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]`
|
||||||
|
|
||||||
2. Documents with a custom field "customer" (text) that equals "bob"
|
2. Documents with a custom field "customer" (text) that equals "bob"
|
||||||
(case sensitive):
|
(case sensitive):
|
||||||
|
|
||||||
`?custom_field_query=["customer", "exact", "bob"]`
|
`?custom_field_query=["customer", "exact", "bob"]`
|
||||||
|
|
||||||
3. Documents with a custom field "answered" (boolean) set to `true`:
|
3. Documents with a custom field "answered" (boolean) set to `true`:
|
||||||
|
|
||||||
`?custom_field_query=["answered", "exact", true]`
|
`?custom_field_query=["answered", "exact", true]`
|
||||||
|
|
||||||
4. Documents with a custom field "favorite animal" (select) set to either
|
4. Documents with a custom field "favorite animal" (select) set to either
|
||||||
"cat" or "dog":
|
"cat" or "dog":
|
||||||
|
|
||||||
`?custom_field_query=["favorite animal", "in", ["cat", "dog"]]`
|
`?custom_field_query=["favorite animal", "in", ["cat", "dog"]]`
|
||||||
|
|
||||||
5. Documents with a custom field "address" (text) that is empty:
|
5. Documents with a custom field "address" (text) that is empty:
|
||||||
|
|
||||||
`?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]`
|
`?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]`
|
||||||
|
|
||||||
6. Documents that don't have a field called "foo":
|
6. Documents that don't have a field called "foo":
|
||||||
|
|
||||||
`?custom_field_query=["foo", "exists", false]`
|
`?custom_field_query=["foo", "exists", false]`
|
||||||
|
|
||||||
7. Documents that have document links "references" to both document 3 and 7:
|
7. Documents that have document links "references" to both document 3 and 7:
|
||||||
|
|
||||||
`?custom_field_query=["references", "contains", [3, 7]]`
|
`?custom_field_query=["references", "contains", [3, 7]]`
|
||||||
|
|
||||||
All field types support basic operations including `exact`, `in`, `isnull`,
|
All field types support basic operations including `exact`, `in`, `isnull`,
|
||||||
and `exists`. String, URL, and monetary fields support case-insensitive
|
and `exists`. String, URL, and monetary fields support case-insensitive
|
||||||
@@ -326,8 +326,8 @@ Get auto completions for a partial search term.
|
|||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
|
|
||||||
- `term`: The incomplete term.
|
- `term`: The incomplete term.
|
||||||
- `limit`: Amount of results. Defaults to 10.
|
- `limit`: Amount of results. Defaults to 10.
|
||||||
|
|
||||||
Results returned by the endpoint are ordered by importance of the term
|
Results returned by the endpoint are ordered by importance of the term
|
||||||
in the document index. The first result is the term that has the highest
|
in the document index. The first result is the term that has the highest
|
||||||
@@ -351,19 +351,23 @@ from there.
|
|||||||
|
|
||||||
The endpoint supports the following optional form fields:
|
The endpoint supports the following optional form fields:
|
||||||
|
|
||||||
- `title`: Specify a title that the consumer should use for the
|
- `title`: Specify a title that the consumer should use for the
|
||||||
document.
|
document.
|
||||||
- `created`: Specify a DateTime where the document was created (e.g.
|
- `created`: Specify a DateTime where the document was created (e.g.
|
||||||
"2016-04-19" or "2016-04-19 06:15:00+02:00").
|
"2016-04-19" or "2016-04-19 06:15:00+02:00").
|
||||||
- `correspondent`: Specify the ID of a correspondent that the consumer
|
- `correspondent`: Specify the ID of a correspondent that the consumer
|
||||||
should use for the document.
|
should use for the document.
|
||||||
- `document_type`: Similar to correspondent.
|
- `document_type`: Similar to correspondent.
|
||||||
- `storage_path`: Similar to correspondent.
|
- `storage_path`: Similar to correspondent.
|
||||||
- `tags`: Similar to correspondent. Specify this multiple times to
|
- `tags`: Similar to correspondent. Specify this multiple times to
|
||||||
have multiple tags added to the document.
|
have multiple tags added to the document.
|
||||||
- `archive_serial_number`: An optional archive serial number to set.
|
- `archive_serial_number`: An optional archive serial number to set.
|
||||||
- `custom_fields`: An array of custom field ids to assign (with an empty
|
- `custom_fields`: An array of custom field ids to assign (with an empty
|
||||||
value) to the document.
|
value) to the document.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
Sending a `Content-Length` header with correct size is mandatory.
|
||||||
|
|
||||||
The endpoint will immediately return HTTP 200 if the document consumption
|
The endpoint will immediately return HTTP 200 if the document consumption
|
||||||
process was started successfully, with the UUID of the consumption task
|
process was started successfully, with the UUID of the consumption task
|
||||||
@@ -429,50 +433,55 @@ a json payload of the format:
|
|||||||
|
|
||||||
The following methods are supported:
|
The following methods are supported:
|
||||||
|
|
||||||
- `set_correspondent`
|
- `set_correspondent`
|
||||||
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
|
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
|
||||||
- `set_document_type`
|
- `set_document_type`
|
||||||
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
|
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
|
||||||
- `set_storage_path`
|
- `set_storage_path`
|
||||||
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
|
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
|
||||||
- `add_tag`
|
- `add_tag`
|
||||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||||
- `remove_tag`
|
- `remove_tag`
|
||||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||||
- `modify_tags`
|
- `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 / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||||
- `delete`
|
- `delete`
|
||||||
- No `parameters` required
|
- No `parameters` required
|
||||||
- `reprocess`
|
- `reprocess`
|
||||||
- No `parameters` required
|
- No `parameters` required
|
||||||
- `set_permissions`
|
- `set_permissions`
|
||||||
- Requires `parameters`:
|
- Requires `parameters`:
|
||||||
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
||||||
- `"owner": OWNER_ID or null`
|
- `"owner": OWNER_ID or null`
|
||||||
- `"merge": true or false` (defaults to false)
|
- `"merge": true or false` (defaults to false)
|
||||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||||
removing them) or be merged with existing permissions.
|
removing them) or be merged with existing permissions.
|
||||||
- `merge`
|
- `merge`
|
||||||
- No additional `parameters` required.
|
- No additional `parameters` required.
|
||||||
- The ordering of the merged document is determined by the list of IDs.
|
- The ordering of the merged document is determined by the list of IDs.
|
||||||
- Optional `parameters`:
|
- Optional `parameters`:
|
||||||
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
||||||
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
|
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
|
||||||
all documents that are merged.
|
all documents that are merged.
|
||||||
- `split`
|
- `split`
|
||||||
- Requires `parameters`:
|
- Requires `parameters`:
|
||||||
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
|
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
|
||||||
- Optional `parameters`:
|
- Optional `parameters`:
|
||||||
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
|
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
|
||||||
the document.
|
the document.
|
||||||
- The split operation only accepts a single document.
|
- The split operation only accepts a single document.
|
||||||
- `rotate`
|
- `rotate`
|
||||||
- Requires `parameters`:
|
- Requires `parameters`:
|
||||||
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
|
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
|
||||||
- `delete_pages`
|
- `delete_pages`
|
||||||
- Requires `parameters`:
|
- Requires `parameters`:
|
||||||
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
|
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
|
||||||
- The delete_pages operation only accepts a single document.
|
- The delete_pages operation only accepts a single document.
|
||||||
|
- `modify_custom_fields`
|
||||||
|
- Requires `parameters`:
|
||||||
|
- `"add_custom_fields": { CUSTOM_FIELD_ID: VALUE }`: JSON object consisting of custom field id:value pairs to add to the document, can also be a list of custom field IDs
|
||||||
|
to add with empty values.
|
||||||
|
- `"remove_custom_fields": [CUSTOM_FIELD_ID]`: custom field ids to remove from the document.
|
||||||
|
|
||||||
### Objects
|
### Objects
|
||||||
|
|
||||||
@@ -494,16 +503,16 @@ operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json
|
|||||||
|
|
||||||
The REST API is versioned since Paperless-ngx 1.3.0.
|
The REST API is versioned since Paperless-ngx 1.3.0.
|
||||||
|
|
||||||
- Versioning ensures that changes to the API don't break older
|
- Versioning ensures that changes to the API don't break older
|
||||||
clients.
|
clients.
|
||||||
- Clients specify the specific version of the API they wish to use
|
- Clients specify the specific version of the API they wish to use
|
||||||
with every request and Paperless will handle the request using the
|
with every request and Paperless will handle the request using the
|
||||||
specified API version.
|
specified API version.
|
||||||
- Even if the underlying data model changes, older API versions will
|
- Even if the underlying data model changes, older API versions will
|
||||||
always serve compatible data.
|
always serve compatible data.
|
||||||
- If no version is specified, Paperless will serve version 1 to ensure
|
- If no version is specified, Paperless will serve version 1 to ensure
|
||||||
compatibility with older clients that do not request a specific API
|
compatibility with older clients that do not request a specific API
|
||||||
version.
|
version.
|
||||||
|
|
||||||
API versions are specified by submitting an additional HTTP `Accept`
|
API versions are specified by submitting an additional HTTP `Accept`
|
||||||
header with every request:
|
header with every request:
|
||||||
@@ -540,19 +549,27 @@ Initial API version.
|
|||||||
|
|
||||||
#### Version 2
|
#### Version 2
|
||||||
|
|
||||||
- Added field `Tag.color`. This read/write string field contains a hex
|
- Added field `Tag.color`. This read/write string field contains a hex
|
||||||
color such as `#a6cee3`.
|
color such as `#a6cee3`.
|
||||||
- Added read-only field `Tag.text_color`. This field contains the text
|
- Added read-only field `Tag.text_color`. This field contains the text
|
||||||
color to use for a specific tag, which is either black or white
|
color to use for a specific tag, which is either black or white
|
||||||
depending on the brightness of `Tag.color`.
|
depending on the brightness of `Tag.color`.
|
||||||
- Removed field `Tag.colour`.
|
- Removed field `Tag.colour`.
|
||||||
|
|
||||||
#### Version 3
|
#### Version 3
|
||||||
|
|
||||||
- Permissions endpoints have been added.
|
- Permissions endpoints have been added.
|
||||||
- The format of the `/api/ui_settings/` has changed.
|
- The format of the `/api/ui_settings/` has changed.
|
||||||
|
|
||||||
#### Version 4
|
#### Version 4
|
||||||
|
|
||||||
- Consumption templates were refactored to workflows and API endpoints
|
- Consumption templates were refactored to workflows and API endpoints
|
||||||
changed as such.
|
changed as such.
|
||||||
|
|
||||||
|
#### Version 5
|
||||||
|
|
||||||
|
- Added bulk deletion methods for documents and objects.
|
||||||
|
|
||||||
|
#### Version 6
|
||||||
|
|
||||||
|
- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`.
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
--md-hue: 222;
|
--md-hue: 222;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 400px) {
|
@media (min-width: 768px) {
|
||||||
.grid-left {
|
.grid-left {
|
||||||
width: 33%;
|
width: 33%;
|
||||||
float: left;
|
float: left;
|
||||||
|
8023
docs/changelog.md
8023
docs/changelog.md
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,17 @@ common [OCR](#ocr) related settings and some frontend settings. If set, these wi
|
|||||||
preference over the settings via environment variables. If not set, the environment setting
|
preference over the settings via environment variables. If not set, the environment setting
|
||||||
or applicable default will be utilized instead.
|
or applicable default will be utilized instead.
|
||||||
|
|
||||||
- If you run paperless on docker, `paperless.conf` is not used.
|
- If you run paperless on docker, `paperless.conf` is not used.
|
||||||
Rather, configure paperless by copying necessary options to
|
Rather, configure paperless by copying necessary options to
|
||||||
`docker-compose.env`.
|
`docker-compose.env`.
|
||||||
|
|
||||||
- If you are running paperless on anything else, paperless will search
|
- If you are running paperless on anything else, paperless will search
|
||||||
for the configuration file in these locations and use the first one
|
for the configuration file in these locations and use the first one
|
||||||
it finds:
|
it finds:
|
||||||
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
|
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
|
||||||
- `/path/to/paperless/paperless.conf`
|
- `/path/to/paperless/paperless.conf`
|
||||||
- `/etc/paperless.conf`
|
- `/etc/paperless.conf`
|
||||||
- `/usr/local/etc/paperless.conf`
|
- `/usr/local/etc/paperless.conf`
|
||||||
|
|
||||||
## Required services
|
## Required services
|
||||||
|
|
||||||
@@ -596,7 +596,7 @@ system. See the corresponding
|
|||||||
|
|
||||||
: Disables the regular frontend username / password login, i.e. once you have setup SSO. Note that this setting does not disable the Django admin login nor logging in with local credentials via the API. To prevent access to the Django admin, consider blocking `/admin/` in your [web server or reverse proxy configuration](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx).
|
: Disables the regular frontend username / password login, i.e. once you have setup SSO. Note that this setting does not disable the Django admin login nor logging in with local credentials via the API. To prevent access to the Django admin, consider blocking `/admin/` in your [web server or reverse proxy configuration](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx).
|
||||||
|
|
||||||
You can optionally also automatically redirect users to the SSO login with [PAPERLESS_REDIRECT_LOGIN_TO_SSO](#PAPERLESS_REDIRECT_LOGIN_TO_SSO)
|
You can optionally also automatically redirect users to the SSO login with [PAPERLESS_REDIRECT_LOGIN_TO_SSO](#PAPERLESS_REDIRECT_LOGIN_TO_SSO)
|
||||||
|
|
||||||
Defaults to False
|
Defaults to False
|
||||||
|
|
||||||
@@ -1217,6 +1217,10 @@ consumers working on the same file. Configure this to prevent that.
|
|||||||
|
|
||||||
Defaults to none (thus will use [PAPERLESS_URL](#PAPERLESS_URL)).
|
Defaults to none (thus will use [PAPERLESS_URL](#PAPERLESS_URL)).
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
This setting only applies to OAuth Email setup (not to the SSO setup).
|
||||||
|
|
||||||
#### [`PAPERLESS_GMAIL_OAUTH_CLIENT_ID=<str>`](#PAPERLESS_GMAIL_OAUTH_CLIENT_ID) {#PAPERLESS_GMAIL_OAUTH_CLIENT_ID}
|
#### [`PAPERLESS_GMAIL_OAUTH_CLIENT_ID=<str>`](#PAPERLESS_GMAIL_OAUTH_CLIENT_ID) {#PAPERLESS_GMAIL_OAUTH_CLIENT_ID}
|
||||||
|
|
||||||
: The OAuth client ID for Gmail. This is required for Gmail OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
|
: The OAuth client ID for Gmail. This is required for Gmail OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
|
||||||
@@ -1519,7 +1523,7 @@ one pod).
|
|||||||
actual user ID on the host system, which you can get by executing
|
actual user ID on the host system, which you can get by executing
|
||||||
|
|
||||||
``` shell-session
|
``` shell-session
|
||||||
$ id -u
|
id -u
|
||||||
```
|
```
|
||||||
|
|
||||||
Paperless will change ownership on its folders to this user, so you
|
Paperless will change ownership on its folders to this user, so you
|
||||||
@@ -1534,7 +1538,7 @@ actual user ID on the host system, which you can get by executing
|
|||||||
actual group ID on the host system, which you can get by executing
|
actual group ID on the host system, which you can get by executing
|
||||||
|
|
||||||
``` shell-session
|
``` shell-session
|
||||||
$ id -g
|
id -g
|
||||||
```
|
```
|
||||||
|
|
||||||
Paperless will change ownership on its folders to this group, so you
|
Paperless will change ownership on its folders to this group, so you
|
||||||
|
@@ -6,23 +6,23 @@ on Paperless-ngx.
|
|||||||
Check out the source from GitHub. The repository is organized in the
|
Check out the source from GitHub. The repository is organized in the
|
||||||
following way:
|
following way:
|
||||||
|
|
||||||
- `main` always represents the latest release and will only see
|
- `main` always represents the latest release and will only see
|
||||||
changes when a new release is made.
|
changes when a new release is made.
|
||||||
- `dev` contains the code that will be in the next release.
|
- `dev` contains the code that will be in the next release.
|
||||||
- `feature-X` contains bigger changes that will be in some release, but
|
- `feature-X` contains bigger changes that will be in some release, but
|
||||||
not necessarily the next one.
|
not necessarily the next one.
|
||||||
|
|
||||||
When making functional changes to Paperless-ngx, _always_ make your changes
|
When making functional changes to Paperless-ngx, _always_ make your changes
|
||||||
on the `dev` branch.
|
on the `dev` branch.
|
||||||
|
|
||||||
Apart from that, the folder structure is as follows:
|
Apart from that, the folder structure is as follows:
|
||||||
|
|
||||||
- `docs/` - Documentation.
|
- `docs/` - Documentation.
|
||||||
- `src-ui/` - Code of the front end.
|
- `src-ui/` - Code of the front end.
|
||||||
- `src/` - Code of the back end.
|
- `src/` - Code of the back end.
|
||||||
- `scripts/` - Various scripts that help with different parts of
|
- `scripts/` - Various scripts that help with different parts of
|
||||||
development.
|
development.
|
||||||
- `docker/` - Files required to build the docker image.
|
- `docker/` - Files required to build the docker image.
|
||||||
|
|
||||||
## Contributing to Paperless-ngx
|
## Contributing to Paperless-ngx
|
||||||
|
|
||||||
@@ -69,13 +69,13 @@ first-time setup.
|
|||||||
3. Create `consume` and `media` directories:
|
3. Create `consume` and `media` directories:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ mkdir -p consume media
|
mkdir -p consume media
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Install the Python dependencies:
|
4. Install the Python dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pipenv install --dev
|
pipenv install --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -85,7 +85,7 @@ first-time setup.
|
|||||||
5. Install pre-commit hooks:
|
5. Install pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pre-commit install
|
pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Apply migrations and create a superuser for your development instance:
|
6. Apply migrations and create a superuser for your development instance:
|
||||||
@@ -93,23 +93,23 @@ first-time setup.
|
|||||||
```bash
|
```bash
|
||||||
# src/
|
# src/
|
||||||
|
|
||||||
$ python3 manage.py migrate
|
python3 manage.py migrate
|
||||||
$ python3 manage.py createsuperuser
|
python3 manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
7. You can now either ...
|
7. You can now either ...
|
||||||
|
|
||||||
- install redis or
|
- install redis or
|
||||||
|
|
||||||
- use the included `scripts/start_services.sh` to use docker to fire
|
- use the included `scripts/start_services.sh` to use docker to fire
|
||||||
up a redis instance (and some other services such as tika,
|
up a redis instance (and some other services such as tika,
|
||||||
gotenberg and a database server) or
|
gotenberg and a database server) or
|
||||||
|
|
||||||
- spin up a bare redis container
|
- spin up a bare redis container
|
||||||
|
|
||||||
```
|
```
|
||||||
$ docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Continue with either back-end or front-end development – or both :-).
|
8. Continue with either back-end or front-end development – or both :-).
|
||||||
|
|
||||||
@@ -122,9 +122,9 @@ work well for development, but you can use whatever you want.
|
|||||||
Configure the IDE to use the `src/`-folder as the base source folder.
|
Configure the IDE to use the `src/`-folder as the base source folder.
|
||||||
Configure the following launch configurations in your IDE:
|
Configure the following launch configurations in your IDE:
|
||||||
|
|
||||||
- `python3 manage.py runserver`
|
- `python3 manage.py runserver`
|
||||||
- `python3 manage.py document_consumer`
|
- `python3 manage.py document_consumer`
|
||||||
- `celery --app paperless worker -l DEBUG` (or any other log level)
|
- `celery --app paperless worker -l DEBUG` (or any other log level)
|
||||||
|
|
||||||
To start them all:
|
To start them all:
|
||||||
|
|
||||||
@@ -150,11 +150,11 @@ $ ng build --configuration production
|
|||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- Run `pytest` in the `src/` directory to execute all tests. This also
|
- Run `pytest` in the `src/` directory to execute all tests. This also
|
||||||
generates a HTML coverage report. When runnings test, `paperless.conf`
|
generates a HTML coverage report. When runnings test, `paperless.conf`
|
||||||
is loaded as well. However, the tests rely on the default
|
is loaded as well. However, the tests rely on the default
|
||||||
configuration. This is not ideal. But for now, make sure no settings
|
configuration. This is not ideal. But for now, make sure no settings
|
||||||
except for DEBUG are overridden when testing.
|
except for DEBUG are overridden when testing.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
|||||||
1. Install the Angular CLI. You might need sudo privileges to perform this command:
|
1. Install the Angular CLI. You might need sudo privileges to perform this command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm install -g @angular/cli
|
npm install -g @angular/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Make sure that it's on your path.
|
2. Make sure that it's on your path.
|
||||||
@@ -184,13 +184,13 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
|||||||
3. Install all necessary modules:
|
3. Install all necessary modules:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
4. You can launch a development server by running:
|
4. You can launch a development server by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ng serve
|
ng serve
|
||||||
```
|
```
|
||||||
|
|
||||||
This will automatically update whenever you save. However, in-place
|
This will automatically update whenever you save. However, in-place
|
||||||
@@ -245,14 +245,14 @@ these parts have to be translated separately.
|
|||||||
|
|
||||||
### Front end localization
|
### Front end localization
|
||||||
|
|
||||||
- The AngularJS front end does localization according to the [Angular
|
- The AngularJS front end does localization according to the [Angular
|
||||||
documentation](https://angular.io/guide/i18n).
|
documentation](https://angular.io/guide/i18n).
|
||||||
- The source language of the project is "en_US".
|
- The source language of the project is "en_US".
|
||||||
- The source strings end up in the file `src-ui/messages.xlf`.
|
- The source strings end up in the file `src-ui/messages.xlf`.
|
||||||
- The translated strings need to be placed in the
|
- The translated strings need to be placed in the
|
||||||
`src-ui/src/locale/` folder.
|
`src-ui/src/locale/` folder.
|
||||||
- In order to extract added or changed strings from the source files,
|
- In order to extract added or changed strings from the source files,
|
||||||
call `ng extract-i18n`.
|
call `ng extract-i18n`.
|
||||||
|
|
||||||
Adding new languages requires adding the translated files in the
|
Adding new languages requires adding the translated files in the
|
||||||
`src-ui/src/locale/` folder and adjusting a couple files.
|
`src-ui/src/locale/` folder and adjusting a couple files.
|
||||||
@@ -298,18 +298,18 @@ A majority of the strings that appear in the back end appear only when
|
|||||||
the admin is used. However, some of these are still shown on the front
|
the admin is used. However, some of these are still shown on the front
|
||||||
end (such as error messages).
|
end (such as error messages).
|
||||||
|
|
||||||
- The django application does localization according to the [Django
|
- The django application does localization according to the [Django
|
||||||
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
|
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
|
||||||
- The source language of the project is "en_US".
|
- The source language of the project is "en_US".
|
||||||
- Localization files end up in the folder `src/locale/`.
|
- Localization files end up in the folder `src/locale/`.
|
||||||
- In order to extract strings from the application, call
|
- In order to extract strings from the application, call
|
||||||
`python3 manage.py makemessages -l en_US`. This is important after
|
`python3 manage.py makemessages -l en_US`. This is important after
|
||||||
making changes to translatable strings.
|
making changes to translatable strings.
|
||||||
- The message files need to be compiled for them to show up in the
|
- The message files need to be compiled for them to show up in the
|
||||||
application. Call `python3 manage.py compilemessages` to do this.
|
application. Call `python3 manage.py compilemessages` to do this.
|
||||||
The generated files don't get committed into git, since these are
|
The generated files don't get committed into git, since these are
|
||||||
derived artifacts. The build pipeline takes care of executing this
|
derived artifacts. The build pipeline takes care of executing this
|
||||||
command.
|
command.
|
||||||
|
|
||||||
Adding new languages requires adding the translated files in the
|
Adding new languages requires adding the translated files in the
|
||||||
`src/locale/`-folder and adjusting the file
|
`src/locale/`-folder and adjusting the file
|
||||||
@@ -335,13 +335,13 @@ If you want to build the documentation locally, this is how you do it:
|
|||||||
1. Have an active pipenv shell (`pipenv shell`) and install Python dependencies:
|
1. Have an active pipenv shell (`pipenv shell`) and install Python dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pipenv install --dev
|
pipenv install --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Build the documentation
|
2. Build the documentation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ mkdocs build --config-file mkdocs.yml
|
mkdocs build --config-file mkdocs.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
_alternatively..._
|
_alternatively..._
|
||||||
@@ -352,7 +352,7 @@ If you want to build the documentation locally, this is how you do it:
|
|||||||
something.
|
something.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ mkdocs serve
|
mkdocs serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the Docker image
|
## Building the Docker image
|
||||||
@@ -378,10 +378,10 @@ base code.
|
|||||||
Paperless-ngx uses parsers to add documents. A parser is
|
Paperless-ngx uses parsers to add documents. A parser is
|
||||||
responsible for:
|
responsible for:
|
||||||
|
|
||||||
- Retrieving the content from the original
|
- Retrieving the content from the original
|
||||||
- Creating a thumbnail
|
- Creating a thumbnail
|
||||||
- _optional:_ Retrieving a created date from the original
|
- _optional:_ Retrieving a created date from the original
|
||||||
- _optional:_ Creating an archived document from the original
|
- _optional:_ Creating an archived document from the original
|
||||||
|
|
||||||
Custom parsers can be added to Paperless-ngx to support more file types. In
|
Custom parsers can be added to Paperless-ngx to support more file types. In
|
||||||
order to do that, you need to write the parser itself and announce its
|
order to do that, you need to write the parser itself and announce its
|
||||||
@@ -439,14 +439,37 @@ def myparser_consumer_declaration(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `parser` is a reference to a class that extends `DocumentParser`.
|
- `parser` is a reference to a class that extends `DocumentParser`.
|
||||||
- `weight` is used whenever two or more parsers are able to parse a
|
- `weight` is used whenever two or more parsers are able to parse a
|
||||||
file: The parser with the higher weight wins. This can be used to
|
file: The parser with the higher weight wins. This can be used to
|
||||||
override the parsers provided by Paperless-ngx.
|
override the parsers provided by Paperless-ngx.
|
||||||
- `mime_types` is a dictionary. The keys are the mime types your
|
- `mime_types` is a dictionary. The keys are the mime types your
|
||||||
parser supports and the value is the default file extension that
|
parser supports and the value is the default file extension that
|
||||||
Paperless-ngx should use when storing files and serving them for
|
Paperless-ngx should use when storing files and serving them for
|
||||||
download. We could guess that from the file extensions, but some
|
download. We could guess that from the file extensions, but some
|
||||||
mime types have many extensions associated with them and the Python
|
mime types have many extensions associated with them and the Python
|
||||||
methods responsible for guessing the extension do not always return
|
methods responsible for guessing the extension do not always return
|
||||||
the same value.
|
the same value.
|
||||||
|
|
||||||
|
## Using Visual Studio Code devcontainer
|
||||||
|
|
||||||
|
Another easy way to get started with development is to use Visual Studio
|
||||||
|
Code devcontainers. This approach will create a preconfigured development
|
||||||
|
environment with all of the required tools and dependencies.
|
||||||
|
[Learn more about devcontainers](https://code.visualstudio.com/docs/devcontainers/containers).
|
||||||
|
The .devcontainer/vscode/tasks.json and .devcontainer/vscode/launch.json files
|
||||||
|
contain more information about the specific tasks and launch configurations (see the
|
||||||
|
non-standard "description" field).
|
||||||
|
|
||||||
|
To get started:
|
||||||
|
|
||||||
|
1. Clone the repository on your machine and open the Paperless-ngx folder in VS Code.
|
||||||
|
|
||||||
|
2. VS Code will prompt you with "Reopen in container". Do so and wait for the environment to start.
|
||||||
|
|
||||||
|
3. Initialize the project by running the task **Project Setup: Run all Init Tasks**. This
|
||||||
|
will initialize the database tables and create a superuser. Then you can compile the front end
|
||||||
|
for production or run the frontend in debug mode.
|
||||||
|
|
||||||
|
4. The project is ready for debugging, start either run the fullstack debug or individual debug
|
||||||
|
processes. Yo spin up the project without debugging run the task **Project Start: Run all Services**
|
||||||
|
44
docs/faq.md
44
docs/faq.md
@@ -40,28 +40,28 @@ system. On Linux, chances are high that this location is
|
|||||||
You can always drag those files out of that folder to use them
|
You can always drag those files out of that folder to use them
|
||||||
elsewhere. Here are a couple notes about that.
|
elsewhere. Here are a couple notes about that.
|
||||||
|
|
||||||
- Paperless-ngx never modifies your original documents. It keeps
|
- Paperless-ngx never modifies your original documents. It keeps
|
||||||
checksums of all documents and uses a scheduled sanity checker to
|
checksums of all documents and uses a scheduled sanity checker to
|
||||||
check that they remain the same.
|
check that they remain the same.
|
||||||
- By default, paperless uses the internal ID of each document as its
|
- By default, paperless uses the internal ID of each document as its
|
||||||
filename. This might not be very convenient for export. However, you
|
filename. This might not be very convenient for export. However, you
|
||||||
can adjust the way files are stored in paperless by
|
can adjust the way files are stored in paperless by
|
||||||
[configuring the filename format](advanced_usage.md#file-name-handling).
|
[configuring the filename format](advanced_usage.md#file-name-handling).
|
||||||
- [The exporter](administration.md#exporter) is
|
- [The exporter](administration.md#exporter) is
|
||||||
another easy way to get your files out of paperless with reasonable
|
another easy way to get your files out of paperless with reasonable
|
||||||
file names.
|
file names.
|
||||||
|
|
||||||
## _What file types does paperless-ngx support?_
|
## _What file types does paperless-ngx support?_
|
||||||
|
|
||||||
**A:** Currently, the following files are supported:
|
**A:** Currently, the following files are supported:
|
||||||
|
|
||||||
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
|
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
|
||||||
WebP images are processed with OCR and converted into PDF documents.
|
WebP images are processed with OCR and converted into PDF documents.
|
||||||
- Plain text documents are supported as well and are added verbatim to
|
- Plain text documents are supported as well and are added verbatim to
|
||||||
paperless.
|
paperless.
|
||||||
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
|
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
|
||||||
Paperless also supports various Office documents (.docx, .doc, odt,
|
Paperless also supports various Office documents (.docx, .doc, odt,
|
||||||
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
||||||
|
|
||||||
Paperless-ngx determines the type of a file by inspecting its content.
|
Paperless-ngx determines the type of a file by inspecting its content.
|
||||||
The file extensions do not matter.
|
The file extensions do not matter.
|
||||||
@@ -127,11 +127,11 @@ ASGI-enabled web server as well that processes WebSocket connections,
|
|||||||
and configure Apache to redirect WebSocket connections to this server.
|
and configure Apache to redirect WebSocket connections to this server.
|
||||||
Multiple options for ASGI servers exist:
|
Multiple options for ASGI servers exist:
|
||||||
|
|
||||||
- `gunicorn` with `uvicorn` as the worker implementation (the default
|
- `gunicorn` with `uvicorn` as the worker implementation (the default
|
||||||
of paperless)
|
of paperless)
|
||||||
- `daphne` as a standalone server, which is the reference
|
- `daphne` as a standalone server, which is the reference
|
||||||
implementation for ASGI.
|
implementation for ASGI.
|
||||||
- `uvicorn` as a standalone server
|
- `uvicorn` as a standalone server
|
||||||
|
|
||||||
## _What about the Redis licensing change and using one of the open source forks_?
|
## _What about the Redis licensing change and using one of the open source forks_?
|
||||||
|
|
||||||
|
368
docs/setup.md
368
docs/setup.md
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
You can go multiple routes to setup and run Paperless:
|
You can go multiple routes to setup and run Paperless:
|
||||||
|
|
||||||
- [Use the easy install docker script](#docker_script)
|
- [Use the script to setup a Docker install](#docker_script)
|
||||||
- [Pull the image from Docker Hub](#docker_hub)
|
- [Use the Docker compose templates](#docker)
|
||||||
- [Build the Docker image yourself](#docker_build)
|
- [Build the Docker image yourself](#docker_build)
|
||||||
- [Install Paperless directly on your system manually (bare metal)](#bare_metal)
|
- [Install Paperless-ngx directly on your system manually ("bare metal")](#bare_metal)
|
||||||
- A user-maintained list of commercial hosting providers can be found [in the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects)
|
- A user-maintained list of commercial hosting providers can be found [in the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects)
|
||||||
|
|
||||||
The Docker routes are quick & easy. These are the recommended routes.
|
The Docker routes are quick & easy. These are the recommended routes.
|
||||||
This configures all the stuff from the above automatically so that it
|
This configures all the stuff from the above automatically so that it
|
||||||
@@ -18,24 +18,19 @@ The bare metal route is complicated to setup but makes it easier should
|
|||||||
you want to contribute some code back. You need to configure and run the
|
you want to contribute some code back. You need to configure and run the
|
||||||
above mentioned components yourself.
|
above mentioned components yourself.
|
||||||
|
|
||||||
### Docker using the Installation Script {#docker_script}
|
### Use the Installation Script {#docker_script}
|
||||||
|
|
||||||
Paperless provides an interactive installation script. This script will
|
Paperless provides an interactive installation script to setup a Docker Compose
|
||||||
ask you for a couple configuration options, download and create the
|
installation. The script asks for a couple configuration options, and will then create the
|
||||||
necessary configuration files, pull the docker image, start paperless
|
necessary configuration files, pull the docker image, start Paperless-ngx and create your superuser
|
||||||
and create your user account. This script essentially performs all the
|
account. The script essentially automatically performs the steps described in [Docker setup](#docker).
|
||||||
steps described in [Docker setup](#docker_hub) automatically.
|
|
||||||
|
|
||||||
1. Make sure that Docker and Docker Compose are installed.
|
1. Make sure that Docker and Docker Compose are [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
||||||
|
|
||||||
!!! tip
|
|
||||||
|
|
||||||
See the Docker installation instructions at https://docs.docker.com/engine/install/
|
|
||||||
|
|
||||||
2. Download and run the installation script:
|
2. Download and run the installation script:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -43,83 +38,46 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
macOS users will need to install e.g. [gnu-sed](https://formulae.brew.sh/formula/gnu-sed) with support
|
macOS users will need to install e.g. [gnu-sed](https://formulae.brew.sh/formula/gnu-sed) with support
|
||||||
for running as `sed`.
|
for running as `sed`.
|
||||||
|
|
||||||
### From GHCR / Docker Hub {#docker_hub}
|
### Use Docker Compose {#docker}
|
||||||
|
|
||||||
1. Login with your user and create a folder in your home-directory to have a place for your
|
1. Make sure that Docker and Docker Compose are [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
||||||
configuration files and consumption directory.
|
|
||||||
|
|
||||||
```shell-session
|
|
||||||
$ mkdir -v ~/paperless-ngx
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Go to the [/docker/compose directory on the project
|
2. Go to the [/docker/compose directory on the project
|
||||||
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose){:target="\_blank"}
|
||||||
and download one of the `docker-compose.*.yml` files,
|
and download one of the `docker-compose.*.yml` files, depending on which database backend
|
||||||
depending on which database backend you want to use. Rename this
|
you want to use. Place the files in a local directory and rename it `docker-compose.yml`. Download the
|
||||||
file to `docker-compose.yml`. If you want to enable
|
`docker-compose.env` file and the `.env` file as well in the same directory.
|
||||||
optional support for Office documents, download a file with
|
|
||||||
`-tika` in the file name. Download the
|
If you want to enable optional support for Office and other documents, download a
|
||||||
`docker-compose.env` file and the `.env` file as well and store them
|
file with `-tika` in the file name.
|
||||||
in the same directory.
|
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
For new installations, it is recommended to use PostgreSQL as the
|
For new installations, it is recommended to use PostgreSQL as the
|
||||||
database backend.
|
database backend.
|
||||||
|
|
||||||
3. Install [Docker](https://docs.docker.com/engine/install/) and
|
3. Modify `docker-compose.yml` as needed. For example, you may want to change the paths to the
|
||||||
[Docker Compose](https://docs.docker.com/compose/install/).
|
consumption, media etc. directories to use 'bind mounts'.
|
||||||
|
Find the line that specifies where to mount the directory, e.g.:
|
||||||
!!! warning
|
|
||||||
|
|
||||||
If you want to use the included `docker-compose.*.yml` file, you
|
|
||||||
need to have at least Docker version **17.09.0** and Docker Compose
|
|
||||||
version **v2**. To check do: `docker compose version` or `docker -v`
|
|
||||||
|
|
||||||
See the [Docker installation guide](https://docs.docker.com/engine/install/) on how to install the current
|
|
||||||
version of Docker for your operating system or Linux distribution of
|
|
||||||
choice. To get the latest version of Docker Compose, follow the
|
|
||||||
[Docker Compose installation guide](https://docs.docker.com/compose/install/linux/) if your package repository
|
|
||||||
doesn't include it.
|
|
||||||
|
|
||||||
4. Modify `docker-compose.yml` to your preferences. You may want to
|
|
||||||
change the path to the consumption directory. Find the line that
|
|
||||||
specifies where to mount the consumption directory:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- ./consume:/usr/src/paperless/consume
|
- ./consume:/usr/src/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the part BEFORE the colon with a local directory of your
|
Replace the part _before_ the colon with a local directory of your choice:
|
||||||
choice:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
Don't change the part after the colon or paperless won't find your
|
You may also want to change the default port that the webserver will
|
||||||
documents.
|
use from the default (8000) to something else, e.g. for port 8010:
|
||||||
|
|
||||||
You may also need to change the default port that the webserver will
|
|
||||||
use from the default (8000):
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8010:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the part BEFORE the colon with a port of your choice:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ports:
|
|
||||||
- 8010:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
Don't change the part after the colon or edit other lines that
|
|
||||||
refer to port 8000. Modifying the part before the colon will map
|
|
||||||
requests on another port to the webserver running on the default
|
|
||||||
port.
|
|
||||||
|
|
||||||
**Rootless**
|
**Rootless**
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
@@ -129,11 +87,11 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
If you want to run Paperless as a rootless container, you will need
|
If you want to run Paperless as a rootless container, you will need
|
||||||
to do the following in your `docker-compose.yml`:
|
to do the following in your `docker-compose.yml`:
|
||||||
|
|
||||||
- set the `user` running the container to map to the `paperless`
|
- set the `user` running the container to map to the `paperless`
|
||||||
user in the container. This value (`user_id` below), should be
|
user in the container. This value (`user_id` below), should be
|
||||||
the same id that `USERMAP_UID` and `USERMAP_GID` are set to in
|
the same id that `USERMAP_UID` and `USERMAP_GID` are set to in
|
||||||
the next step. See `USERMAP_UID` and `USERMAP_GID`
|
the next step. See `USERMAP_UID` and `USERMAP_GID`
|
||||||
[here](configuration.md#docker).
|
[here](configuration.md#docker).
|
||||||
|
|
||||||
Your entry for Paperless should contain something like:
|
Your entry for Paperless should contain something like:
|
||||||
|
|
||||||
@@ -143,21 +101,16 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
> user: <user_id>
|
> user: <user_id>
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
5. Modify `docker-compose.env`, following the comments in the file. The
|
4. Modify `docker-compose.env` with any configuration options you'd like.
|
||||||
most important change is to set `USERMAP_UID` and `USERMAP_GID` to
|
See the [configuration documentation](configuration.md) for all options.
|
||||||
the uid and gid of your user on the host system. Use `id -u` and
|
|
||||||
`id -g` to get these.
|
|
||||||
|
|
||||||
This ensures that both the docker container and you on the host
|
You may also need to set `USERMAP_UID` and `USERMAP_GID` to
|
||||||
machine have write access to the consumption directory. If your UID
|
the uid and gid of your user on the host system. Use `id -u` and
|
||||||
|
`id -g` to get these. This ensures that both the container and the host
|
||||||
|
user have write access to the consumption directory. If your UID
|
||||||
and GID on the host system is 1000 (the default for the first normal
|
and GID on the host system is 1000 (the default for the first normal
|
||||||
user on most systems), it will work out of the box without any
|
user on most systems), it will work out of the box without any
|
||||||
modifications. `id "username"` to check.
|
modifications. Run `id "username"` to check.
|
||||||
|
|
||||||
!!! note
|
|
||||||
|
|
||||||
You can copy any setting from the file `paperless.conf.example` and
|
|
||||||
paste it here. Have a look at [configuration](configuration.md) to see what's available.
|
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -174,33 +127,30 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
[`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See
|
[`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See
|
||||||
[here](configuration.md#polling).
|
[here](configuration.md#polling).
|
||||||
|
|
||||||
6. Run `docker compose pull`. This will pull the image.
|
5. Run `docker compose pull`. This will pull the image from the GitHub container registry
|
||||||
|
by default but you can change the image to pull from Docker Hub by changing the `image`
|
||||||
|
line to `image: paperlessngx/paperless-ngx:latest`.
|
||||||
|
|
||||||
7. To be able to login, you will need a super user. To create it,
|
6. To be able to login, you will need a "superuser". To create it,
|
||||||
execute the following command:
|
execute the following command:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ docker compose run --rm webserver createsuperuser
|
docker compose run --rm webserver createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
or using docker exec from within the container:
|
or using docker exec from within the container:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ python3 manage.py createsuperuser
|
python3 manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
This will prompt you to set a username, an optional e-mail address
|
This will guide you through the superuser setup.
|
||||||
and finally a password (at least 8 characters).
|
|
||||||
|
|
||||||
8. Run `docker compose up -d`. This will create and start the necessary containers.
|
7. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||||
|
|
||||||
9. The default `docker-compose.yml` exports the webserver on your local
|
8. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
||||||
port
|
(or similar, depending on your configuration). Use the superuser credentials you have
|
||||||
|
created in the previous step to login.
|
||||||
8000\. If you did not change this, you should now be able to visit
|
|
||||||
your Paperless instance at `http://127.0.0.1:8000` or your servers
|
|
||||||
IP-Address:8000. Use the login credentials you have created with the
|
|
||||||
previous step.
|
|
||||||
|
|
||||||
### Build the Docker image yourself {#docker_build}
|
### Build the Docker image yourself {#docker_build}
|
||||||
|
|
||||||
@@ -222,7 +172,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webserver:
|
webserver:
|
||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
and replace it with a line that instructs Docker Compose to build
|
and replace it with a line that instructs Docker Compose to build
|
||||||
@@ -230,15 +180,15 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webserver:
|
webserver:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Follow steps 3 to 8 of [Docker Setup](#docker_hub). When asked to run
|
4. Follow the [Docker setup](#docker) above except when asked to run
|
||||||
`docker compose pull` to pull the image, do
|
`docker compose pull` to pull the image, run
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ docker compose build
|
docker compose build
|
||||||
```
|
```
|
||||||
|
|
||||||
instead to build the image.
|
instead to build the image.
|
||||||
@@ -257,20 +207,20 @@ are released, dependency support is confirmed, etc.
|
|||||||
|
|
||||||
1. Install dependencies. Paperless requires the following packages.
|
1. Install dependencies. Paperless requires the following packages.
|
||||||
|
|
||||||
- `python3`
|
- `python3`
|
||||||
- `python3-pip`
|
- `python3-pip`
|
||||||
- `python3-dev`
|
- `python3-dev`
|
||||||
- `default-libmysqlclient-dev` for MariaDB
|
- `default-libmysqlclient-dev` for MariaDB
|
||||||
- `pkg-config` for mysqlclient (python dependency)
|
- `pkg-config` for mysqlclient (python dependency)
|
||||||
- `fonts-liberation` for generating thumbnails for plain text
|
- `fonts-liberation` for generating thumbnails for plain text
|
||||||
files
|
files
|
||||||
- `imagemagick` >= 6 for PDF conversion
|
- `imagemagick` >= 6 for PDF conversion
|
||||||
- `gnupg` for handling encrypted documents
|
- `gnupg` for handling encrypted documents
|
||||||
- `libpq-dev` for PostgreSQL
|
- `libpq-dev` for PostgreSQL
|
||||||
- `libmagic-dev` for mime type detection
|
- `libmagic-dev` for mime type detection
|
||||||
- `mariadb-client` for MariaDB compile time
|
- `mariadb-client` for MariaDB compile time
|
||||||
- `libzbar0` for barcode detection
|
- `libzbar0` for barcode detection
|
||||||
- `poppler-utils` for barcode detection
|
- `poppler-utils` for barcode detection
|
||||||
|
|
||||||
Use this list for your preferred package management:
|
Use this list for your preferred package management:
|
||||||
|
|
||||||
@@ -281,17 +231,17 @@ are released, dependency support is confirmed, etc.
|
|||||||
These dependencies are required for OCRmyPDF, which is used for text
|
These dependencies are required for OCRmyPDF, which is used for text
|
||||||
recognition.
|
recognition.
|
||||||
|
|
||||||
- `unpaper`
|
- `unpaper`
|
||||||
- `ghostscript`
|
- `ghostscript`
|
||||||
- `icc-profiles-free`
|
- `icc-profiles-free`
|
||||||
- `qpdf`
|
- `qpdf`
|
||||||
- `liblept5`
|
- `liblept5`
|
||||||
- `libxml2`
|
- `libxml2`
|
||||||
- `pngquant` (suggested for certain PDF image optimizations)
|
- `pngquant` (suggested for certain PDF image optimizations)
|
||||||
- `zlib1g`
|
- `zlib1g`
|
||||||
- `tesseract-ocr` >= 4.0.0 for OCR
|
- `tesseract-ocr` >= 4.0.0 for OCR
|
||||||
- `tesseract-ocr` language packs (`tesseract-ocr-eng`,
|
- `tesseract-ocr` language packs (`tesseract-ocr-eng`,
|
||||||
`tesseract-ocr-deu`, etc)
|
`tesseract-ocr-deu`, etc)
|
||||||
|
|
||||||
Use this list for your preferred package management:
|
Use this list for your preferred package management:
|
||||||
|
|
||||||
@@ -301,15 +251,15 @@ are released, dependency support is confirmed, etc.
|
|||||||
|
|
||||||
On Raspberry Pi, these libraries are required as well:
|
On Raspberry Pi, these libraries are required as well:
|
||||||
|
|
||||||
- `libatlas-base-dev`
|
- `libatlas-base-dev`
|
||||||
- `libxslt1-dev`
|
- `libxslt1-dev`
|
||||||
- `mime-support`
|
- `mime-support`
|
||||||
|
|
||||||
You will also need these for installing some of the python dependencies:
|
You will also need these for installing some of the python dependencies:
|
||||||
|
|
||||||
- `build-essential`
|
- `build-essential`
|
||||||
- `python3-setuptools`
|
- `python3-setuptools`
|
||||||
- `python3-wheel`
|
- `python3-wheel`
|
||||||
|
|
||||||
Use this list for your preferred package management:
|
Use this list for your preferred package management:
|
||||||
|
|
||||||
@@ -361,33 +311,33 @@ are released, dependency support is confirmed, etc.
|
|||||||
needs. Required settings for getting
|
needs. Required settings for getting
|
||||||
paperless running are:
|
paperless running are:
|
||||||
|
|
||||||
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your redis server, such as
|
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your redis server, such as
|
||||||
<redis://localhost:6379>.
|
<redis://localhost:6379>.
|
||||||
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) optional, and should be one of `postgres`,
|
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) optional, and should be one of `postgres`,
|
||||||
`mariadb`, or `sqlite`
|
`mariadb`, or `sqlite`
|
||||||
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
|
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
|
||||||
PostgreSQL server is running. Do not configure this to use
|
PostgreSQL server is running. Do not configure this to use
|
||||||
SQLite instead. Also configure port, database name, user and
|
SQLite instead. Also configure port, database name, user and
|
||||||
password as necessary.
|
password as necessary.
|
||||||
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to a folder which
|
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to a folder which
|
||||||
paperless should watch for documents. You might want to have
|
paperless should watch for documents. You might want to have
|
||||||
this somewhere else. Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
|
this somewhere else. Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
|
||||||
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where paperless stores its data.
|
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where paperless stores its data.
|
||||||
If you like, you can point both to the same directory.
|
If you like, you can point both to the same directory.
|
||||||
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
|
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
|
||||||
characters. It's used for authentication. Failure to do so
|
characters. It's used for authentication. Failure to do so
|
||||||
allows third parties to forge authentication credentials.
|
allows third parties to forge authentication credentials.
|
||||||
- [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
|
- [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
|
||||||
point to your domain. Please see
|
point to your domain. Please see
|
||||||
[configuration](configuration.md) for more
|
[configuration](configuration.md) for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
Many more adjustments can be made to paperless, especially the OCR
|
Many more adjustments can be made to paperless, especially the OCR
|
||||||
part. The following options are recommended for everyone:
|
part. The following options are recommended for everyone:
|
||||||
|
|
||||||
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
|
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
|
||||||
documents are written in.
|
documents are written in.
|
||||||
- Set [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to your local time zone.
|
- Set [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to your local time zone.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
@@ -395,9 +345,9 @@ are released, dependency support is confirmed, etc.
|
|||||||
|
|
||||||
7. Create the following directories if they are missing:
|
7. Create the following directories if they are missing:
|
||||||
|
|
||||||
- `/opt/paperless/media`
|
- `/opt/paperless/media`
|
||||||
- `/opt/paperless/data`
|
- `/opt/paperless/data`
|
||||||
- `/opt/paperless/consume`
|
- `/opt/paperless/consume`
|
||||||
|
|
||||||
Adjust as necessary if you configured different folders.
|
Adjust as necessary if you configured different folders.
|
||||||
Ensure that the paperless user has write permissions for every one
|
Ensure that the paperless user has write permissions for every one
|
||||||
@@ -586,29 +536,29 @@ your setup depending on how you installed paperless.
|
|||||||
This setup describes how to update an existing paperless Docker
|
This setup describes how to update an existing paperless Docker
|
||||||
installation. The important things to keep in mind are as follows:
|
installation. The important things to keep in mind are as follows:
|
||||||
|
|
||||||
- Read the [changelog](changelog.md) and
|
- Read the [changelog](changelog.md) and
|
||||||
take note of breaking changes.
|
take note of breaking changes.
|
||||||
- You should decide if you want to stick with SQLite or want to
|
- You should decide if you want to stick with SQLite or want to
|
||||||
migrate your database to PostgreSQL. See [documentation](#sqlite_to_psql)
|
migrate your database to PostgreSQL. See [documentation](#sqlite_to_psql)
|
||||||
for details on
|
for details on
|
||||||
how to move your data from SQLite to PostgreSQL. Both work fine with
|
how to move your data from SQLite to PostgreSQL. Both work fine with
|
||||||
paperless. However, if you already have a database server running
|
paperless. However, if you already have a database server running
|
||||||
for other services, you might as well use it for paperless as well.
|
for other services, you might as well use it for paperless as well.
|
||||||
- The task scheduler of paperless, which is used to execute periodic
|
- The task scheduler of paperless, which is used to execute periodic
|
||||||
tasks such as email checking and maintenance, requires a
|
tasks such as email checking and maintenance, requires a
|
||||||
[redis](https://redis.io/) message broker instance. The
|
[redis](https://redis.io/) message broker instance. The
|
||||||
Docker Compose route takes care of that.
|
Docker Compose route takes care of that.
|
||||||
- The layout of the folder structure for your documents and data
|
- The layout of the folder structure for your documents and data
|
||||||
remains the same, so you can just plug your old docker volumes into
|
remains the same, so you can just plug your old docker volumes into
|
||||||
paperless-ngx and expect it to find everything where it should be.
|
paperless-ngx and expect it to find everything where it should be.
|
||||||
|
|
||||||
Migration to paperless-ngx is then performed in a few simple steps:
|
Migration to paperless-ngx is then performed in a few simple steps:
|
||||||
|
|
||||||
1. Stop paperless.
|
1. Stop paperless.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cd /path/to/current/paperless
|
cd /path/to/current/paperless
|
||||||
$ docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Do a backup for two purposes: If something goes wrong, you still
|
2. Do a backup for two purposes: If something goes wrong, you still
|
||||||
@@ -632,7 +582,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
names of your volumes with
|
names of your volumes with
|
||||||
|
|
||||||
``` shell-session
|
``` shell-session
|
||||||
$ docker volume ls | grep _data
|
docker volume ls | grep _data
|
||||||
```
|
```
|
||||||
|
|
||||||
and adjust the project name in the `.env` file so that it matches
|
and adjust the project name in the `.env` file so that it matches
|
||||||
@@ -643,7 +593,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
after you migrated your existing SQLite database.
|
after you migrated your existing SQLite database.
|
||||||
|
|
||||||
5. Adjust `docker-compose.yml` and `docker-compose.env` to your needs.
|
5. Adjust `docker-compose.yml` and `docker-compose.env` to your needs.
|
||||||
See [Docker setup](#docker_hub) details on
|
See [Docker setup](#docker) details on
|
||||||
which edits are advised.
|
which edits are advised.
|
||||||
|
|
||||||
6. [Update paperless.](administration.md#updating)
|
6. [Update paperless.](administration.md#updating)
|
||||||
@@ -653,7 +603,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
the search index:
|
the search index:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ docker compose run --rm webserver document_index reindex
|
docker compose run --rm webserver document_index reindex
|
||||||
```
|
```
|
||||||
|
|
||||||
This will migrate your database and create the search index. After
|
This will migrate your database and create the search index. After
|
||||||
@@ -662,7 +612,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
8. Start paperless-ngx.
|
8. Start paperless-ngx.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This will run paperless in the background and automatically start it
|
This will run paperless in the background and automatically start it
|
||||||
@@ -763,30 +713,30 @@ Paperless runs on Raspberry Pi. However, some things are rather slow on
|
|||||||
the Pi and configuring some options in paperless can help improve
|
the Pi and configuring some options in paperless can help improve
|
||||||
performance immensely:
|
performance immensely:
|
||||||
|
|
||||||
- Stick with SQLite to save some resources.
|
- Stick with SQLite to save some resources.
|
||||||
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
|
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
|
||||||
only OCR the first page of your documents. In most cases, this page
|
only OCR the first page of your documents. In most cases, this page
|
||||||
contains enough information to be able to find it.
|
contains enough information to be able to find it.
|
||||||
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
|
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
|
||||||
configured to use all cores. The Raspberry Pi models 3 and up have 4
|
configured to use all cores. The Raspberry Pi models 3 and up have 4
|
||||||
cores, meaning that paperless will use 2 workers and 2 threads per
|
cores, meaning that paperless will use 2 workers and 2 threads per
|
||||||
worker. This may result in sluggish response times during
|
worker. This may result in sluggish response times during
|
||||||
consumption, so you might want to lower these settings (example: 2
|
consumption, so you might want to lower these settings (example: 2
|
||||||
workers and 1 thread to always have some computing power left for
|
workers and 1 thread to always have some computing power left for
|
||||||
other tasks).
|
other tasks).
|
||||||
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
|
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
|
||||||
OCR'ing your documents before feeding them into paperless. Some
|
OCR'ing your documents before feeding them into paperless. Some
|
||||||
scanners are able to do this!
|
scanners are able to do this!
|
||||||
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
|
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
|
||||||
file generation for already ocr'ed documents, or `always` to skip it
|
file generation for already ocr'ed documents, or `always` to skip it
|
||||||
for all documents.
|
for all documents.
|
||||||
- If you want to perform OCR on the device, consider using
|
- If you want to perform OCR on the device, consider using
|
||||||
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
||||||
less memory at the expense of slightly worse OCR results.
|
less memory at the expense of slightly worse OCR results.
|
||||||
- If using docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
|
- If using docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
|
||||||
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
|
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
|
||||||
more advanced language processing, which can take more memory and
|
more advanced language processing, which can take more memory and
|
||||||
processing time.
|
processing time.
|
||||||
|
|
||||||
For details, refer to [configuration](configuration.md).
|
For details, refer to [configuration](configuration.md).
|
||||||
|
|
||||||
|
@@ -4,27 +4,27 @@
|
|||||||
|
|
||||||
Check for the following issues:
|
Check for the following issues:
|
||||||
|
|
||||||
- Ensure that the directory you're putting your documents in is the
|
- Ensure that the directory you're putting your documents in is the
|
||||||
folder paperless is watching. With docker, this setting is performed
|
folder paperless is watching. With docker, this setting is performed
|
||||||
in the `docker-compose.yml` file. Without Docker, look at the
|
in the `docker-compose.yml` file. Without Docker, look at the
|
||||||
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
|
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
|
||||||
using docker.
|
using docker.
|
||||||
|
|
||||||
- Ensure that redis is up and running. Paperless does its task
|
- Ensure that redis is up and running. Paperless does its task
|
||||||
processing asynchronously, and for documents to arrive at the task
|
processing asynchronously, and for documents to arrive at the task
|
||||||
processor, it needs redis to run.
|
processor, it needs redis to run.
|
||||||
|
|
||||||
- Ensure that the task processor is running. Docker does this
|
- Ensure that the task processor is running. Docker does this
|
||||||
automatically. Manually invoke the task processor by executing
|
automatically. Manually invoke the task processor by executing
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ celery --app paperless worker
|
celery --app paperless worker
|
||||||
```
|
```
|
||||||
|
|
||||||
- Look at the output of paperless and inspect it for any errors.
|
- Look at the output of paperless and inspect it for any errors.
|
||||||
|
|
||||||
- Go to the admin interface, and check if there are failed tasks. If
|
- Go to the admin interface, and check if there are failed tasks. If
|
||||||
so, the tasks will contain an error message.
|
so, the tasks will contain an error message.
|
||||||
|
|
||||||
## Consumer warns `OCR for XX failed`
|
## Consumer warns `OCR for XX failed`
|
||||||
|
|
||||||
@@ -78,12 +78,12 @@ Ensure that `chown` is possible on these directories.
|
|||||||
This indicates that the Auto matching algorithm found no documents to
|
This indicates that the Auto matching algorithm found no documents to
|
||||||
learn from. This may have two reasons:
|
learn from. This may have two reasons:
|
||||||
|
|
||||||
- You don't use the Auto matching algorithm: The error can be safely
|
- You don't use the Auto matching algorithm: The error can be safely
|
||||||
ignored in this case.
|
ignored in this case.
|
||||||
- You are using the Auto matching algorithm: The classifier explicitly
|
- You are using the Auto matching algorithm: The classifier explicitly
|
||||||
excludes documents with Inbox tags. Verify that there are documents
|
excludes documents with Inbox tags. Verify that there are documents
|
||||||
in your archive without inbox tags. The algorithm will only learn
|
in your archive without inbox tags. The algorithm will only learn
|
||||||
from documents not in your inbox.
|
from documents not in your inbox.
|
||||||
|
|
||||||
## UserWarning in sklearn on every single document
|
## UserWarning in sklearn on every single document
|
||||||
|
|
||||||
@@ -127,10 +127,10 @@ change in the `docker-compose.yml` file:
|
|||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
command:
|
command:
|
||||||
- 'gotenberg'
|
- 'gotenberg'
|
||||||
- '--chromium-disable-javascript=true'
|
- '--chromium-disable-javascript=true'
|
||||||
- '--chromium-allow-list=file:///tmp/.*'
|
- '--chromium-allow-list=file:///tmp/.*'
|
||||||
- '--api-timeout=60'
|
- '--api-timeout=60'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Permission denied errors in the consumption directory
|
## Permission denied errors in the consumption directory
|
||||||
@@ -144,7 +144,7 @@ The following error occurred while consuming document.pdf: [Errno 13] Permission
|
|||||||
This happens when paperless does not have permission to delete files
|
This happens when paperless does not have permission to delete files
|
||||||
inside the consumption directory. Ensure that `USERMAP_UID` and
|
inside the consumption directory. Ensure that `USERMAP_UID` and
|
||||||
`USERMAP_GID` are set to the user id and group id you use on the host
|
`USERMAP_GID` are set to the user id and group id you use on the host
|
||||||
operating system, if these are different from `1000`. See [Docker setup](setup.md#docker_hub).
|
operating system, if these are different from `1000`. See [Docker setup](setup.md#docker).
|
||||||
|
|
||||||
Also ensure that you are able to read and write to the consumption
|
Also ensure that you are able to read and write to the consumption
|
||||||
directory on the host.
|
directory on the host.
|
||||||
@@ -353,6 +353,20 @@ ways from the original. As the logs indicate, if you encounter this error you ca
|
|||||||
`PAPERLESS_OCR_USER_ARGS: '{"continue_on_soft_render_error": true}'` to try to 'force'
|
`PAPERLESS_OCR_USER_ARGS: '{"continue_on_soft_render_error": true}'` to try to 'force'
|
||||||
processing documents with this issue.
|
processing documents with this issue.
|
||||||
|
|
||||||
|
## Logs show "possible incompatible database column" when deleting documents {#convert-uuid-field}
|
||||||
|
|
||||||
|
You may see errors when deleting documents like:
|
||||||
|
|
||||||
|
```
|
||||||
|
Data too long for column 'transaction_id' at row 1
|
||||||
|
```
|
||||||
|
|
||||||
|
This error can occur in installations which have upgraded from a version of Paperless-ngx that used Django 4 (Paperless-ngx versions prior to v2.13.0) with a MariaDB/MySQL database. Due to the backawards-incompatible change in Django 5, the column "documents_document.transaction_id" will need to be re-created, which can be done with a one-time run of the following management command:
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ python3 manage.py convert_mariadb_uuid
|
||||||
|
```
|
||||||
|
|
||||||
## Platform-Specific Deployment Troubleshooting
|
## Platform-Specific Deployment Troubleshooting
|
||||||
|
|
||||||
A user-maintained wiki page is available to help troubleshoot issues that may arise when trying to deploy Paperless-ngx on specific platforms, for example SELinux. Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Platform%E2%80%90Specific-Troubleshooting).
|
A user-maintained wiki page is available to help troubleshoot issues that may arise when trying to deploy Paperless-ngx on specific platforms, for example SELinux. Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Platform%E2%80%90Specific-Troubleshooting).
|
||||||
|
386
docs/usage.md
386
docs/usage.md
@@ -10,37 +10,37 @@ and provides many utilities for finding and managing your documents.
|
|||||||
Paperless essentially consists of two different parts for managing your
|
Paperless essentially consists of two different parts for managing your
|
||||||
documents:
|
documents:
|
||||||
|
|
||||||
- The _consumer_ watches a specified folder and adds all documents in
|
- The _consumer_ watches a specified folder and adds all documents in
|
||||||
that folder to paperless.
|
that folder to paperless.
|
||||||
- The _web server_ provides a UI that you use to manage and search for
|
- The _web server_ provides a UI that you use to manage and search for
|
||||||
your scanned documents.
|
your scanned documents.
|
||||||
|
|
||||||
Each document has a couple of fields that you can assign to them:
|
Each document has a couple of fields that you can assign to them:
|
||||||
|
|
||||||
- A _Document_ is a piece of paper that sometimes contains valuable
|
- A _Document_ is a piece of paper that sometimes contains valuable
|
||||||
information.
|
information.
|
||||||
- The _correspondent_ of a document is the person, institution or
|
- The _correspondent_ of a document is the person, institution or
|
||||||
company that a document either originates from, or is sent to.
|
company that a document either originates from, or is sent to.
|
||||||
- A _tag_ is a label that you can assign to documents. Think of labels
|
- A _tag_ is a label that you can assign to documents. Think of labels
|
||||||
as more powerful folders: Multiple documents can be grouped together
|
as more powerful folders: Multiple documents can be grouped together
|
||||||
with a single tag, however, a single document can also have multiple
|
with a single tag, however, a single document can also have multiple
|
||||||
tags. This is not possible with folders. The reason folders are not
|
tags. This is not possible with folders. The reason folders are not
|
||||||
implemented in paperless is simply that tags are much more versatile
|
implemented in paperless is simply that tags are much more versatile
|
||||||
than folders.
|
than folders.
|
||||||
- A _document type_ is used to demarcate the type of a document such
|
- A _document type_ is used to demarcate the type of a document such
|
||||||
as letter, bank statement, invoice, contract, etc. It is used to
|
as letter, bank statement, invoice, contract, etc. It is used to
|
||||||
identify what a document is about.
|
identify what a document is about.
|
||||||
- The _date added_ of a document is the date the document was scanned
|
- The _date added_ of a document is the date the document was scanned
|
||||||
into paperless. You cannot and should not change this date.
|
into paperless. You cannot and should not change this date.
|
||||||
- The _date created_ of a document is the date the document was
|
- The _date created_ of a document is the date the document was
|
||||||
initially issued. This can be the date you bought a product, the
|
initially issued. This can be the date you bought a product, the
|
||||||
date you signed a contract, or the date a letter was sent to you.
|
date you signed a contract, or the date a letter was sent to you.
|
||||||
- The _archive serial number_ (short: ASN) of a document is the
|
- The _archive serial number_ (short: ASN) of a document is the
|
||||||
identifier of the document in your physical document binders. See
|
identifier of the document in your physical document binders. See
|
||||||
[recommended workflow](#usage-recommended-workflow) below.
|
[recommended workflow](#usage-recommended-workflow) below.
|
||||||
- The _content_ of a document is the text that was OCR'ed from the
|
- The _content_ of a document is the text that was OCR'ed from the
|
||||||
document. This text is fed into the search engine and is used for
|
document. This text is fed into the search engine and is used for
|
||||||
matching tags, correspondents and document types.
|
matching tags, correspondents and document types.
|
||||||
|
|
||||||
## Adding documents to paperless
|
## Adding documents to paperless
|
||||||
|
|
||||||
@@ -142,21 +142,21 @@ patterns can include wildcards and multiple patterns separated by a comma.
|
|||||||
The actions all ensure that the same mail is not consumed twice by
|
The actions all ensure that the same mail is not consumed twice by
|
||||||
different means. These are as follows:
|
different means. These are as follows:
|
||||||
|
|
||||||
- **Delete:** Immediately deletes mail that paperless has consumed
|
- **Delete:** Immediately deletes mail that paperless has consumed
|
||||||
documents from. Use with caution.
|
documents from. Use with caution.
|
||||||
- **Mark as read:** Mark consumed mail as read. Paperless will not
|
- **Mark as read:** Mark consumed mail as read. Paperless will not
|
||||||
consume documents from already read mails. If you read a mail before
|
consume documents from already read mails. If you read a mail before
|
||||||
paperless sees it, it will be ignored.
|
paperless sees it, it will be ignored.
|
||||||
- **Flag:** Sets the 'important' flag on mails with consumed
|
- **Flag:** Sets the 'important' flag on mails with consumed
|
||||||
documents. Paperless will not consume flagged mails.
|
documents. Paperless will not consume flagged mails.
|
||||||
- **Move to folder:** Moves consumed mails out of the way so that
|
- **Move to folder:** Moves consumed mails out of the way so that
|
||||||
paperless won't consume them again.
|
paperless won't consume them again.
|
||||||
- **Add custom Tag:** Adds a custom tag to mails with consumed
|
- **Add custom Tag:** Adds a custom tag to mails with consumed
|
||||||
documents (the IMAP standard calls these "keywords"). Paperless
|
documents (the IMAP standard calls these "keywords"). Paperless
|
||||||
will not consume mails already tagged. Not all mail servers support
|
will not consume mails already tagged. Not all mail servers support
|
||||||
this feature!
|
this feature!
|
||||||
|
|
||||||
- **Apple Mail support:** Apple Mail clients allow differently colored tags. For this to work use `apple:<color>` (e.g. _apple:green_) as a custom tag. Available colors are _red_, _orange_, _yellow_, _blue_, _green_, _violet_ and _grey_.
|
- **Apple Mail support:** Apple Mail clients allow differently colored tags. For this to work use `apple:<color>` (e.g. _apple:green_) as a custom tag. Available colors are _red_, _orange_, _yellow_, _blue_, _green_, _violet_ and _grey_.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
@@ -299,6 +299,12 @@ In order to enable the password reset feature you will need to setup an SMTP bac
|
|||||||
[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST). If your installation does not have
|
[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST). If your installation does not have
|
||||||
[`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) set, the reset link included in emails will use the server host.
|
[`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) set, the reset link included in emails will use the server host.
|
||||||
|
|
||||||
|
### Two-factor authentication
|
||||||
|
|
||||||
|
Users can enable two-factor authentication (2FA) for their accounts from the 'My Profile' dialog. Opening the dropdown reveals a QR code that can be scanned by a 2FA app (e.g. Google Authenticator) to generate a code. The code must then be entered in the dialog to enable 2FA. If the code is accepted and 2FA is enabled, the user will be shown a set of 10 recovery codes that can be used to login in the event that the 2FA device is lost or unavailable. These codes should be stored securely and cannot be retrieved again. Once enabled, users will be required to enter a code from their 2FA app when logging in.
|
||||||
|
|
||||||
|
Should a user lose access to their 2FA device and all recovery codes, a superuser can disable 2FA for the user from the 'Users & Groups' management screen.
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -316,6 +322,8 @@ fields and permissions, which will be merged.
|
|||||||
|
|
||||||
### Workflow Triggers
|
### Workflow Triggers
|
||||||
|
|
||||||
|
#### Types
|
||||||
|
|
||||||
Currently, there are three events that correspond to workflow trigger 'types':
|
Currently, there are three events that correspond to workflow trigger 'types':
|
||||||
|
|
||||||
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
|
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
|
||||||
@@ -325,8 +333,10 @@ Currently, there are three events that correspond to workflow trigger 'types':
|
|||||||
be used for filtering.
|
be used for filtering.
|
||||||
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
|
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
|
||||||
tags, doc type, or correspondent.
|
tags, doc type, or correspondent.
|
||||||
|
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
|
||||||
|
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date.
|
||||||
|
|
||||||
The following flow diagram illustrates the three trigger types:
|
The following flow diagram illustrates the three document trigger types:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -360,62 +370,88 @@ flowchart TD
|
|||||||
|
|
||||||
Workflows allow you to filter by:
|
Workflows allow you to filter by:
|
||||||
|
|
||||||
- Source, e.g. documents uploaded via consume folder, API (& the web UI) and mail fetch
|
- Source, e.g. documents uploaded via consume folder, API (& the web UI) and mail fetch
|
||||||
- File name, including wildcards e.g. \*.pdf will apply to all pdfs
|
- File name, including wildcards e.g. \*.pdf will apply to all pdfs
|
||||||
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
|
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
|
||||||
example, automatically assigning documents to different owners based on the upload directory.
|
example, automatically assigning documents to different owners based on the upload directory.
|
||||||
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
|
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
|
||||||
- Content matching (`Added` and `Updated` triggers only). Filter document content using the matching settings.
|
- Content matching (`Added` and `Updated` triggers only). Filter document content using the matching settings.
|
||||||
- Tags (`Added` and `Updated` triggers only). Filter for documents with any of the specified tags
|
- Tags (`Added` and `Updated` triggers only). Filter for documents with any of the specified tags
|
||||||
- Document type (`Added` and `Updated` triggers only). Filter documents with this doc type
|
- Document type (`Added` and `Updated` triggers only). Filter documents with this doc type
|
||||||
- Correspondent (`Added` and `Updated` triggers only). Filter documents with this correspondent
|
- Correspondent (`Added` and `Updated` triggers only). Filter documents with this correspondent
|
||||||
|
|
||||||
### Workflow Actions
|
### Workflow Actions
|
||||||
|
|
||||||
There are currently two types of workflow actions, "Assignment", which can assign:
|
#### Types
|
||||||
|
|
||||||
- Title, see [title placeholders](usage.md#title-placeholders) below
|
The following workflow action types are available:
|
||||||
- Tags, correspondent, document type and storage path
|
|
||||||
- Document owner
|
|
||||||
- View and / or edit permissions to users or groups
|
|
||||||
- Custom fields. Note that no value for the field will be set
|
|
||||||
|
|
||||||
and "Removal" actions, which can remove either all of or specific sets of the following:
|
##### Assignment
|
||||||
|
|
||||||
- Tags, correspondents, document types or storage paths
|
"Assignment" actions can assign:
|
||||||
- Document owner
|
|
||||||
- View and / or edit permissions
|
|
||||||
- Custom fields
|
|
||||||
|
|
||||||
#### Title placeholders
|
- Title, see [workflow placeholders](usage.md#workflow-placeholders) below
|
||||||
|
- Tags, correspondent, document type and storage path
|
||||||
|
- Document owner
|
||||||
|
- View and / or edit permissions to users or groups
|
||||||
|
- Custom fields. Note that no value for the field will be set
|
||||||
|
|
||||||
Workflow titles can include placeholders but the available options differ depending on the type of
|
##### Removal
|
||||||
workflow trigger. This is because at the time of consumption (when the title is to be set), no automatic tags etc. have been
|
|
||||||
|
"Removal" actions can remove either all of or specific sets of the following:
|
||||||
|
|
||||||
|
- Tags, correspondents, document types or storage paths
|
||||||
|
- Document owner
|
||||||
|
- View and / or edit permissions
|
||||||
|
- Custom fields
|
||||||
|
|
||||||
|
##### Email
|
||||||
|
|
||||||
|
"Email" actions can send documents via email. This action requires a mail server to be [configured](configuration.md#email-sending). You can specify:
|
||||||
|
|
||||||
|
- The recipient email address(es) separated by commas
|
||||||
|
- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
|
||||||
|
- Whether to include the document as an attachment
|
||||||
|
|
||||||
|
##### Webhook
|
||||||
|
|
||||||
|
"Webhook" actions send a POST request to a specified URL. You can specify:
|
||||||
|
|
||||||
|
- The URL to send the request to
|
||||||
|
- The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
|
||||||
|
- The request headers as key-value pairs
|
||||||
|
|
||||||
|
#### Workflow placeholders
|
||||||
|
|
||||||
|
Some workflow text can include placeholders but the available options differ depending on the type of
|
||||||
|
workflow trigger. This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been
|
||||||
applied. You can use the following placeholders with any trigger type:
|
applied. You can use the following placeholders with any trigger type:
|
||||||
|
|
||||||
- `{correspondent}`: assigned correspondent name
|
- `{correspondent}`: assigned correspondent name
|
||||||
- `{document_type}`: assigned document type name
|
- `{document_type}`: assigned document type name
|
||||||
- `{owner_username}`: assigned owner username
|
- `{owner_username}`: assigned owner username
|
||||||
- `{added}`: added datetime
|
- `{added}`: added datetime
|
||||||
- `{added_year}`: added year
|
- `{added_year}`: added year
|
||||||
- `{added_year_short}`: added year
|
- `{added_year_short}`: added year
|
||||||
- `{added_month}`: added month
|
- `{added_month}`: added month
|
||||||
- `{added_month_name}`: added month name
|
- `{added_month_name}`: added month name
|
||||||
- `{added_month_name_short}`: added month short name
|
- `{added_month_name_short}`: added month short name
|
||||||
- `{added_day}`: added day
|
- `{added_day}`: added day
|
||||||
- `{added_time}`: added time in HH:MM format
|
- `{added_time}`: added time in HH:MM format
|
||||||
- `{original_filename}`: original file name without extension
|
- `{original_filename}`: original file name without extension
|
||||||
|
- `{filename}`: current file name without extension
|
||||||
|
|
||||||
The following placeholders are only available for "added" or "updated" triggers
|
The following placeholders are only available for "added" or "updated" triggers
|
||||||
|
|
||||||
- `{created}`: created datetime
|
- `{created}`: created datetime
|
||||||
- `{created_year}`: created year
|
- `{created_year}`: created year
|
||||||
- `{created_year_short}`: created year
|
- `{created_year_short}`: created year
|
||||||
- `{created_month}`: created month
|
- `{created_month}`: created month
|
||||||
- `{created_month_name}`: created month name
|
- `{created_month_name}`: created month name
|
||||||
- `{created_month_name_short}`: created month short name
|
- `{created_month_name_short}`: created month short name
|
||||||
- `{created_day}`: created day
|
- `{created_day}`: created day
|
||||||
- `{created_time}`: created time in HH:MM format
|
- `{created_time}`: created time in HH:MM format
|
||||||
|
- `{doc_url}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
|
||||||
|
|
||||||
### Workflow permissions
|
### Workflow permissions
|
||||||
|
|
||||||
@@ -450,24 +486,24 @@ Multiple fields may be attached to a document but the same field name cannot be
|
|||||||
|
|
||||||
The following custom field types are supported:
|
The following custom field types are supported:
|
||||||
|
|
||||||
- `Text`: any text
|
- `Text`: any text
|
||||||
- `Boolean`: true / false (check / unchecked) field
|
- `Boolean`: true / false (check / unchecked) field
|
||||||
- `Date`: date
|
- `Date`: date
|
||||||
- `URL`: a valid url
|
- `URL`: a valid url
|
||||||
- `Integer`: integer number e.g. 12
|
- `Integer`: integer number e.g. 12
|
||||||
- `Number`: float number e.g. 12.3456
|
- `Number`: float number e.g. 12.3456
|
||||||
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
|
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
|
||||||
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
|
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
|
||||||
- `Select`: a pre-defined list of strings from which the user can choose
|
- `Select`: a pre-defined list of strings from which the user can choose
|
||||||
|
|
||||||
## Share Links
|
## Share Links
|
||||||
|
|
||||||
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
||||||
|
|
||||||
- Share links do not require a user to login and thus link directly to a file.
|
- Share links do not require a user to login and thus link directly to a file.
|
||||||
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
||||||
- Links can optionally have an expiration time set.
|
- Links can optionally have an expiration time set.
|
||||||
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
|
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
@@ -477,10 +513,10 @@ Paperless-ngx added the ability to create shareable links to files in version 2.
|
|||||||
|
|
||||||
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
|
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
|
||||||
|
|
||||||
- Merging documents: available when selecting multiple documents for 'bulk editing'.
|
- Merging documents: available when selecting multiple documents for 'bulk editing'.
|
||||||
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
|
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
|
||||||
- Splitting documents: available from an individual document's details page.
|
- Splitting documents: available from an individual document's details page.
|
||||||
- Deleting pages: available from an individual document's details page.
|
- Deleting pages: available from an individual document's details page.
|
||||||
|
|
||||||
!!! important
|
!!! important
|
||||||
|
|
||||||
@@ -558,18 +594,18 @@ the system.
|
|||||||
Here are a couple examples of tags and types that you could use in your
|
Here are a couple examples of tags and types that you could use in your
|
||||||
collection.
|
collection.
|
||||||
|
|
||||||
- An `inbox` tag for newly added documents that you haven't manually
|
- An `inbox` tag for newly added documents that you haven't manually
|
||||||
edited yet.
|
edited yet.
|
||||||
- A tag `car` for everything car related (repairs, registration,
|
- A tag `car` for everything car related (repairs, registration,
|
||||||
insurance, etc)
|
insurance, etc)
|
||||||
- A tag `todo` for documents that you still need to do something with,
|
- A tag `todo` for documents that you still need to do something with,
|
||||||
such as reply, or perform some task online.
|
such as reply, or perform some task online.
|
||||||
- A tag `bank account x` for all bank statement related to that
|
- A tag `bank account x` for all bank statement related to that
|
||||||
account.
|
account.
|
||||||
- A tag `mail` for anything that you added to paperless via its mail
|
- A tag `mail` for anything that you added to paperless via its mail
|
||||||
processing capabilities.
|
processing capabilities.
|
||||||
- A tag `missing_metadata` when you still need to add some metadata to
|
- A tag `missing_metadata` when you still need to add some metadata to
|
||||||
a document, but can't or don't want to do this right now.
|
a document, but can't or don't want to do this right now.
|
||||||
|
|
||||||
## Searching {#basic-usage_searching}
|
## Searching {#basic-usage_searching}
|
||||||
|
|
||||||
@@ -658,8 +694,8 @@ The following diagram shows how easy it is to manage your documents.
|
|||||||
|
|
||||||
### Preparations in paperless
|
### Preparations in paperless
|
||||||
|
|
||||||
- Create an inbox tag that gets assigned to all new documents.
|
- Create an inbox tag that gets assigned to all new documents.
|
||||||
- Create a TODO tag.
|
- Create a TODO tag.
|
||||||
|
|
||||||
### Processing of the physical documents
|
### Processing of the physical documents
|
||||||
|
|
||||||
@@ -733,78 +769,78 @@ Some documents require attention and require you to act on the document.
|
|||||||
You may take two different approaches to handle these documents based on
|
You may take two different approaches to handle these documents based on
|
||||||
how regularly you intend to scan documents and use paperless.
|
how regularly you intend to scan documents and use paperless.
|
||||||
|
|
||||||
- If you scan and process your documents in paperless regularly,
|
- If you scan and process your documents in paperless regularly,
|
||||||
assign a TODO tag to all scanned documents that you need to process.
|
assign a TODO tag to all scanned documents that you need to process.
|
||||||
Create a saved view on the dashboard that shows all documents with
|
Create a saved view on the dashboard that shows all documents with
|
||||||
this tag.
|
this tag.
|
||||||
- If you do not scan documents regularly and use paperless solely for
|
- If you do not scan documents regularly and use paperless solely for
|
||||||
archiving, create a physical todo box next to your physical inbox
|
archiving, create a physical todo box next to your physical inbox
|
||||||
and put documents you need to process in the TODO box. When you
|
and put documents you need to process in the TODO box. When you
|
||||||
performed the task associated with the document, move it to the
|
performed the task associated with the document, move it to the
|
||||||
inbox.
|
inbox.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Paperless-ngx consists of the following components:
|
Paperless-ngx consists of the following components:
|
||||||
|
|
||||||
- **The webserver:** This serves the administration pages, the API,
|
- **The webserver:** This serves the administration pages, the API,
|
||||||
and the new frontend. This is the main tool you'll be using to interact
|
and the new frontend. This is the main tool you'll be using to interact
|
||||||
with paperless. You may start the webserver directly with
|
with paperless. You may start the webserver directly with
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ cd /path/to/paperless/src/
|
cd /path/to/paperless/src/
|
||||||
$ gunicorn -c ../gunicorn.conf.py paperless.wsgi
|
gunicorn -c ../gunicorn.conf.py paperless.wsgi
|
||||||
```
|
```
|
||||||
|
|
||||||
or by any other means such as Apache `mod_wsgi`.
|
or by any other means such as Apache `mod_wsgi`.
|
||||||
|
|
||||||
- **The consumer:** This is what watches your consumption folder for
|
- **The consumer:** This is what watches your consumption folder for
|
||||||
documents. However, the consumer itself does not really consume your
|
documents. However, the consumer itself does not really consume your
|
||||||
documents. Now it notifies a task processor that a new file is ready
|
documents. Now it notifies a task processor that a new file is ready
|
||||||
for consumption. I suppose it should be named differently. This was
|
for consumption. I suppose it should be named differently. This was
|
||||||
also used to check your emails, but that's now done elsewhere as
|
also used to check your emails, but that's now done elsewhere as
|
||||||
well.
|
well.
|
||||||
|
|
||||||
Start the consumer with the management command `document_consumer`:
|
Start the consumer with the management command `document_consumer`:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ cd /path/to/paperless/src/
|
cd /path/to/paperless/src/
|
||||||
$ python3 manage.py document_consumer
|
python3 manage.py document_consumer
|
||||||
```
|
```
|
||||||
|
|
||||||
- **The task processor:** Paperless relies on [Celery - Distributed
|
- **The task processor:** Paperless relies on [Celery - Distributed
|
||||||
Task Queue](https://docs.celeryq.dev/en/stable/index.html) for doing
|
Task Queue](https://docs.celeryq.dev/en/stable/index.html) for doing
|
||||||
most of the heavy lifting. This is a task queue that accepts tasks
|
most of the heavy lifting. This is a task queue that accepts tasks
|
||||||
from multiple sources and processes these in parallel. It also comes
|
from multiple sources and processes these in parallel. It also comes
|
||||||
with a scheduler that executes certain commands periodically.
|
with a scheduler that executes certain commands periodically.
|
||||||
|
|
||||||
This task processor is responsible for:
|
This task processor is responsible for:
|
||||||
|
|
||||||
- Consuming documents. When the consumer finds new documents, it
|
- Consuming documents. When the consumer finds new documents, it
|
||||||
notifies the task processor to start a consumption task.
|
notifies the task processor to start a consumption task.
|
||||||
- The task processor also performs the consumption of any
|
- The task processor also performs the consumption of any
|
||||||
documents you upload through the web interface.
|
documents you upload through the web interface.
|
||||||
- Consuming emails. It periodically checks your configured
|
- Consuming emails. It periodically checks your configured
|
||||||
accounts for new emails and notifies the task processor to
|
accounts for new emails and notifies the task processor to
|
||||||
consume the attachment of an email.
|
consume the attachment of an email.
|
||||||
- Maintaining the search index and the automatic matching
|
- Maintaining the search index and the automatic matching
|
||||||
algorithm. These are things that paperless needs to do from time
|
algorithm. These are things that paperless needs to do from time
|
||||||
to time in order to operate properly.
|
to time in order to operate properly.
|
||||||
|
|
||||||
This allows paperless to process multiple documents from your
|
This allows paperless to process multiple documents from your
|
||||||
consumption folder in parallel! On a modern multi core system, this
|
consumption folder in parallel! On a modern multi core system, this
|
||||||
makes the consumption process with full OCR blazingly fast.
|
makes the consumption process with full OCR blazingly fast.
|
||||||
|
|
||||||
The task processor comes with a built-in admin interface that you
|
The task processor comes with a built-in admin interface that you
|
||||||
can use to check whenever any of the tasks fail and inspect the
|
can use to check whenever any of the tasks fail and inspect the
|
||||||
errors (i.e., wrong email credentials, errors during consuming a
|
errors (i.e., wrong email credentials, errors during consuming a
|
||||||
specific file, etc).
|
specific file, etc).
|
||||||
|
|
||||||
- A [redis](https://redis.io/) message broker: This is a really
|
- A [redis](https://redis.io/) message broker: This is a really
|
||||||
lightweight service that is responsible for getting the tasks from
|
lightweight service that is responsible for getting the tasks from
|
||||||
the webserver and the consumer to the task scheduler. These run in a
|
the webserver and the consumer to the task scheduler. These run in a
|
||||||
different process (maybe even on different machines!), and
|
different process (maybe even on different machines!), and
|
||||||
therefore, this is necessary.
|
therefore, this is necessary.
|
||||||
|
|
||||||
- Optional: A database server. Paperless supports PostgreSQL, MariaDB
|
- Optional: A database server. Paperless supports PostgreSQL, MariaDB
|
||||||
and SQLite for storing its data.
|
and SQLite for storing its data.
|
||||||
|
@@ -330,8 +330,13 @@ SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!#$%&()*+,-./:;<=>?@[\]^_`{|}~' < /dev/ur
|
|||||||
|
|
||||||
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
||||||
|
|
||||||
_split_langs="${OCR_LANGUAGE//+/ }"
|
# OCR_LANG requires underscores, replace dashes if the user gave them with underscores
|
||||||
read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
|
readonly ocr_langs=${OCR_LANGUAGE//-/_}
|
||||||
|
# OCR_LANGS (the install version) uses dashes, not underscores, so convert underscore to dash and plus to space
|
||||||
|
install_langs=${OCR_LANGUAGE//_/-} # First convert any underscores to dashes
|
||||||
|
install_langs=${install_langs//+/ } # Then convert plus signs to spaces
|
||||||
|
|
||||||
|
read -r -a install_langs_array <<< "${install_langs}"
|
||||||
|
|
||||||
{
|
{
|
||||||
if [[ ! $URL == "" ]] ; then
|
if [[ ! $URL == "" ]] ; then
|
||||||
@@ -344,10 +349,10 @@ read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
|
|||||||
echo "USERMAP_GID=$USERMAP_GID"
|
echo "USERMAP_GID=$USERMAP_GID"
|
||||||
fi
|
fi
|
||||||
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
||||||
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
|
echo "PAPERLESS_OCR_LANGUAGE=$ocr_langs"
|
||||||
echo "PAPERLESS_SECRET_KEY='$SECRET_KEY'"
|
echo "PAPERLESS_SECRET_KEY='$SECRET_KEY'"
|
||||||
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${OCR_LANGUAGES_ARRAY[*]} ]] ; then
|
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${install_langs_array[*]} ]] ; then
|
||||||
echo "PAPERLESS_OCR_LANGUAGES=${OCR_LANGUAGES_ARRAY[*]}"
|
echo "PAPERLESS_OCR_LANGUAGES=${install_langs_array[*]}"
|
||||||
fi
|
fi
|
||||||
} > docker-compose.env
|
} > docker-compose.env
|
||||||
|
|
||||||
|
@@ -11,8 +11,7 @@
|
|||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": [
|
"project": [
|
||||||
"tsconfig.json",
|
"tsconfig.json"
|
||||||
"e2e/tsconfig.json"
|
|
||||||
],
|
],
|
||||||
"createDefaultProgram": true
|
"createDefaultProgram": true
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
const REQUESTS_HAR = 'e2e/admin/requests/api-settings.har'
|
const REQUESTS_HAR = path.join(__dirname, 'requests/api-settings.har')
|
||||||
|
|
||||||
test('should activate / deactivate save button when settings change', async ({
|
test('should activate / deactivate save button when settings change', async ({
|
||||||
page,
|
page,
|
||||||
@@ -33,24 +34,3 @@ test('should apply appearance changes when set', async ({ page }) => {
|
|||||||
await page.getByLabel('Enable dark mode').click()
|
await page.getByLabel('Enable dark mode').click()
|
||||||
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /dark/)
|
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /dark/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should toggle saved view options when set & saved', async ({ page }) => {
|
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/settings/savedviews')
|
|
||||||
await page.getByLabel('Show on dashboard').first().click()
|
|
||||||
await page.getByLabel('Show in sidebar').first().click()
|
|
||||||
const updatePromise = page.waitForRequest((request) => {
|
|
||||||
if (!request.url().includes('8')) return true // skip other saved views
|
|
||||||
const data = request.postDataJSON()
|
|
||||||
const isValid =
|
|
||||||
data['show_on_dashboard'] === true && data['show_in_sidebar'] === true
|
|
||||||
return (
|
|
||||||
isValid &&
|
|
||||||
request.method() === 'PATCH' &&
|
|
||||||
request.url().includes('/api/saved_views/')
|
|
||||||
)
|
|
||||||
})
|
|
||||||
await page.getByRole('button', { name: 'Save' }).scrollIntoViewIfNeeded()
|
|
||||||
await page.getByRole('button', { name: 'Save' }).click()
|
|
||||||
await updatePromise
|
|
||||||
})
|
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
const REQUESTS_HAR1 = 'e2e/dashboard/requests/api-dashboard1.har'
|
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-dashboard1.har')
|
||||||
const REQUESTS_HAR2 = 'e2e/dashboard/requests/api-dashboard2.har'
|
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-dashboard2.har')
|
||||||
const REQUESTS_HAR3 = 'e2e/dashboard/requests/api-dashboard3.har'
|
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-dashboard3.har')
|
||||||
const REQUESTS_HAR4 = 'e2e/dashboard/requests/api-dashboard4.har'
|
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-dashboard4.har')
|
||||||
|
|
||||||
test('dashboard inbox link', async ({ page }) => {
|
test('dashboard inbox link', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
const REQUESTS_HAR = 'e2e/document-detail/requests/api-document-detail.har'
|
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
|
||||||
const REQUESTS_HAR2 = 'e2e/document-detail/requests/api-document-detail2.har'
|
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-detail2.har')
|
||||||
|
|
||||||
test('should activate / deactivate save button when changes are saved', async ({
|
test('should activate / deactivate save button when changes are saved', async ({
|
||||||
page,
|
page,
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
const REQUESTS_HAR1 = 'e2e/document-list/requests/api-document-list1.har'
|
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-document-list1.har')
|
||||||
const REQUESTS_HAR2 = 'e2e/document-list/requests/api-document-list2.har'
|
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-list2.har')
|
||||||
const REQUESTS_HAR3 = 'e2e/document-list/requests/api-document-list3.har'
|
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-document-list3.har')
|
||||||
const REQUESTS_HAR4 = 'e2e/document-list/requests/api-document-list4.har'
|
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-document-list4.har')
|
||||||
const REQUESTS_HAR5 = 'e2e/document-list/requests/api-document-list5.har'
|
const REQUESTS_HAR5 = path.join(__dirname, 'requests/api-document-list5.har')
|
||||||
const REQUESTS_HAR6 = 'e2e/document-list/requests/api-document-list6.har'
|
const REQUESTS_HAR6 = path.join(__dirname, 'requests/api-document-list6.har')
|
||||||
|
|
||||||
test('basic filtering', async ({ page }) => {
|
test('basic filtering', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||||
@@ -134,11 +135,11 @@ test('sorting', async ({ page }) => {
|
|||||||
test('change views', async ({ page }) => {
|
test('change views', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.locator('.btn-group label').first().click()
|
await page.locator('.btn-group > label').first().click()
|
||||||
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
||||||
await page.locator('.btn-group label').nth(1).click()
|
await page.locator('label:nth-child(4)').first().click()
|
||||||
await expect(page.locator('pngx-document-card-small').first()).toBeAttached()
|
await expect(page.locator('pngx-document-card-small').first()).toBeAttached()
|
||||||
await page.locator('.btn-group label').nth(2).click()
|
await page.locator('label:nth-child(6)').click()
|
||||||
await expect(page.locator('pngx-document-card-large').first()).toBeAttached()
|
await expect(page.locator('pngx-document-card-large').first()).toBeAttached()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
const REQUESTS_HAR = 'e2e/permissions/requests/api-global-permissions.har'
|
const REQUESTS_HAR = path.join(__dirname, 'requests/api-global-permissions.har')
|
||||||
|
|
||||||
test('should not allow user to edit settings', async ({ page }) => {
|
test('should not allow user to edit settings', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import * as webpack from 'webpack'
|
|
||||||
import {
|
import {
|
||||||
CustomWebpackBrowserSchema,
|
CustomWebpackBrowserSchema,
|
||||||
TargetOptions,
|
TargetOptions,
|
||||||
} from '@angular-builders/custom-webpack'
|
} from '@angular-builders/custom-webpack'
|
||||||
|
import * as webpack from 'webpack'
|
||||||
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
|
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
|
3191
src-ui/messages.xlf
3191
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
9334
src-ui/package-lock.json
generated
9334
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,60 +11,63 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^18.2.6",
|
"@angular/cdk": "^19.0.2",
|
||||||
"@angular/common": "~18.2.6",
|
"@angular/common": "~19.0.3",
|
||||||
"@angular/compiler": "~18.2.6",
|
"@angular/compiler": "~19.0.3",
|
||||||
"@angular/core": "~18.2.6",
|
"@angular/core": "~19.0.3",
|
||||||
"@angular/forms": "~18.2.6",
|
"@angular/forms": "~19.0.3",
|
||||||
"@angular/localize": "~18.2.6",
|
"@angular/localize": "~19.0.3",
|
||||||
"@angular/platform-browser": "~18.2.6",
|
"@angular/platform-browser": "~19.0.3",
|
||||||
"@angular/platform-browser-dynamic": "~18.2.6",
|
"@angular/platform-browser-dynamic": "~19.0.3",
|
||||||
"@angular/router": "~18.2.6",
|
"@angular/router": "~19.0.3",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^17.0.1",
|
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||||
"@ng-select/ng-select": "^13.9.0",
|
"@ng-select/ng-select": "^14.1.0",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
"ng2-pdf-viewer": "^10.3.1",
|
"ng2-pdf-viewer": "^10.4.0",
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^9.0.0",
|
"ngx-color": "^9.0.0",
|
||||||
"ngx-cookie-service": "^18.0.0",
|
"ngx-cookie-service": "^19.0.0",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^15.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"tslib": "^2.7.0",
|
"tslib": "^2.8.1",
|
||||||
"uuid": "^10.0.0",
|
"utif": "^3.1.0",
|
||||||
"zone.js": "^0.14.8"
|
"uuid": "^11.0.2",
|
||||||
|
"zone.js": "^0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^18.0.0",
|
"@angular-builders/custom-webpack": "^19.0.0-beta.0",
|
||||||
"@angular-builders/jest": "^18.0.0",
|
"@angular-builders/jest": "^19.0.0-beta.1",
|
||||||
"@angular-devkit/build-angular": "^18.2.2",
|
"@angular-devkit/build-angular": "^19.0.4",
|
||||||
"@angular-devkit/core": "^18.2.6",
|
"@angular-devkit/core": "^19.0.4",
|
||||||
"@angular-devkit/schematics": "^18.2.6",
|
"@angular-devkit/schematics": "^19.0.4",
|
||||||
"@angular-eslint/builder": "18.3.1",
|
"@angular-eslint/builder": "19.0.0",
|
||||||
"@angular-eslint/eslint-plugin": "18.3.1",
|
"@angular-eslint/eslint-plugin": "19.0.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "18.3.1",
|
"@angular-eslint/eslint-plugin-template": "19.0.0",
|
||||||
"@angular-eslint/schematics": "18.3.1",
|
"@angular-eslint/schematics": "19.0.0",
|
||||||
"@angular-eslint/template-parser": "18.3.1",
|
"@angular-eslint/template-parser": "19.0.0",
|
||||||
"@angular/cli": "~18.2.6",
|
"@angular/cli": "~19.0.4",
|
||||||
"@angular/compiler-cli": "~18.2.2",
|
"@angular/compiler-cli": "~19.0.3",
|
||||||
"@codecov/webpack-plugin": "^1.2.0",
|
"@codecov/webpack-plugin": "^1.2.1",
|
||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.48.2",
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.7.4",
|
"@types/node": "^22.8.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.8.0",
|
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
||||||
"@typescript-eslint/parser": "^8.8.0",
|
"@typescript-eslint/parser": "^8.12.2",
|
||||||
"@typescript-eslint/utils": "^8.0.0",
|
"@typescript-eslint/utils": "^8.0.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.14.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-preset-angular": "^14.2.4",
|
"jest-preset-angular": "^14.4.2",
|
||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
},
|
||||||
|
"typings": "./src/typings.d.ts"
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { jest } from '@jest/globals'
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
require('jest-preset-angular/setup-jest')
|
|
||||||
}
|
|
||||||
import '@angular/localize/init'
|
import '@angular/localize/init'
|
||||||
import { TextEncoder, TextDecoder } from 'util'
|
import { jest } from '@jest/globals'
|
||||||
|
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
|
||||||
|
import { TextDecoder, TextEncoder } from 'util'
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
setupZoneTestEnv()
|
||||||
|
}
|
||||||
global.TextEncoder = TextEncoder
|
global.TextEncoder = TextEncoder
|
||||||
global.TextDecoder = TextDecoder
|
global.TextDecoder = TextDecoder
|
||||||
|
|
||||||
|
@@ -1,32 +1,33 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
import { ConfigComponent } from './components/admin/config/config.component'
|
||||||
|
import { LogsComponent } from './components/admin/logs/logs.component'
|
||||||
|
import { SettingsComponent } from './components/admin/settings/settings.component'
|
||||||
|
import { TasksComponent } from './components/admin/tasks/tasks.component'
|
||||||
|
import { TrashComponent } from './components/admin/trash/trash.component'
|
||||||
|
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
||||||
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
|
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { LogsComponent } from './components/admin/logs/logs.component'
|
import { MailComponent } from './components/manage/mail/mail.component'
|
||||||
import { SettingsComponent } from './components/admin/settings/settings.component'
|
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
|
||||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
import { TasksComponent } from './components/admin/tasks/tasks.component'
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
import { PermissionsGuard } from './guards/permissions.guard'
|
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||||
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
|
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
} from './services/permissions.service'
|
} from './services/permissions.service'
|
||||||
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
|
||||||
import { MailComponent } from './components/manage/mail/mail.component'
|
|
||||||
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
|
|
||||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
|
||||||
import { ConfigComponent } from './components/admin/config/config.component'
|
|
||||||
import { TrashComponent } from './components/admin/trash/trash.component'
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
@@ -165,6 +166,10 @@ export const routes: Routes = [
|
|||||||
path: 'settings/usersgroups',
|
path: 'settings/usersgroups',
|
||||||
redirectTo: '/usersgroups',
|
redirectTo: '/usersgroups',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/savedviews',
|
||||||
|
redirectTo: '/savedviews',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
@@ -255,6 +260,17 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'savedviews',
|
||||||
|
component: SavedViewsComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.SavedView,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -1,30 +1,31 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
|
||||||
fakeAsync,
|
fakeAsync,
|
||||||
|
TestBed,
|
||||||
tick,
|
tick,
|
||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { Router, RouterModule } from '@angular/router'
|
import { Router, RouterModule } from '@angular/router'
|
||||||
import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||||
|
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { routes } from './app-routing.module'
|
import { routes } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
|
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||||
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import {
|
import {
|
||||||
ConsumerStatusService,
|
ConsumerStatusService,
|
||||||
FileStatus,
|
FileStatus,
|
||||||
} from './services/consumer-status.service'
|
} from './services/consumer-status.service'
|
||||||
import { PermissionsService } from './services/permissions.service'
|
|
||||||
import { ToastService, Toast } from './services/toast.service'
|
|
||||||
import { SettingsService } from './services/settings.service'
|
|
||||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
|
||||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
|
||||||
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { HotKeyService } from './services/hot-key.service'
|
import { HotKeyService } from './services/hot-key.service'
|
||||||
import { PermissionsGuard } from './guards/permissions.guard'
|
import { PermissionsService } from './services/permissions.service'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { SettingsService } from './services/settings.service'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { Toast, ToastService } from './services/toast.service'
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
let component: AppComponent
|
let component: AppComponent
|
||||||
@@ -39,12 +40,15 @@ describe('AppComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [AppComponent, ToastsComponent, FileDropComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
TourNgBootstrapModule,
|
TourNgBootstrapModule,
|
||||||
RouterModule.forRoot(routes),
|
RouterModule.forRoot(routes),
|
||||||
NgxFileDropModule,
|
NgxFileDropModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
AppComponent,
|
||||||
|
ToastsComponent,
|
||||||
|
FileDropComponent,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
|
@@ -1,23 +1,31 @@
|
|||||||
import { SettingsService } from './services/settings.service'
|
|
||||||
import { SETTINGS_KEYS } from './data/ui-settings'
|
|
||||||
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router, RouterOutlet } from '@angular/router'
|
||||||
import { Subscription, first } from 'rxjs'
|
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
import { first, Subscription } from 'rxjs'
|
||||||
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
|
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||||
|
import { SETTINGS_KEYS } from './data/ui-settings'
|
||||||
import { ConsumerStatusService } from './services/consumer-status.service'
|
import { ConsumerStatusService } from './services/consumer-status.service'
|
||||||
import { ToastService } from './services/toast.service'
|
import { HotKeyService } from './services/hot-key.service'
|
||||||
import { TasksService } from './services/tasks.service'
|
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
} from './services/permissions.service'
|
} from './services/permissions.service'
|
||||||
import { HotKeyService } from './services/hot-key.service'
|
import { SettingsService } from './services/settings.service'
|
||||||
|
import { TasksService } from './services/tasks.service'
|
||||||
|
import { ToastService } from './services/toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-root',
|
selector: 'pngx-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
|
imports: [
|
||||||
|
FileDropComponent,
|
||||||
|
ToastsComponent,
|
||||||
|
TourNgBootstrapModule,
|
||||||
|
RouterOutlet,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
newDocumentSubscription: Subscription
|
newDocumentSubscription: Subscription
|
||||||
@@ -165,7 +173,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
anchorId: 'tour.dashboard',
|
anchorId: 'tour.dashboard',
|
||||||
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
|
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
|
||||||
route: '/dashboard',
|
route: '/dashboard',
|
||||||
delayAfterNavigation: 500,
|
delayAfterNavigation: 500,
|
||||||
isOptional: false,
|
isOptional: false,
|
||||||
@@ -227,7 +235,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchorId: 'tour.settings',
|
anchorId: 'tour.settings',
|
||||||
content: $localize`Check out the settings for various tweaks to the web app and toggle settings for saved views.`,
|
content: $localize`Check out the settings for various tweaks to the web app.`,
|
||||||
route: '/settings',
|
route: '/settings',
|
||||||
backdropConfig: {
|
backdropConfig: {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
@@ -1,571 +0,0 @@
|
|||||||
import { BrowserModule } from '@angular/platform-browser'
|
|
||||||
import { APP_INITIALIZER, NgModule } from '@angular/core'
|
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
|
||||||
import { AppComponent } from './app.component'
|
|
||||||
import {
|
|
||||||
NgbDateAdapter,
|
|
||||||
NgbDateParserFormatter,
|
|
||||||
NgbModule,
|
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import {
|
|
||||||
HTTP_INTERCEPTORS,
|
|
||||||
provideHttpClient,
|
|
||||||
withInterceptorsFromDi,
|
|
||||||
} from '@angular/common/http'
|
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
|
||||||
import { LogsComponent } from './components/admin/logs/logs.component'
|
|
||||||
import { SettingsComponent } from './components/admin/settings/settings.component'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { DatePipe, registerLocaleData } from '@angular/common'
|
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
|
||||||
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
|
||||||
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
|
||||||
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
|
||||||
import { TagComponent } from './components/common/tag/tag.component'
|
|
||||||
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
|
||||||
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
|
||||||
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
|
||||||
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
|
|
||||||
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
|
||||||
import { DatesDropdownComponent } from './components/common/dates-dropdown/dates-dropdown.component'
|
|
||||||
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
|
|
||||||
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
|
|
||||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
|
|
||||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
|
||||||
import { TextComponent } from './components/common/input/text/text.component'
|
|
||||||
import { TextAreaComponent } from './components/common/input/textarea/textarea.component'
|
|
||||||
import { SelectComponent } from './components/common/input/select/select.component'
|
|
||||||
import { CheckComponent } from './components/common/input/check/check.component'
|
|
||||||
import { UrlComponent } from './components/common/input/url/url.component'
|
|
||||||
import { PasswordComponent } from './components/common/input/password/password.component'
|
|
||||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
|
||||||
import { TagsComponent } from './components/common/input/tags/tags.component'
|
|
||||||
import { IfPermissionsDirective } from './directives/if-permissions.directive'
|
|
||||||
import { SortableDirective } from './directives/sortable.directive'
|
|
||||||
import { CookieService } from 'ngx-cookie-service'
|
|
||||||
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
|
||||||
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
|
|
||||||
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
|
|
||||||
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
|
|
||||||
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
|
|
||||||
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
|
|
||||||
import { YesNoPipe } from './pipes/yes-no.pipe'
|
|
||||||
import { FileSizePipe } from './pipes/file-size.pipe'
|
|
||||||
import { FilterPipe } from './pipes/filter.pipe'
|
|
||||||
import { DocumentTitlePipe } from './pipes/document-title.pipe'
|
|
||||||
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
|
|
||||||
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
|
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
|
||||||
import { NumberComponent } from './components/common/input/number/number.component'
|
|
||||||
import { SafeUrlPipe } from './pipes/safeurl.pipe'
|
|
||||||
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
|
|
||||||
import { CustomDatePipe } from './pipes/custom-date.pipe'
|
|
||||||
import { DateComponent } from './components/common/input/date/date.component'
|
|
||||||
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
|
|
||||||
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
|
|
||||||
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
|
|
||||||
import { ColorSliderModule } from 'ngx-color/slider'
|
|
||||||
import { ColorComponent } from './components/common/input/color/color.component'
|
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
|
||||||
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
|
|
||||||
import { PermissionsGuard } from './guards/permissions.guard'
|
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
|
||||||
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
|
||||||
import { SettingsService } from './services/settings.service'
|
|
||||||
import { TasksComponent } from './components/admin/tasks/tasks.component'
|
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
|
||||||
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
|
||||||
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
|
||||||
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
|
|
||||||
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
|
||||||
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
|
||||||
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
|
|
||||||
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
|
|
||||||
import { IfOwnerDirective } from './directives/if-owner.directive'
|
|
||||||
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
|
|
||||||
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
|
|
||||||
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
|
|
||||||
import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component'
|
|
||||||
import { UsernamePipe } from './pipes/username.pipe'
|
|
||||||
import { LogoComponent } from './components/common/logo/logo.component'
|
|
||||||
import { IsNumberPipe } from './pipes/is-number.pipe'
|
|
||||||
import { ShareLinksDropdownComponent } from './components/common/share-links-dropdown/share-links-dropdown.component'
|
|
||||||
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
|
||||||
import { WorkflowEditDialogComponent } from './components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
|
||||||
import { MailComponent } from './components/manage/mail/mail.component'
|
|
||||||
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
|
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
|
||||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
|
||||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
|
||||||
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
|
||||||
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
|
|
||||||
import { CustomFieldsQueryDropdownComponent } from './components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
|
|
||||||
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
|
|
||||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
|
|
||||||
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
|
||||||
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
|
||||||
import { ConfigComponent } from './components/admin/config/config.component'
|
|
||||||
import { FileComponent } from './components/common/input/file/file.component'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
|
|
||||||
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
|
||||||
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
|
|
||||||
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
|
||||||
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
|
||||||
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
|
||||||
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
|
|
||||||
import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component'
|
|
||||||
import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component'
|
|
||||||
import { GlobalSearchComponent } from './components/app-frame/global-search/global-search.component'
|
|
||||||
import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component'
|
|
||||||
import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
|
|
||||||
import { TrashComponent } from './components/admin/trash/trash.component'
|
|
||||||
import {
|
|
||||||
airplane,
|
|
||||||
archive,
|
|
||||||
arrowClockwise,
|
|
||||||
arrowCounterclockwise,
|
|
||||||
arrowDown,
|
|
||||||
arrowLeft,
|
|
||||||
arrowRepeat,
|
|
||||||
arrowRight,
|
|
||||||
arrowRightShort,
|
|
||||||
arrowUpRight,
|
|
||||||
asterisk,
|
|
||||||
braces,
|
|
||||||
bodyText,
|
|
||||||
boxArrowInRight,
|
|
||||||
boxArrowUp,
|
|
||||||
boxArrowUpRight,
|
|
||||||
boxes,
|
|
||||||
calendar,
|
|
||||||
calendarEvent,
|
|
||||||
calendarEventFill,
|
|
||||||
cardChecklist,
|
|
||||||
cardHeading,
|
|
||||||
caretDown,
|
|
||||||
caretUp,
|
|
||||||
chatLeftText,
|
|
||||||
check,
|
|
||||||
check2All,
|
|
||||||
checkAll,
|
|
||||||
checkCircleFill,
|
|
||||||
checkLg,
|
|
||||||
chevronDoubleLeft,
|
|
||||||
chevronDoubleRight,
|
|
||||||
clipboard,
|
|
||||||
clipboardCheck,
|
|
||||||
clipboardCheckFill,
|
|
||||||
clipboardFill,
|
|
||||||
dash,
|
|
||||||
dashCircle,
|
|
||||||
diagram3,
|
|
||||||
dice5,
|
|
||||||
doorOpen,
|
|
||||||
download,
|
|
||||||
envelope,
|
|
||||||
envelopeAt,
|
|
||||||
envelopeAtFill,
|
|
||||||
exclamationCircleFill,
|
|
||||||
exclamationTriangle,
|
|
||||||
exclamationTriangleFill,
|
|
||||||
eye,
|
|
||||||
fileEarmark,
|
|
||||||
fileEarmarkCheck,
|
|
||||||
fileEarmarkFill,
|
|
||||||
fileEarmarkLock,
|
|
||||||
fileEarmarkMinus,
|
|
||||||
files,
|
|
||||||
fileText,
|
|
||||||
filter,
|
|
||||||
folder,
|
|
||||||
folderFill,
|
|
||||||
funnel,
|
|
||||||
gear,
|
|
||||||
google,
|
|
||||||
grid,
|
|
||||||
gripVertical,
|
|
||||||
hash,
|
|
||||||
hddStack,
|
|
||||||
house,
|
|
||||||
infoCircle,
|
|
||||||
journals,
|
|
||||||
link,
|
|
||||||
listTask,
|
|
||||||
listUl,
|
|
||||||
microsoft,
|
|
||||||
nodePlus,
|
|
||||||
pencil,
|
|
||||||
people,
|
|
||||||
peopleFill,
|
|
||||||
person,
|
|
||||||
personCircle,
|
|
||||||
personFill,
|
|
||||||
personFillLock,
|
|
||||||
personLock,
|
|
||||||
personSquare,
|
|
||||||
plus,
|
|
||||||
plusCircle,
|
|
||||||
questionCircle,
|
|
||||||
scissors,
|
|
||||||
search,
|
|
||||||
slashCircle,
|
|
||||||
sliders2Vertical,
|
|
||||||
sortAlphaDown,
|
|
||||||
sortAlphaUpAlt,
|
|
||||||
tagFill,
|
|
||||||
tag,
|
|
||||||
tags,
|
|
||||||
textIndentLeft,
|
|
||||||
textLeft,
|
|
||||||
threeDots,
|
|
||||||
threeDotsVertical,
|
|
||||||
trash,
|
|
||||||
uiRadios,
|
|
||||||
upcScan,
|
|
||||||
x,
|
|
||||||
xCircle,
|
|
||||||
xLg,
|
|
||||||
} from 'ngx-bootstrap-icons'
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
airplane,
|
|
||||||
archive,
|
|
||||||
arrowClockwise,
|
|
||||||
arrowCounterclockwise,
|
|
||||||
arrowDown,
|
|
||||||
arrowLeft,
|
|
||||||
arrowRepeat,
|
|
||||||
arrowRight,
|
|
||||||
arrowRightShort,
|
|
||||||
arrowUpRight,
|
|
||||||
asterisk,
|
|
||||||
braces,
|
|
||||||
bodyText,
|
|
||||||
boxArrowInRight,
|
|
||||||
boxArrowUp,
|
|
||||||
boxArrowUpRight,
|
|
||||||
boxes,
|
|
||||||
calendar,
|
|
||||||
calendarEvent,
|
|
||||||
calendarEventFill,
|
|
||||||
cardChecklist,
|
|
||||||
cardHeading,
|
|
||||||
caretDown,
|
|
||||||
caretUp,
|
|
||||||
chatLeftText,
|
|
||||||
check,
|
|
||||||
check2All,
|
|
||||||
checkAll,
|
|
||||||
checkCircleFill,
|
|
||||||
checkLg,
|
|
||||||
chevronDoubleLeft,
|
|
||||||
chevronDoubleRight,
|
|
||||||
clipboard,
|
|
||||||
clipboardCheck,
|
|
||||||
clipboardCheckFill,
|
|
||||||
clipboardFill,
|
|
||||||
dash,
|
|
||||||
dashCircle,
|
|
||||||
diagram3,
|
|
||||||
dice5,
|
|
||||||
doorOpen,
|
|
||||||
download,
|
|
||||||
envelope,
|
|
||||||
envelopeAt,
|
|
||||||
envelopeAtFill,
|
|
||||||
exclamationCircleFill,
|
|
||||||
exclamationTriangle,
|
|
||||||
exclamationTriangleFill,
|
|
||||||
eye,
|
|
||||||
fileEarmark,
|
|
||||||
fileEarmarkCheck,
|
|
||||||
fileEarmarkFill,
|
|
||||||
fileEarmarkLock,
|
|
||||||
fileEarmarkMinus,
|
|
||||||
files,
|
|
||||||
fileText,
|
|
||||||
filter,
|
|
||||||
folder,
|
|
||||||
folderFill,
|
|
||||||
funnel,
|
|
||||||
gear,
|
|
||||||
google,
|
|
||||||
grid,
|
|
||||||
gripVertical,
|
|
||||||
hash,
|
|
||||||
hddStack,
|
|
||||||
house,
|
|
||||||
infoCircle,
|
|
||||||
journals,
|
|
||||||
link,
|
|
||||||
listTask,
|
|
||||||
listUl,
|
|
||||||
microsoft,
|
|
||||||
nodePlus,
|
|
||||||
pencil,
|
|
||||||
people,
|
|
||||||
peopleFill,
|
|
||||||
person,
|
|
||||||
personCircle,
|
|
||||||
personFill,
|
|
||||||
personFillLock,
|
|
||||||
personLock,
|
|
||||||
personSquare,
|
|
||||||
plus,
|
|
||||||
plusCircle,
|
|
||||||
questionCircle,
|
|
||||||
scissors,
|
|
||||||
search,
|
|
||||||
slashCircle,
|
|
||||||
sliders2Vertical,
|
|
||||||
sortAlphaDown,
|
|
||||||
sortAlphaUpAlt,
|
|
||||||
tagFill,
|
|
||||||
tag,
|
|
||||||
tags,
|
|
||||||
textIndentLeft,
|
|
||||||
textLeft,
|
|
||||||
threeDots,
|
|
||||||
threeDotsVertical,
|
|
||||||
trash,
|
|
||||||
uiRadios,
|
|
||||||
upcScan,
|
|
||||||
x,
|
|
||||||
xCircle,
|
|
||||||
xLg,
|
|
||||||
}
|
|
||||||
|
|
||||||
import localeAf from '@angular/common/locales/af'
|
|
||||||
import localeAr from '@angular/common/locales/ar'
|
|
||||||
import localeBe from '@angular/common/locales/be'
|
|
||||||
import localeBg from '@angular/common/locales/bg'
|
|
||||||
import localeCa from '@angular/common/locales/ca'
|
|
||||||
import localeCs from '@angular/common/locales/cs'
|
|
||||||
import localeDa from '@angular/common/locales/da'
|
|
||||||
import localeDe from '@angular/common/locales/de'
|
|
||||||
import localeEl from '@angular/common/locales/el'
|
|
||||||
import localeEnGb from '@angular/common/locales/en-GB'
|
|
||||||
import localeEs from '@angular/common/locales/es'
|
|
||||||
import localeFi from '@angular/common/locales/fi'
|
|
||||||
import localeFr from '@angular/common/locales/fr'
|
|
||||||
import localeHu from '@angular/common/locales/hu'
|
|
||||||
import localeIt from '@angular/common/locales/it'
|
|
||||||
import localeJa from '@angular/common/locales/ja'
|
|
||||||
import localeKo from '@angular/common/locales/ko'
|
|
||||||
import localeLb from '@angular/common/locales/lb'
|
|
||||||
import localeNl from '@angular/common/locales/nl'
|
|
||||||
import localeNo from '@angular/common/locales/no'
|
|
||||||
import localePl from '@angular/common/locales/pl'
|
|
||||||
import localePt from '@angular/common/locales/pt'
|
|
||||||
import localeRo from '@angular/common/locales/ro'
|
|
||||||
import localeRu from '@angular/common/locales/ru'
|
|
||||||
import localeSk from '@angular/common/locales/sk'
|
|
||||||
import localeSl from '@angular/common/locales/sl'
|
|
||||||
import localeSr from '@angular/common/locales/sr'
|
|
||||||
import localeSv from '@angular/common/locales/sv'
|
|
||||||
import localeTr from '@angular/common/locales/tr'
|
|
||||||
import localeUk from '@angular/common/locales/uk'
|
|
||||||
import localeZh from '@angular/common/locales/zh'
|
|
||||||
|
|
||||||
registerLocaleData(localeAf)
|
|
||||||
registerLocaleData(localeAr)
|
|
||||||
registerLocaleData(localeBe)
|
|
||||||
registerLocaleData(localeBg)
|
|
||||||
registerLocaleData(localeCa)
|
|
||||||
registerLocaleData(localeCs)
|
|
||||||
registerLocaleData(localeDa)
|
|
||||||
registerLocaleData(localeDe)
|
|
||||||
registerLocaleData(localeEl)
|
|
||||||
registerLocaleData(localeEnGb)
|
|
||||||
registerLocaleData(localeEs)
|
|
||||||
registerLocaleData(localeFi)
|
|
||||||
registerLocaleData(localeFr)
|
|
||||||
registerLocaleData(localeHu)
|
|
||||||
registerLocaleData(localeIt)
|
|
||||||
registerLocaleData(localeJa)
|
|
||||||
registerLocaleData(localeKo)
|
|
||||||
registerLocaleData(localeLb)
|
|
||||||
registerLocaleData(localeNl)
|
|
||||||
registerLocaleData(localeNo)
|
|
||||||
registerLocaleData(localePl)
|
|
||||||
registerLocaleData(localePt, 'pt-BR')
|
|
||||||
registerLocaleData(localePt, 'pt-PT')
|
|
||||||
registerLocaleData(localeRo)
|
|
||||||
registerLocaleData(localeRu)
|
|
||||||
registerLocaleData(localeSk)
|
|
||||||
registerLocaleData(localeSl)
|
|
||||||
registerLocaleData(localeSr)
|
|
||||||
registerLocaleData(localeSv)
|
|
||||||
registerLocaleData(localeTr)
|
|
||||||
registerLocaleData(localeUk)
|
|
||||||
registerLocaleData(localeZh)
|
|
||||||
|
|
||||||
function initializeApp(settings: SettingsService) {
|
|
||||||
return () => {
|
|
||||||
return settings.initializeSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent,
|
|
||||||
DocumentListComponent,
|
|
||||||
DocumentDetailComponent,
|
|
||||||
DashboardComponent,
|
|
||||||
TagListComponent,
|
|
||||||
DocumentTypeListComponent,
|
|
||||||
CorrespondentListComponent,
|
|
||||||
StoragePathListComponent,
|
|
||||||
LogsComponent,
|
|
||||||
SettingsComponent,
|
|
||||||
NotFoundComponent,
|
|
||||||
CorrespondentEditDialogComponent,
|
|
||||||
ConfirmDialogComponent,
|
|
||||||
TagEditDialogComponent,
|
|
||||||
DocumentTypeEditDialogComponent,
|
|
||||||
StoragePathEditDialogComponent,
|
|
||||||
TagComponent,
|
|
||||||
ClearableBadgeComponent,
|
|
||||||
PageHeaderComponent,
|
|
||||||
AppFrameComponent,
|
|
||||||
ToastsComponent,
|
|
||||||
FilterEditorComponent,
|
|
||||||
FilterableDropdownComponent,
|
|
||||||
ToggleableDropdownButtonComponent,
|
|
||||||
DatesDropdownComponent,
|
|
||||||
DocumentCardLargeComponent,
|
|
||||||
DocumentCardSmallComponent,
|
|
||||||
BulkEditorComponent,
|
|
||||||
TextComponent,
|
|
||||||
TextAreaComponent,
|
|
||||||
SelectComponent,
|
|
||||||
CheckComponent,
|
|
||||||
UrlComponent,
|
|
||||||
PasswordComponent,
|
|
||||||
SaveViewConfigDialogComponent,
|
|
||||||
TagsComponent,
|
|
||||||
IfPermissionsDirective,
|
|
||||||
SortableDirective,
|
|
||||||
SavedViewWidgetComponent,
|
|
||||||
StatisticsWidgetComponent,
|
|
||||||
UploadFileWidgetComponent,
|
|
||||||
WidgetFrameComponent,
|
|
||||||
WelcomeWidgetComponent,
|
|
||||||
YesNoPipe,
|
|
||||||
FileSizePipe,
|
|
||||||
FilterPipe,
|
|
||||||
DocumentTitlePipe,
|
|
||||||
MetadataCollapseComponent,
|
|
||||||
SelectDialogComponent,
|
|
||||||
NumberComponent,
|
|
||||||
SafeUrlPipe,
|
|
||||||
SafeHtmlPipe,
|
|
||||||
CustomDatePipe,
|
|
||||||
DateComponent,
|
|
||||||
ColorComponent,
|
|
||||||
DocumentAsnComponent,
|
|
||||||
DocumentNotesComponent,
|
|
||||||
TasksComponent,
|
|
||||||
UserEditDialogComponent,
|
|
||||||
GroupEditDialogComponent,
|
|
||||||
PermissionsSelectComponent,
|
|
||||||
MailAccountEditDialogComponent,
|
|
||||||
MailRuleEditDialogComponent,
|
|
||||||
PermissionsUserComponent,
|
|
||||||
PermissionsGroupComponent,
|
|
||||||
IfOwnerDirective,
|
|
||||||
IfObjectPermissionsDirective,
|
|
||||||
PermissionsDialogComponent,
|
|
||||||
PermissionsFormComponent,
|
|
||||||
PermissionsFilterDropdownComponent,
|
|
||||||
UsernamePipe,
|
|
||||||
LogoComponent,
|
|
||||||
IsNumberPipe,
|
|
||||||
ShareLinksDropdownComponent,
|
|
||||||
WorkflowsComponent,
|
|
||||||
WorkflowEditDialogComponent,
|
|
||||||
MailComponent,
|
|
||||||
UsersAndGroupsComponent,
|
|
||||||
FileDropComponent,
|
|
||||||
CustomFieldsComponent,
|
|
||||||
CustomFieldEditDialogComponent,
|
|
||||||
CustomFieldsDropdownComponent,
|
|
||||||
CustomFieldsQueryDropdownComponent,
|
|
||||||
ProfileEditDialogComponent,
|
|
||||||
DocumentLinkComponent,
|
|
||||||
PreviewPopupComponent,
|
|
||||||
SwitchComponent,
|
|
||||||
ConfigComponent,
|
|
||||||
FileComponent,
|
|
||||||
ConfirmButtonComponent,
|
|
||||||
MonetaryComponent,
|
|
||||||
SystemStatusDialogComponent,
|
|
||||||
RotateConfirmDialogComponent,
|
|
||||||
MergeConfirmDialogComponent,
|
|
||||||
SplitConfirmDialogComponent,
|
|
||||||
DocumentHistoryComponent,
|
|
||||||
DragDropSelectComponent,
|
|
||||||
CustomFieldDisplayComponent,
|
|
||||||
GlobalSearchComponent,
|
|
||||||
HotkeyDialogComponent,
|
|
||||||
DeletePagesConfirmDialogComponent,
|
|
||||||
TrashComponent,
|
|
||||||
],
|
|
||||||
bootstrap: [AppComponent],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
AppRoutingModule,
|
|
||||||
NgbModule,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
PdfViewerModule,
|
|
||||||
NgxFileDropModule,
|
|
||||||
NgSelectModule,
|
|
||||||
ColorSliderModule,
|
|
||||||
TourNgBootstrapModule,
|
|
||||||
DragDropModule,
|
|
||||||
NgxBootstrapIconsModule.pick(icons),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: initializeApp,
|
|
||||||
deps: [SettingsService],
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
DatePipe,
|
|
||||||
CookieService,
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: CsrfInterceptor,
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: ApiVersionInterceptor,
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
FilterPipe,
|
|
||||||
DocumentTitlePipe,
|
|
||||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
|
||||||
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
|
||||||
PermissionsGuard,
|
|
||||||
DirtyDocGuard,
|
|
||||||
DirtySavedViewGuard,
|
|
||||||
UsernamePipe,
|
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
@@ -1,24 +1,24 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
import { ConfigComponent } from './config.component'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { of, throwError } from 'rxjs'
|
|
||||||
import { OutputTypeConfig } from 'src/app/data/paperless-config'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { TextComponent } from '../../common/input/text/text.component'
|
|
||||||
import { NumberComponent } from '../../common/input/number/number.component'
|
|
||||||
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
|
||||||
import { FileComponent } from '../../common/input/file/file.component'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { of, throwError } from 'rxjs'
|
||||||
|
import { OutputTypeConfig } from 'src/app/data/paperless-config'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { FileComponent } from '../../common/input/file/file.component'
|
||||||
|
import { NumberComponent } from '../../common/input/number/number.component'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
|
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||||
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { ConfigComponent } from './config.component'
|
||||||
|
|
||||||
describe('ConfigComponent', () => {
|
describe('ConfigComponent', () => {
|
||||||
let component: ConfigComponent
|
let component: ConfigComponent
|
||||||
@@ -29,15 +29,6 @@ describe('ConfigComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [
|
|
||||||
ConfigComponent,
|
|
||||||
TextComponent,
|
|
||||||
SelectComponent,
|
|
||||||
NumberComponent,
|
|
||||||
SwitchComponent,
|
|
||||||
FileComponent,
|
|
||||||
PageHeaderComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
@@ -45,6 +36,13 @@ describe('ConfigComponent', () => {
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
ConfigComponent,
|
||||||
|
TextComponent,
|
||||||
|
SelectComponent,
|
||||||
|
NumberComponent,
|
||||||
|
SwitchComponent,
|
||||||
|
FileComponent,
|
||||||
|
PageHeaderComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
@@ -1,33 +1,60 @@
|
|||||||
|
import { AsyncPipe } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
Subject,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
first,
|
first,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import {
|
import {
|
||||||
PaperlessConfigOptions,
|
|
||||||
ConfigCategory,
|
ConfigCategory,
|
||||||
ConfigOption,
|
ConfigOption,
|
||||||
ConfigOptionType,
|
ConfigOptionType,
|
||||||
PaperlessConfig,
|
PaperlessConfig,
|
||||||
|
PaperlessConfigOptions,
|
||||||
} from 'src/app/data/paperless-config'
|
} from 'src/app/data/paperless-config'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { FileComponent } from '../../common/input/file/file.component'
|
||||||
|
import { NumberComponent } from '../../common/input/number/number.component'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
|
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||||
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-config',
|
selector: 'pngx-config',
|
||||||
templateUrl: './config.component.html',
|
templateUrl: './config.component.html',
|
||||||
styleUrl: './config.component.scss',
|
styleUrl: './config.component.scss',
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
SelectComponent,
|
||||||
|
SwitchComponent,
|
||||||
|
TextComponent,
|
||||||
|
NumberComponent,
|
||||||
|
FileComponent,
|
||||||
|
AsyncPipe,
|
||||||
|
NgbNavModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ConfigComponent
|
export class ConfigComponent
|
||||||
extends ComponentWithPermissions
|
extends LoadingComponentWithPermissions
|
||||||
implements OnInit, OnDestroy, DirtyComponent
|
implements OnInit, OnDestroy, DirtyComponent
|
||||||
{
|
{
|
||||||
public readonly ConfigOptionType = ConfigOptionType
|
public readonly ConfigOptionType = ConfigOptionType
|
||||||
@@ -45,15 +72,11 @@ export class ConfigComponent
|
|||||||
return PaperlessConfigOptions.filter((o) => o.category === category)
|
return PaperlessConfigOptions.filter((o) => o.category === category)
|
||||||
}
|
}
|
||||||
|
|
||||||
public loading: boolean = false
|
|
||||||
|
|
||||||
initialConfig: PaperlessConfig
|
initialConfig: PaperlessConfig
|
||||||
store: BehaviorSubject<any>
|
store: BehaviorSubject<any>
|
||||||
storeSub: Subscription
|
storeSub: Subscription
|
||||||
isDirty$: Observable<boolean>
|
isDirty$: Observable<boolean>
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
@@ -67,7 +90,6 @@ export class ConfigComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loading = true
|
|
||||||
this.configService
|
this.configService
|
||||||
.getConfig()
|
.getConfig()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
info="Review the log files for the application and for email checking."
|
info="Review the log files for the application and for email checking."
|
||||||
i18n-info>
|
i18n-info>
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
|
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@if (isLoading || !logFiles.length) {
|
@if (loading || !logFiles.length) {
|
||||||
<div class="ps-2 d-flex align-items-center">
|
<div class="ps-2 d-flex align-items-center">
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
@if (!logFiles.length) {
|
@if (!logFiles.length) {
|
||||||
@@ -30,15 +30,13 @@
|
|||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||||
|
|
||||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
||||||
@if (isLoading && logFiles.length) {
|
@if (loading && logFiles.length) {
|
||||||
<div>
|
<div>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
<ng-container i18n>Loading...</ng-container>
|
<ng-container i18n>Loading...</ng-container>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@for (log of logs; track log) {
|
@for (log of logs; track $index) {
|
||||||
<p
|
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
|
||||||
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
|
|
||||||
>{{log}}</p>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,18 +1,13 @@
|
|||||||
import {
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
ComponentFixture,
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
TestBed,
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
fakeAsync,
|
import { BrowserModule, By } from '@angular/platform-browser'
|
||||||
tick,
|
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
||||||
} from '@angular/core/testing'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { of, throwError } from 'rxjs'
|
||||||
import { LogService } from 'src/app/services/rest/log.service'
|
import { LogService } from 'src/app/services/rest/log.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { LogsComponent } from './logs.component'
|
import { LogsComponent } from './logs.component'
|
||||||
import { of, throwError } from 'rxjs'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { BrowserModule, By } from '@angular/platform-browser'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
const paperless_logs = [
|
const paperless_logs = [
|
||||||
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
||||||
@@ -37,11 +32,12 @@ describe('LogsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [LogsComponent, PageHeaderComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
LogsComponent,
|
||||||
|
PageHeaderComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
@@ -90,8 +86,7 @@ describe('LogsComponent', () => {
|
|||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
component.toggleAutoRefresh()
|
component.autoRefreshEnabled = false
|
||||||
expect(component.autoRefreshInterval).toBeNull()
|
|
||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
@@ -1,24 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
OnDestroy,
|
|
||||||
ChangeDetectorRef,
|
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { filter, takeUntil, timer } from 'rxjs'
|
||||||
import { LogService } from 'src/app/services/rest/log.service'
|
import { LogService } from 'src/app/services/rest/log.service'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-logs',
|
selector: 'pngx-logs',
|
||||||
templateUrl: './logs.component.html',
|
templateUrl: './logs.component.html',
|
||||||
styleUrls: ['./logs.component.scss'],
|
styleUrls: ['./logs.component.scss'],
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
NgbNavModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class LogsComponent implements OnInit, OnDestroy {
|
export class LogsComponent
|
||||||
|
extends LoadingComponentWithPermissions
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private changedetectorRef: ChangeDetectorRef
|
private changedetectorRef: ChangeDetectorRef
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
public logs: string[] = []
|
public logs: string[] = []
|
||||||
|
|
||||||
@@ -26,50 +41,50 @@ export class LogsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
public activeLog: string
|
public activeLog: string
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
public autoRefreshEnabled: boolean = true
|
||||||
|
|
||||||
public isLoading: boolean = false
|
|
||||||
|
|
||||||
public autoRefreshInterval: any
|
|
||||||
|
|
||||||
@ViewChild('logContainer') logContainer: ElementRef
|
@ViewChild('logContainer') logContainer: ElementRef
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isLoading = true
|
|
||||||
this.logService
|
this.logService
|
||||||
.list()
|
.list()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result) => {
|
.subscribe((result) => {
|
||||||
this.logFiles = result
|
this.logFiles = result
|
||||||
this.isLoading = false
|
this.loading = false
|
||||||
if (this.logFiles.length > 0) {
|
if (this.logFiles.length > 0) {
|
||||||
this.activeLog = this.logFiles[0]
|
this.activeLog = this.logFiles[0]
|
||||||
this.reloadLogs()
|
this.reloadLogs()
|
||||||
}
|
}
|
||||||
this.toggleAutoRefresh()
|
timer(5000, 5000)
|
||||||
|
.pipe(
|
||||||
|
filter(() => this.autoRefreshEnabled),
|
||||||
|
takeUntil(this.unsubscribeNotifier)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.reloadLogs()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.unsubscribeNotifier.next(true)
|
super.ngOnDestroy()
|
||||||
this.unsubscribeNotifier.complete()
|
|
||||||
clearInterval(this.autoRefreshInterval)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadLogs() {
|
reloadLogs() {
|
||||||
this.isLoading = true
|
this.loading = true
|
||||||
this.logService
|
this.logService
|
||||||
.get(this.activeLog)
|
.get(this.activeLog)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
this.logs = result
|
this.logs = result
|
||||||
this.isLoading = false
|
this.loading = false
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.logs = []
|
this.logs = []
|
||||||
this.isLoading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -96,15 +111,4 @@ export class LogsComponent implements OnInit, OnDestroy {
|
|||||||
behavior: 'auto',
|
behavior: 'auto',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAutoRefresh(): void {
|
|
||||||
if (this.autoRefreshInterval) {
|
|
||||||
clearInterval(this.autoRefreshInterval)
|
|
||||||
this.autoRefreshInterval = null
|
|
||||||
} else {
|
|
||||||
this.autoRefreshInterval = setInterval(() => {
|
|
||||||
this.reloadLogs()
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<pngx-page-header
|
<pngx-page-header
|
||||||
title="Settings"
|
title="Settings"
|
||||||
i18n-title
|
i18n-title
|
||||||
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
|
info="Options to customize appearance, notifications and more. Settings apply to the <strong>current user only</strong>."
|
||||||
i18n-info
|
i18n-info
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
||||||
@@ -39,193 +39,204 @@
|
|||||||
<li [ngbNavItem]="SettingsNavIDs.General">
|
<li [ngbNavItem]="SettingsNavIDs.General">
|
||||||
<a ngbNavLink i18n>General</a>
|
<a ngbNavLink i18n>General</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
<div class="row">
|
||||||
<h4 i18n>Appearance</h4>
|
<div class="col-xl-6 pe-xl-5">
|
||||||
|
<h4 i18n>Appearance</h4>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
<span i18n>Display language</span>
|
<span i18n>Display language</span>
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<select class="form-select" formControlName="displayLanguage">
|
|
||||||
@for (lang of displayLanguageOptions; track lang) {
|
|
||||||
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code && currentLocale !== 'en-US') {
|
|
||||||
<span> - {{lang.englishName}}</span>
|
|
||||||
}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
@if (displayLanguageIsDirty) {
|
|
||||||
<small class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Date display</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<select class="form-select" formControlName="dateLocale">
|
|
||||||
@for (lang of dateLocaleOptions; track lang) {
|
|
||||||
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code) {
|
|
||||||
<span> - {{today | customDate:'shortDate':null:lang.code}}</span>
|
|
||||||
}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Date format</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="radio" id="dateFormatShort" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="shortDate">
|
|
||||||
<label class="form-check-label" for="dateFormatShort" i18n>Short: {{today | customDate:'shortDate':null:computedDateLocale}}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="radio" id="dateFormatMedium" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="mediumDate">
|
|
||||||
<label class="form-check-label" for="dateFormatMedium" i18n>Medium: {{today | customDate:'mediumDate':null:computedDateLocale}}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="radio" id="dateFormatLong" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="longDate">
|
|
||||||
<label class="form-check-label" for="dateFormatLong" i18n>Long: {{today | customDate:'longDate':null:computedDateLocale}}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Items per page</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<select class="form-select" formControlName="documentListItemPerPage">
|
|
||||||
<option [ngValue]="10">10</option>
|
|
||||||
<option [ngValue]="25">25</option>
|
|
||||||
<option [ngValue]="50">50</option>
|
|
||||||
<option [ngValue]="100">100</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Document editor</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Sidebar</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<pngx-input-check i18n-title title="Use 'slim' sidebar (icons only)" formControlName="slimSidebarEnabled"></pngx-input-check>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Dark mode</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<pngx-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem"></pngx-input-check>
|
|
||||||
<pngx-input-check [hidden]="settingsForm.value.darkModeUseSystem" i18n-title title="Enable dark mode" formControlName="darkModeEnabled"></pngx-input-check>
|
|
||||||
<pngx-input-check i18n-title title="Invert thumbnails in dark mode" formControlName="darkModeInvertThumbs"></pngx-input-check>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3 col-form-label pt-0">
|
|
||||||
<span i18n>Theme Color</span>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-3">
|
|
||||||
<pngx-input-color i18n-title formControlName="themeColor" [error]="error?.color"></pngx-input-color>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
|
||||||
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="offset-md-3 col">
|
|
||||||
<p i18n>
|
|
||||||
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
|
|
||||||
Actual updating of the app must still be performed manually.
|
|
||||||
</p>
|
|
||||||
<p i18n>
|
|
||||||
<em>No tracking data is collected by the app in any way.</em>
|
|
||||||
</p>
|
|
||||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Document editing</h4>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="offset-md-3 col">
|
|
||||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="offset-md-3 col">
|
|
||||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs"></pngx-input-check>
|
|
||||||
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Global search</h4>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="offset-md-3 col">
|
|
||||||
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="offset-md-3 col">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-2 col-form-label pt-0">
|
|
||||||
<span i18n>Full search links to</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="searchLink">
|
|
||||||
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
|
<select class="form-select" formControlName="displayLanguage">
|
||||||
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
|
@for (lang of displayLanguageOptions; track lang) {
|
||||||
|
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code && currentLocale !== 'en-US') {
|
||||||
|
<span> - {{lang.englishName}}</span>
|
||||||
|
}</option>
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
@if (displayLanguageIsDirty) {
|
||||||
|
<small class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Date display</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
|
||||||
|
<select class="form-select" formControlName="dateLocale">
|
||||||
|
@for (lang of dateLocaleOptions; track lang) {
|
||||||
|
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code) {
|
||||||
|
<span> - {{today | customDate:'shortDate':null:lang.code}}</span>
|
||||||
|
}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Date format</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="radio" id="dateFormatShort" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="shortDate">
|
||||||
|
<label class="form-check-label" for="dateFormatShort" i18n>Short: {{today | customDate:'shortDate':null:computedDateLocale}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="radio" id="dateFormatMedium" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="mediumDate">
|
||||||
|
<label class="form-check-label" for="dateFormatMedium" i18n>Medium: {{today | customDate:'mediumDate':null:computedDateLocale}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="radio" id="dateFormatLong" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="longDate">
|
||||||
|
<label class="form-check-label" for="dateFormatLong" i18n>Long: {{today | customDate:'longDate':null:computedDateLocale}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Items per page</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
|
||||||
|
<select class="form-select" formControlName="documentListItemPerPage">
|
||||||
|
<option [ngValue]="10">10</option>
|
||||||
|
<option [ngValue]="25">25</option>
|
||||||
|
<option [ngValue]="50">50</option>
|
||||||
|
<option [ngValue]="100">100</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Sidebar</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
|
||||||
|
<pngx-input-check i18n-title title="Use 'slim' sidebar (icons only)" formControlName="slimSidebarEnabled"></pngx-input-check>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Dark mode</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem"></pngx-input-check>
|
||||||
|
<pngx-input-check [hidden]="settingsForm.value.darkModeUseSystem" i18n-title title="Enable dark mode" formControlName="darkModeEnabled"></pngx-input-check>
|
||||||
|
<pngx-input-check i18n-title title="Invert thumbnails in dark mode" formControlName="darkModeInvertThumbs"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Theme Color</span>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-4">
|
||||||
|
<pngx-input-color i18n-title formControlName="themeColor" [error]="error?.color"></pngx-input-color>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
||||||
|
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4" i18n>Document editing</h4>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-xl-6 ps-xl-5">
|
||||||
|
<h4 class="mt-4 mt-md-0" id="update-checking" i18n>Update checking</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col d-flex flex-row align-items-start">
|
||||||
|
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||||
|
<button class="btn btn-sm btn-link text-muted me-auto p-0 ms-2" title="What's this?" i18n-title type="button" triggers="mouseenter:mouseleave" [ngbPopover]="updatesPopover" [autoClose]="true">
|
||||||
|
<i-bs name="question-circle"></i-bs>
|
||||||
|
</button>
|
||||||
|
<ng-template #updatesPopover>
|
||||||
|
<p i18n>
|
||||||
|
Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em i18n>No tracking data is collected by the app in any way.</em>
|
||||||
|
</p>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Notes</h4>
|
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs"></pngx-input-check>
|
||||||
|
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<h4 class="mt-4" i18n>Global search</h4>
|
||||||
<div class="offset-md-3 col">
|
<div class="row mb-3">
|
||||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Full search links to</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<select class="form-select" formControlName="searchLink">
|
||||||
|
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
|
||||||
|
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4" i18n>Saved Views</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4" i18n>Notes</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -239,7 +250,7 @@
|
|||||||
<h4 i18n>Default Permissions</h4>
|
<h4 i18n>Default Permissions</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="col">
|
||||||
<p i18n>
|
<p i18n>
|
||||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||||
</p>
|
</p>
|
||||||
@@ -321,7 +332,7 @@
|
|||||||
<h4 i18n>Document processing</h4>
|
<h4 i18n>Document processing</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="col">
|
||||||
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
|
||||||
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
|
||||||
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
|
||||||
@@ -331,87 +342,6 @@
|
|||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }">
|
|
||||||
<a ngbNavLink i18n>Saved views</a>
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
|
|
||||||
<h4 i18n>Settings</h4>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="offset-md-3 col">
|
|
||||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 i18n>Views</h4>
|
|
||||||
<ul class="list-group" formGroupName="savedViews">
|
|
||||||
|
|
||||||
@for (view of savedViews; track view) {
|
|
||||||
<li class="list-group-item py-3">
|
|
||||||
<div [formGroupName]="view.id">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-check form-switch mt-3">
|
|
||||||
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
|
||||||
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
|
||||||
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
|
||||||
<pngx-confirm-button
|
|
||||||
label="Delete"
|
|
||||||
i18n-label
|
|
||||||
(confirm)="deleteSavedView(view)"
|
|
||||||
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
|
|
||||||
buttonClasses="btn-sm btn-outline-danger form-control"
|
|
||||||
iconName="trash">
|
|
||||||
</pngx-confirm-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<pngx-input-number i18n-title title="Documents page size" [showAdd]="false" formControlName="page_size"></pngx-input-number>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
|
|
||||||
<select class="form-select" formControlName="display_mode">
|
|
||||||
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
|
|
||||||
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
|
|
||||||
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
@if (displayFields) {
|
|
||||||
<pngx-input-drag-drop-select i18n-title title="Show" i18n-emptyText emptyText="Default" [items]="displayFields" formControlName="display_fields"></pngx-input-drag-drop-select>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (savedViews && savedViews.length === 0) {
|
|
||||||
<li class="list-group-item">
|
|
||||||
<div i18n>No saved views defined.</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!savedViews) {
|
|
||||||
<li class="list-group-item">
|
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</ng-template>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
|
@@ -1,35 +1,45 @@
|
|||||||
import { ViewportScroller, DatePipe } from '@angular/common'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
|
import { DatePipe, ViewportScroller } from '@angular/common'
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
|
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import {
|
import {
|
||||||
NgbModule,
|
|
||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgbNavLink,
|
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
NgbModule,
|
||||||
|
NgbNavLink,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import {
|
||||||
|
InstallType,
|
||||||
|
SystemStatus,
|
||||||
|
SystemStatusItemStatus,
|
||||||
|
} from 'src/app/data/system-status'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService, Toast } from 'src/app/services/toast.service'
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
|
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
import { ColorComponent } from '../../common/input/color/color.component'
|
import { ColorComponent } from '../../common/input/color/color.component'
|
||||||
|
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
|
||||||
import { NumberComponent } from '../../common/input/number/number.component'
|
import { NumberComponent } from '../../common/input/number/number.component'
|
||||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||||
@@ -37,25 +47,9 @@ import { SelectComponent } from '../../common/input/select/select.component'
|
|||||||
import { TagsComponent } from '../../common/input/tags/tags.component'
|
import { TagsComponent } from '../../common/input/tags/tags.component'
|
||||||
import { TextComponent } from '../../common/input/text/text.component'
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { SettingsComponent } from './settings.component'
|
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
|
||||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
import { SettingsComponent } from './settings.component'
|
||||||
import {
|
|
||||||
SystemStatus,
|
|
||||||
InstallType,
|
|
||||||
SystemStatusItemStatus,
|
|
||||||
} from 'src/app/data/system-status'
|
|
||||||
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
|
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
const savedViews = [
|
|
||||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
|
||||||
{ id: 2, name: 'view2', show_in_sidebar: false, show_on_dashboard: false },
|
|
||||||
]
|
|
||||||
const users = [
|
const users = [
|
||||||
{ id: 1, username: 'user1', is_superuser: false },
|
{ id: 1, username: 'user1', is_superuser: false },
|
||||||
{ id: 2, username: 'user2', is_superuser: false },
|
{ id: 2, username: 'user2', is_superuser: false },
|
||||||
@@ -70,7 +64,6 @@ describe('SettingsComponent', () => {
|
|||||||
let fixture: ComponentFixture<SettingsComponent>
|
let fixture: ComponentFixture<SettingsComponent>
|
||||||
let router: Router
|
let router: Router
|
||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
let savedViewService: SavedViewService
|
|
||||||
let activatedRoute: ActivatedRoute
|
let activatedRoute: ActivatedRoute
|
||||||
let viewportScroller: ViewportScroller
|
let viewportScroller: ViewportScroller
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
@@ -82,7 +75,16 @@ describe('SettingsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
imports: [
|
||||||
|
NgbModule,
|
||||||
|
RouterTestingModule.withRoutes(routes),
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgbAlertModule,
|
||||||
|
NgSelectModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
NgbModalModule,
|
||||||
|
DragDropModule,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
@@ -101,17 +103,6 @@ describe('SettingsComponent', () => {
|
|||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
DragDropSelectComponent,
|
DragDropSelectComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
|
||||||
NgbModule,
|
|
||||||
RouterTestingModule.withRoutes(routes),
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
NgbAlertModule,
|
|
||||||
NgSelectModule,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
NgbModalModule,
|
|
||||||
DragDropModule,
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
@@ -139,7 +130,6 @@ describe('SettingsComponent', () => {
|
|||||||
.spyOn(permissionsService, 'currentUserOwnsObject')
|
.spyOn(permissionsService, 'currentUserOwnsObject')
|
||||||
.mockReturnValue(true)
|
.mockReturnValue(true)
|
||||||
groupService = TestBed.inject(GroupService)
|
groupService = TestBed.inject(GroupService)
|
||||||
savedViewService = TestBed.inject(SavedViewService)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function completeSetup(excludeService = null) {
|
function completeSetup(excludeService = null) {
|
||||||
@@ -161,15 +151,6 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (excludeService !== savedViewService) {
|
|
||||||
jest.spyOn(savedViewService, 'listAll').mockReturnValue(
|
|
||||||
of({
|
|
||||||
all: savedViews.map((v) => v.id),
|
|
||||||
count: savedViews.length,
|
|
||||||
results: (savedViews as SavedView[]).concat([]),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SettingsComponent)
|
fixture = TestBed.createComponent(SettingsComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
@@ -184,8 +165,6 @@ describe('SettingsComponent', () => {
|
|||||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
|
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
|
||||||
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
|
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
|
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
|
||||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
|
|
||||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'savedviews'])
|
|
||||||
|
|
||||||
const initSpy = jest.spyOn(component, 'initialize')
|
const initSpy = jest.spyOn(component, 'initialize')
|
||||||
component.isDirty = true // mock dirty
|
component.isDirty = true // mock dirty
|
||||||
@@ -213,90 +192,8 @@ describe('SettingsComponent', () => {
|
|||||||
expect(scrollSpy).toHaveBeenCalledWith('#notifications')
|
expect(scrollSpy).toHaveBeenCalledWith('#notifications')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should enable organizing of sidebar saved views even on direct navigation', () => {
|
|
||||||
completeSetup()
|
|
||||||
jest
|
|
||||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
|
||||||
.mockReturnValue(of(convertToParamMap({ section: 'savedviews' })))
|
|
||||||
activatedRoute.snapshot.fragment = '#savedviews'
|
|
||||||
component.ngOnInit()
|
|
||||||
expect(component.activeNavID).toEqual(4) // Saved Views
|
|
||||||
component.ngAfterViewInit()
|
|
||||||
expect(settingsService.organizingSidebarSavedViews).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support save saved views, show error', () => {
|
|
||||||
completeSetup()
|
|
||||||
|
|
||||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
|
||||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
|
||||||
const toastSpy = jest.spyOn(toastService, 'show')
|
|
||||||
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
|
|
||||||
|
|
||||||
const toggle = fixture.debugElement.query(
|
|
||||||
By.css('.form-check.form-switch input')
|
|
||||||
)
|
|
||||||
toggle.nativeElement.checked = true
|
|
||||||
toggle.nativeElement.dispatchEvent(new Event('change'))
|
|
||||||
|
|
||||||
// saved views error first
|
|
||||||
savedViewPatchSpy.mockReturnValueOnce(
|
|
||||||
throwError(() => new Error('unable to save saved views'))
|
|
||||||
)
|
|
||||||
component.saveSettings()
|
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
|
||||||
expect(savedViewPatchSpy).toHaveBeenCalled()
|
|
||||||
toastSpy.mockClear()
|
|
||||||
toastErrorSpy.mockClear()
|
|
||||||
savedViewPatchSpy.mockClear()
|
|
||||||
|
|
||||||
// succeed saved views
|
|
||||||
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
|
|
||||||
component.saveSettings()
|
|
||||||
expect(toastErrorSpy).not.toHaveBeenCalled()
|
|
||||||
expect(savedViewPatchSpy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update only patch saved views that have changed', () => {
|
|
||||||
completeSetup()
|
|
||||||
|
|
||||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
|
||||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
|
|
||||||
component.saveSettings()
|
|
||||||
expect(patchSpy).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
const view = savedViews[0]
|
|
||||||
const toggle = fixture.debugElement.query(
|
|
||||||
By.css('.form-check.form-switch input')
|
|
||||||
)
|
|
||||||
toggle.nativeElement.checked = true
|
|
||||||
toggle.nativeElement.dispatchEvent(new Event('change'))
|
|
||||||
// register change
|
|
||||||
component.savedViewGroup.get(view.id.toString()).value[
|
|
||||||
'show_on_dashboard'
|
|
||||||
] = !view.show_on_dashboard
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
component.saveSettings()
|
|
||||||
expect(patchSpy).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
id: view.id,
|
|
||||||
name: view.name,
|
|
||||||
show_in_sidebar: view.show_in_sidebar,
|
|
||||||
show_on_dashboard: !view.show_on_dashboard,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support save local settings updating appearance settings and calling API, show error', () => {
|
it('should support save local settings updating appearance settings and calling API, show error', () => {
|
||||||
completeSetup()
|
completeSetup()
|
||||||
jest.spyOn(savedViewService, 'patchMany').mockReturnValue(of([]))
|
|
||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
const toastSpy = jest.spyOn(toastService, 'show')
|
const toastSpy = jest.spyOn(toastService, 'show')
|
||||||
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
|
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
|
||||||
@@ -315,7 +212,7 @@ describe('SettingsComponent', () => {
|
|||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toHaveBeenCalled()
|
||||||
expect(storeSpy).toHaveBeenCalled()
|
expect(storeSpy).toHaveBeenCalled()
|
||||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||||
expect(setSpy).toHaveBeenCalledTimes(27)
|
expect(setSpy).toHaveBeenCalledTimes(28)
|
||||||
|
|
||||||
// succeed
|
// succeed
|
||||||
storeSpy.mockReturnValueOnce(of(true))
|
storeSpy.mockReturnValueOnce(of(true))
|
||||||
@@ -326,7 +223,6 @@ describe('SettingsComponent', () => {
|
|||||||
|
|
||||||
it('should offer reload if settings changes require', () => {
|
it('should offer reload if settings changes require', () => {
|
||||||
completeSetup()
|
completeSetup()
|
||||||
jest.spyOn(savedViewService, 'patchMany').mockReturnValue(of([]))
|
|
||||||
let toast: Toast
|
let toast: Toast
|
||||||
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
||||||
component.initialize(true) // reset
|
component.initialize(true) // reset
|
||||||
@@ -361,18 +257,6 @@ describe('SettingsComponent', () => {
|
|||||||
component.clearThemeColor()
|
component.clearThemeColor()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support delete saved view', () => {
|
|
||||||
completeSetup()
|
|
||||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
|
||||||
const deleteSpy = jest.spyOn(savedViewService, 'delete')
|
|
||||||
deleteSpy.mockReturnValue(of(true))
|
|
||||||
component.deleteSavedView(savedViews[0] as SavedView)
|
|
||||||
expect(deleteSpy).toHaveBeenCalled()
|
|
||||||
expect(toastSpy).toHaveBeenCalledWith(
|
|
||||||
`Saved view "${savedViews[0].name}" deleted.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should show errors on load if load users failure', () => {
|
it('should show errors on load if load users failure', () => {
|
||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
jest
|
jest
|
||||||
|
@@ -1,56 +1,69 @@
|
|||||||
import { ViewportScroller } from '@angular/common'
|
import { AsyncPipe, ViewportScroller } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
Component,
|
|
||||||
OnInit,
|
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
OnDestroy,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
LOCALE_ID,
|
LOCALE_ID,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormGroup, FormControl } from '@angular/forms'
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
NgbNavChangeEvent,
|
NgbNavChangeEvent,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Subscription,
|
|
||||||
Observable,
|
Observable,
|
||||||
Subject,
|
Subject,
|
||||||
|
Subscription,
|
||||||
first,
|
first,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import {
|
||||||
|
SystemStatus,
|
||||||
|
SystemStatusItemStatus,
|
||||||
|
} from 'src/app/data/system-status'
|
||||||
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import {
|
import {
|
||||||
PermissionsService,
|
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import {
|
import {
|
||||||
SettingsService,
|
|
||||||
LanguageOption,
|
LanguageOption,
|
||||||
|
SettingsService,
|
||||||
} from 'src/app/services/settings.service'
|
} from 'src/app/services/settings.service'
|
||||||
import { ToastService, Toast } from 'src/app/services/toast.service'
|
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
|
||||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
|
||||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
import {
|
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||||
SystemStatusItemStatus,
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
SystemStatus,
|
import { ColorComponent } from '../../common/input/color/color.component'
|
||||||
} from 'src/app/data/system-status'
|
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||||
import { DisplayMode } from 'src/app/data/document'
|
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
General = 1,
|
General = 1,
|
||||||
@@ -69,15 +82,28 @@ const systemDateFormat = {
|
|||||||
selector: 'pngx-settings',
|
selector: 'pngx-settings',
|
||||||
templateUrl: './settings.component.html',
|
templateUrl: './settings.component.html',
|
||||||
styleUrls: ['./settings.component.scss'],
|
styleUrls: ['./settings.component.scss'],
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
CheckComponent,
|
||||||
|
ColorComponent,
|
||||||
|
SelectComponent,
|
||||||
|
PermissionsGroupComponent,
|
||||||
|
PermissionsUserComponent,
|
||||||
|
CustomDatePipe,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
AsyncPipe,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class SettingsComponent
|
export class SettingsComponent
|
||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||||
{
|
{
|
||||||
activeNavID: number
|
activeNavID: number
|
||||||
DisplayMode = DisplayMode
|
|
||||||
|
|
||||||
savedViewGroup = new FormGroup({})
|
|
||||||
|
|
||||||
settingsForm = new FormGroup({
|
settingsForm = new FormGroup({
|
||||||
bulkEditConfirmationDialogs: new FormControl(null),
|
bulkEditConfirmationDialogs: new FormControl(null),
|
||||||
@@ -88,7 +114,6 @@ export class SettingsComponent
|
|||||||
darkModeEnabled: new FormControl(null),
|
darkModeEnabled: new FormControl(null),
|
||||||
darkModeInvertThumbs: new FormControl(null),
|
darkModeInvertThumbs: new FormControl(null),
|
||||||
themeColor: new FormControl(null),
|
themeColor: new FormControl(null),
|
||||||
useNativePdfViewer: new FormControl(null),
|
|
||||||
displayLanguage: new FormControl(null),
|
displayLanguage: new FormControl(null),
|
||||||
dateLocale: new FormControl(null),
|
dateLocale: new FormControl(null),
|
||||||
dateFormat: new FormControl(null),
|
dateFormat: new FormControl(null),
|
||||||
@@ -99,7 +124,9 @@ export class SettingsComponent
|
|||||||
defaultPermsViewGroups: new FormControl(null),
|
defaultPermsViewGroups: new FormControl(null),
|
||||||
defaultPermsEditUsers: new FormControl(null),
|
defaultPermsEditUsers: new FormControl(null),
|
||||||
defaultPermsEditGroups: new FormControl(null),
|
defaultPermsEditGroups: new FormControl(null),
|
||||||
|
useNativePdfViewer: new FormControl(null),
|
||||||
documentEditingRemoveInboxTags: new FormControl(null),
|
documentEditingRemoveInboxTags: new FormControl(null),
|
||||||
|
documentEditingOverlayThumbnail: new FormControl(null),
|
||||||
searchDbOnly: new FormControl(null),
|
searchDbOnly: new FormControl(null),
|
||||||
searchLink: new FormControl(null),
|
searchLink: new FormControl(null),
|
||||||
|
|
||||||
@@ -109,14 +136,9 @@ export class SettingsComponent
|
|||||||
notificationsConsumerSuppressOnDashboard: new FormControl(null),
|
notificationsConsumerSuppressOnDashboard: new FormControl(null),
|
||||||
|
|
||||||
savedViewsWarnOnUnsavedChange: new FormControl(null),
|
savedViewsWarnOnUnsavedChange: new FormControl(null),
|
||||||
savedViews: this.savedViewGroup,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
savedViews: SavedView[]
|
|
||||||
SettingsNavIDs = SettingsNavIDs
|
SettingsNavIDs = SettingsNavIDs
|
||||||
get displayFields() {
|
|
||||||
return this.settings.allDisplayFields
|
|
||||||
}
|
|
||||||
|
|
||||||
store: BehaviorSubject<any>
|
store: BehaviorSubject<any>
|
||||||
storeSub: Subscription
|
storeSub: Subscription
|
||||||
@@ -151,7 +173,6 @@ export class SettingsComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public savedViewService: SavedViewService,
|
|
||||||
private documentListViewService: DocumentListViewService,
|
private documentListViewService: DocumentListViewService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private settings: SettingsService,
|
private settings: SettingsService,
|
||||||
@@ -213,18 +234,6 @@ export class SettingsComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
PermissionType.SavedView
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.savedViewService.listAll().subscribe((r) => {
|
|
||||||
this.savedViews = r.results
|
|
||||||
this.initialize(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activatedRoute.paramMap.subscribe((paramMap) => {
|
this.activatedRoute.paramMap.subscribe((paramMap) => {
|
||||||
const section = paramMap.get('section')
|
const section = paramMap.get('section')
|
||||||
if (section) {
|
if (section) {
|
||||||
@@ -234,9 +243,6 @@ export class SettingsComponent
|
|||||||
if (navIDKey) {
|
if (navIDKey) {
|
||||||
this.activeNavID = SettingsNavIDs[navIDKey]
|
this.activeNavID = SettingsNavIDs[navIDKey]
|
||||||
}
|
}
|
||||||
if (this.activeNavID === SettingsNavIDs.SavedViews) {
|
|
||||||
this.settings.organizingSidebarSavedViews = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -308,9 +314,11 @@ export class SettingsComponent
|
|||||||
documentEditingRemoveInboxTags: this.settings.get(
|
documentEditingRemoveInboxTags: this.settings.get(
|
||||||
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
|
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
|
||||||
),
|
),
|
||||||
|
documentEditingOverlayThumbnail: this.settings.get(
|
||||||
|
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL
|
||||||
|
),
|
||||||
searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY),
|
searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY),
|
||||||
searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE),
|
searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE),
|
||||||
savedViews: {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,15 +331,11 @@ export class SettingsComponent
|
|||||||
this.router
|
this.router
|
||||||
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
||||||
.then((navigated) => {
|
.then((navigated) => {
|
||||||
this.settings.organizingSidebarSavedViews = false
|
|
||||||
if (!navigated && this.isDirty) {
|
if (!navigated && this.isDirty) {
|
||||||
this.activeNavID = navChangeEvent.activeId
|
this.activeNavID = navChangeEvent.activeId
|
||||||
} else if (navigated && this.isDirty) {
|
} else if (navigated && this.isDirty) {
|
||||||
this.initialize()
|
this.initialize()
|
||||||
}
|
}
|
||||||
if (this.activeNavID === SettingsNavIDs.SavedViews) {
|
|
||||||
this.settings.organizingSidebarSavedViews = true
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,34 +346,6 @@ export class SettingsComponent
|
|||||||
|
|
||||||
let storeData = this.getCurrentSettings()
|
let storeData = this.getCurrentSettings()
|
||||||
|
|
||||||
if (this.savedViews) {
|
|
||||||
this.emptyGroup(this.savedViewGroup)
|
|
||||||
|
|
||||||
for (let view of this.savedViews) {
|
|
||||||
storeData.savedViews[view.id.toString()] = {
|
|
||||||
id: view.id,
|
|
||||||
name: view.name,
|
|
||||||
show_on_dashboard: view.show_on_dashboard,
|
|
||||||
show_in_sidebar: view.show_in_sidebar,
|
|
||||||
page_size: view.page_size,
|
|
||||||
display_mode: view.display_mode,
|
|
||||||
display_fields: view.display_fields,
|
|
||||||
}
|
|
||||||
this.savedViewGroup.addControl(
|
|
||||||
view.id.toString(),
|
|
||||||
new FormGroup({
|
|
||||||
id: new FormControl(null),
|
|
||||||
name: new FormControl(null),
|
|
||||||
show_on_dashboard: new FormControl(null),
|
|
||||||
show_in_sidebar: new FormControl(null),
|
|
||||||
page_size: new FormControl(null),
|
|
||||||
display_mode: new FormControl(null),
|
|
||||||
display_fields: new FormControl([]),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store = new BehaviorSubject(storeData)
|
this.store = new BehaviorSubject(storeData)
|
||||||
|
|
||||||
this.storeSub = this.store.asObservable().subscribe((state) => {
|
this.storeSub = this.store.asObservable().subscribe((state) => {
|
||||||
@@ -409,32 +385,12 @@ export class SettingsComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private emptyGroup(group: FormGroup) {
|
|
||||||
Object.keys(group.controls).forEach((key) => group.removeControl(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
|
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
|
||||||
this.storeSub && this.storeSub.unsubscribe()
|
this.storeSub && this.storeSub.unsubscribe()
|
||||||
this.settings.organizingSidebarSavedViews = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSavedView(savedView: SavedView) {
|
public saveSettings() {
|
||||||
this.savedViewService.delete(savedView).subscribe(() => {
|
|
||||||
this.savedViewGroup.removeControl(savedView.id.toString())
|
|
||||||
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
|
|
||||||
this.toastService.showInfo(
|
|
||||||
$localize`Saved view "${savedView.name}" deleted.`
|
|
||||||
)
|
|
||||||
this.savedViewService.clearCache()
|
|
||||||
this.savedViewService.listAll().subscribe((r) => {
|
|
||||||
this.savedViews = r.results
|
|
||||||
this.initialize(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveLocalSettings() {
|
|
||||||
this.savePending = true
|
this.savePending = true
|
||||||
const reloadRequired =
|
const reloadRequired =
|
||||||
this.settingsForm.value.displayLanguage !=
|
this.settingsForm.value.displayLanguage !=
|
||||||
@@ -539,6 +495,10 @@ export class SettingsComponent
|
|||||||
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
|
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
|
||||||
this.settingsForm.value.documentEditingRemoveInboxTags
|
this.settingsForm.value.documentEditingRemoveInboxTags
|
||||||
)
|
)
|
||||||
|
this.settings.set(
|
||||||
|
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL,
|
||||||
|
this.settingsForm.value.documentEditingOverlayThumbnail
|
||||||
|
)
|
||||||
this.settings.set(
|
this.settings.set(
|
||||||
SETTINGS_KEYS.SEARCH_DB_ONLY,
|
SETTINGS_KEYS.SEARCH_DB_ONLY,
|
||||||
this.settingsForm.value.searchDbOnly
|
this.settingsForm.value.searchDbOnly
|
||||||
@@ -592,31 +552,6 @@ export class SettingsComponent
|
|||||||
return new Date()
|
return new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
|
||||||
// only patch views that have actually changed
|
|
||||||
const changed: SavedView[] = []
|
|
||||||
Object.values(this.savedViewGroup.controls)
|
|
||||||
.filter((g: FormGroup) => !g.pristine)
|
|
||||||
.forEach((group: FormGroup) => {
|
|
||||||
changed.push(group.value)
|
|
||||||
})
|
|
||||||
if (changed.length > 0) {
|
|
||||||
this.savedViewService.patchMany(changed).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.saveLocalSettings()
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`Error while storing settings on server.`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.saveLocalSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.settingsForm.patchValue(this.store.getValue())
|
this.settingsForm.patchValue(this.store.getValue())
|
||||||
}
|
}
|
||||||
|
@@ -4,15 +4,40 @@
|
|||||||
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
|
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
|
||||||
i18n-info
|
i18n-info
|
||||||
>
|
>
|
||||||
<div class="btn-toolbar col col-md-auto align-items-center">
|
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||||
<i-bs name="check2-all"></i-bs> {{dismissButtonText}}
|
<i-bs name="check2-all"></i-bs> {{dismissButtonText}}
|
||||||
</button>
|
</button>
|
||||||
<div class="form-check form-switch mb-0">
|
<div class="form-inline d-flex align-items-center">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
|
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||||
|
<span class="input-group-text text-muted" i18n>Filter by</span>
|
||||||
|
@if (filterTargets.length > 1) {
|
||||||
|
<div ngbDropdown>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
|
||||||
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
|
@for (t of filterTargets; track t.id) {
|
||||||
|
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<span class="input-group-text">{{filterTargetName}}</span>
|
||||||
|
}
|
||||||
|
@if (filterText?.length) {
|
||||||
|
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
|
||||||
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<input #filterInput class="form-control form-control-sm" type="text"
|
||||||
|
(keyup)="filterInputKeyup($event)"
|
||||||
|
[(ngModel)]="filterText">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mb-0 ms-2">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +68,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
|
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task.id) {
|
||||||
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
||||||
<td>
|
<td>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -118,13 +143,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
|
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange()" (navChange)="beforeTabChange()">
|
||||||
<li ngbNavItem="failed">
|
<li ngbNavItem="failed">
|
||||||
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
||||||
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
||||||
}</a>
|
}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li ngbNavItem="completed">
|
<li ngbNavItem="completed">
|
||||||
@@ -132,7 +157,7 @@
|
|||||||
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
||||||
}</a>
|
}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li ngbNavItem="started">
|
<li ngbNavItem="started">
|
||||||
@@ -140,7 +165,7 @@
|
|||||||
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
|
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
|
||||||
}</a>
|
}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li ngbNavItem="queued">
|
<li ngbNavItem="queued">
|
||||||
@@ -148,7 +173,7 @@
|
|||||||
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
|
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
|
||||||
}</a>
|
}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -26,3 +26,14 @@ pre {
|
|||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group .dropdown .btn {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-10 {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
@@ -1,36 +1,36 @@
|
|||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import {
|
import {
|
||||||
HttpTestingController,
|
HttpTestingController,
|
||||||
provideHttpClientTesting,
|
provideHttpClientTesting,
|
||||||
} from '@angular/common/http/testing'
|
} from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import {
|
import {
|
||||||
NgbModal,
|
NgbModal,
|
||||||
|
NgbModalRef,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
NgbNavItem,
|
NgbNavItem,
|
||||||
NgbModalRef,
|
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import {
|
import {
|
||||||
PaperlessTask,
|
PaperlessTask,
|
||||||
PaperlessTaskType,
|
|
||||||
PaperlessTaskStatus,
|
PaperlessTaskStatus,
|
||||||
|
PaperlessTaskType,
|
||||||
} from 'src/app/data/paperless-task'
|
} from 'src/app/data/paperless-task'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { TasksService } from 'src/app/services/tasks.service'
|
import { TasksService } from 'src/app/services/tasks.service'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { TasksComponent } from './tasks.component'
|
import { TasksComponent, TaskTab } from './tasks.component'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
const tasks: PaperlessTask[] = [
|
const tasks: PaperlessTask[] = [
|
||||||
{
|
{
|
||||||
@@ -119,18 +119,16 @@ describe('TasksComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
|
||||||
TasksComponent,
|
|
||||||
PageHeaderComponent,
|
|
||||||
IfPermissionsDirective,
|
|
||||||
CustomDatePipe,
|
|
||||||
ConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
NgbModule,
|
NgbModule,
|
||||||
RouterTestingModule.withRoutes(routes),
|
RouterTestingModule.withRoutes(routes),
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
TasksComponent,
|
||||||
|
PageHeaderComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
CustomDatePipe,
|
||||||
|
ConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@@ -167,7 +165,7 @@ describe('TasksComponent', () => {
|
|||||||
let currentTasksLength = tasks.filter(
|
let currentTasksLength = tasks.filter(
|
||||||
(t) => t.status === PaperlessTaskStatus.Failed
|
(t) => t.status === PaperlessTaskStatus.Failed
|
||||||
).length
|
).length
|
||||||
component.activeTab = 'failed'
|
component.activeTab = TaskTab.Failed
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(tabButtons[0].nativeElement.textContent).toEqual(
|
expect(tabButtons[0].nativeElement.textContent).toEqual(
|
||||||
`Failed${currentTasksLength}`
|
`Failed${currentTasksLength}`
|
||||||
@@ -179,7 +177,7 @@ describe('TasksComponent', () => {
|
|||||||
currentTasksLength = tasks.filter(
|
currentTasksLength = tasks.filter(
|
||||||
(t) => t.status === PaperlessTaskStatus.Complete
|
(t) => t.status === PaperlessTaskStatus.Complete
|
||||||
).length
|
).length
|
||||||
component.activeTab = 'completed'
|
component.activeTab = TaskTab.Completed
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(tabButtons[1].nativeElement.textContent).toEqual(
|
expect(tabButtons[1].nativeElement.textContent).toEqual(
|
||||||
`Complete${currentTasksLength}`
|
`Complete${currentTasksLength}`
|
||||||
@@ -188,7 +186,7 @@ describe('TasksComponent', () => {
|
|||||||
currentTasksLength = tasks.filter(
|
currentTasksLength = tasks.filter(
|
||||||
(t) => t.status === PaperlessTaskStatus.Started
|
(t) => t.status === PaperlessTaskStatus.Started
|
||||||
).length
|
).length
|
||||||
component.activeTab = 'started'
|
component.activeTab = TaskTab.Started
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(tabButtons[2].nativeElement.textContent).toEqual(
|
expect(tabButtons[2].nativeElement.textContent).toEqual(
|
||||||
`Started${currentTasksLength}`
|
`Started${currentTasksLength}`
|
||||||
@@ -197,7 +195,7 @@ describe('TasksComponent', () => {
|
|||||||
currentTasksLength = tasks.filter(
|
currentTasksLength = tasks.filter(
|
||||||
(t) => t.status === PaperlessTaskStatus.Pending
|
(t) => t.status === PaperlessTaskStatus.Pending
|
||||||
).length
|
).length
|
||||||
component.activeTab = 'queued'
|
component.activeTab = TaskTab.Queued
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(tabButtons[3].nativeElement.textContent).toEqual(
|
expect(tabButtons[3].nativeElement.textContent).toEqual(
|
||||||
`Queued${currentTasksLength}`
|
`Queued${currentTasksLength}`
|
||||||
@@ -206,7 +204,7 @@ describe('TasksComponent', () => {
|
|||||||
|
|
||||||
it('should to go page 1 between tab switch', () => {
|
it('should to go page 1 between tab switch', () => {
|
||||||
component.page = 10
|
component.page = 10
|
||||||
component.duringTabChange(2)
|
component.duringTabChange()
|
||||||
expect(component.page).toEqual(1)
|
expect(component.page).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -283,10 +281,63 @@ describe('TasksComponent', () => {
|
|||||||
expect(reloadSpy).toHaveBeenCalledTimes(1)
|
expect(reloadSpy).toHaveBeenCalledTimes(1)
|
||||||
jest.advanceTimersByTime(5000)
|
jest.advanceTimersByTime(5000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
|
component.autoRefreshEnabled = false
|
||||||
component.toggleAutoRefresh()
|
|
||||||
expect(component.autoRefreshInterval).toBeNull()
|
|
||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should filter tasks by file name', () => {
|
||||||
|
const input = fixture.debugElement.query(
|
||||||
|
By.css('pngx-page-header input[type=text]')
|
||||||
|
)
|
||||||
|
input.nativeElement.value = '191092'
|
||||||
|
input.nativeElement.dispatchEvent(new Event('input'))
|
||||||
|
jest.advanceTimersByTime(150) // debounce time
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.filterText).toEqual('191092')
|
||||||
|
expect(
|
||||||
|
fixture.debugElement.queryAll(By.css('table tbody tr')).length
|
||||||
|
).toEqual(2) // 1 task x 2 lines
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter tasks by result', () => {
|
||||||
|
component.activeTab = TaskTab.Failed
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.filterTargetID = 1
|
||||||
|
const input = fixture.debugElement.query(
|
||||||
|
By.css('pngx-page-header input[type=text]')
|
||||||
|
)
|
||||||
|
input.nativeElement.value = 'duplicate'
|
||||||
|
input.nativeElement.dispatchEvent(new Event('input'))
|
||||||
|
jest.advanceTimersByTime(150) // debounce time
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.filterText).toEqual('duplicate')
|
||||||
|
expect(
|
||||||
|
fixture.debugElement.queryAll(By.css('table tbody tr')).length
|
||||||
|
).toEqual(4) // 2 tasks x 2 lines
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support keyboard events for filtering', () => {
|
||||||
|
const input = fixture.debugElement.query(
|
||||||
|
By.css('pngx-page-header input[type=text]')
|
||||||
|
)
|
||||||
|
input.nativeElement.value = '191092'
|
||||||
|
input.nativeElement.dispatchEvent(
|
||||||
|
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||||
|
)
|
||||||
|
expect(component.filterText).toEqual('191092') // no debounce needed
|
||||||
|
input.nativeElement.dispatchEvent(
|
||||||
|
new KeyboardEvent('keyup', { key: 'Escape' })
|
||||||
|
)
|
||||||
|
expect(component.filterText).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset filter and target on tab switch', () => {
|
||||||
|
component.filterText = '191092'
|
||||||
|
component.filterTargetID = 1
|
||||||
|
component.activeTab = TaskTab.Completed
|
||||||
|
component.beforeTabChange()
|
||||||
|
expect(component.filterText).toEqual('')
|
||||||
|
expect(component.filterTargetID).toEqual(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -1,22 +1,75 @@
|
|||||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
import { NgTemplateOutlet, SlicePipe } from '@angular/common'
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import {
|
||||||
import { first } from 'rxjs'
|
NgbCollapseModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbModal,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbPaginationModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import {
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
first,
|
||||||
|
Subject,
|
||||||
|
takeUntil,
|
||||||
|
timer,
|
||||||
|
} from 'rxjs'
|
||||||
import { PaperlessTask } from 'src/app/data/paperless-task'
|
import { PaperlessTask } from 'src/app/data/paperless-task'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { TasksService } from 'src/app/services/tasks.service'
|
import { TasksService } from 'src/app/services/tasks.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
|
export enum TaskTab {
|
||||||
|
Queued = 'queued',
|
||||||
|
Started = 'started',
|
||||||
|
Completed = 'completed',
|
||||||
|
Failed = 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TaskFilterTargetID {
|
||||||
|
Name,
|
||||||
|
Result,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTER_TARGETS = [
|
||||||
|
{ id: TaskFilterTargetID.Name, name: $localize`Name` },
|
||||||
|
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
|
||||||
|
]
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-tasks',
|
selector: 'pngx-tasks',
|
||||||
templateUrl: './tasks.component.html',
|
templateUrl: './tasks.component.html',
|
||||||
styleUrls: ['./tasks.component.scss'],
|
styleUrls: ['./tasks.component.scss'],
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
CustomDatePipe,
|
||||||
|
SlicePipe,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
NgbCollapseModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbPaginationModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class TasksComponent
|
export class TasksComponent
|
||||||
extends ComponentWithPermissions
|
extends LoadingComponentWithPermissions
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
public activeTab: string
|
public activeTab: TaskTab
|
||||||
public selectedTasks: Set<number> = new Set()
|
public selectedTasks: Set<number> = new Set()
|
||||||
public togggleAll: boolean = false
|
public togggleAll: boolean = false
|
||||||
public expandedTask: number
|
public expandedTask: number
|
||||||
@@ -24,7 +77,27 @@ export class TasksComponent
|
|||||||
public pageSize: number = 25
|
public pageSize: number = 25
|
||||||
public page: number = 1
|
public page: number = 1
|
||||||
|
|
||||||
public autoRefreshInterval: any
|
public autoRefreshEnabled: boolean = true
|
||||||
|
|
||||||
|
private _filterText: string = ''
|
||||||
|
get filterText() {
|
||||||
|
return this._filterText
|
||||||
|
}
|
||||||
|
set filterText(value: string) {
|
||||||
|
this.filterDebounce.next(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public filterTargetID: TaskFilterTargetID = TaskFilterTargetID.Name
|
||||||
|
public get filterTargetName(): string {
|
||||||
|
return this.filterTargets.find((t) => t.id == this.filterTargetID).name
|
||||||
|
}
|
||||||
|
private filterDebounce: Subject<string> = new Subject<string>()
|
||||||
|
|
||||||
|
public get filterTargets(): Array<{ id: number; name: string }> {
|
||||||
|
return [TaskTab.Failed, TaskTab.Completed].includes(this.activeTab)
|
||||||
|
? FILTER_TARGETS
|
||||||
|
: FILTER_TARGETS.slice(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
get dismissButtonText(): string {
|
get dismissButtonText(): string {
|
||||||
return this.selectedTasks.size > 0
|
return this.selectedTasks.size > 0
|
||||||
@@ -42,12 +115,28 @@ export class TasksComponent
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.tasksService.reload()
|
this.tasksService.reload()
|
||||||
this.toggleAutoRefresh()
|
timer(5000, 5000)
|
||||||
|
.pipe(
|
||||||
|
filter(() => this.autoRefreshEnabled),
|
||||||
|
takeUntil(this.unsubscribeNotifier)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.tasksService.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.filterDebounce
|
||||||
|
.pipe(
|
||||||
|
takeUntil(this.unsubscribeNotifier),
|
||||||
|
debounceTime(100),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter((query) => !query.length || query.length > 2)
|
||||||
|
)
|
||||||
|
.subscribe((query) => (this._filterText = query))
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
super.ngOnDestroy()
|
||||||
this.tasksService.cancelPending()
|
this.tasksService.cancelPending()
|
||||||
clearInterval(this.autoRefreshInterval)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissTask(task: PaperlessTask) {
|
dismissTask(task: PaperlessTask) {
|
||||||
@@ -96,19 +185,30 @@ export class TasksComponent
|
|||||||
get currentTasks(): PaperlessTask[] {
|
get currentTasks(): PaperlessTask[] {
|
||||||
let tasks: PaperlessTask[] = []
|
let tasks: PaperlessTask[] = []
|
||||||
switch (this.activeTab) {
|
switch (this.activeTab) {
|
||||||
case 'queued':
|
case TaskTab.Queued:
|
||||||
tasks = this.tasksService.queuedFileTasks
|
tasks = this.tasksService.queuedFileTasks
|
||||||
break
|
break
|
||||||
case 'started':
|
case TaskTab.Started:
|
||||||
tasks = this.tasksService.startedFileTasks
|
tasks = this.tasksService.startedFileTasks
|
||||||
break
|
break
|
||||||
case 'completed':
|
case TaskTab.Completed:
|
||||||
tasks = this.tasksService.completedFileTasks
|
tasks = this.tasksService.completedFileTasks
|
||||||
break
|
break
|
||||||
case 'failed':
|
case TaskTab.Failed:
|
||||||
tasks = this.tasksService.failedFileTasks
|
tasks = this.tasksService.failedFileTasks
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if (this._filterText.length) {
|
||||||
|
tasks = tasks.filter((t) => {
|
||||||
|
if (this.filterTargetID == TaskFilterTargetID.Name) {
|
||||||
|
return t.task_file_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(this._filterText.toLowerCase())
|
||||||
|
} else if (this.filterTargetID == TaskFilterTargetID.Result) {
|
||||||
|
return t.result.toLowerCase().includes(this._filterText.toLowerCase())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
return tasks
|
return tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,31 +225,37 @@ export class TasksComponent
|
|||||||
this.selectedTasks.clear()
|
this.selectedTasks.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
duringTabChange(navID: number) {
|
duringTabChange() {
|
||||||
this.page = 1
|
this.page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeTabChange() {
|
||||||
|
this.resetFilter()
|
||||||
|
this.filterTargetID = TaskFilterTargetID.Name
|
||||||
|
}
|
||||||
|
|
||||||
get activeTabLocalized(): string {
|
get activeTabLocalized(): string {
|
||||||
switch (this.activeTab) {
|
switch (this.activeTab) {
|
||||||
case 'queued':
|
case TaskTab.Queued:
|
||||||
return $localize`queued`
|
return $localize`queued`
|
||||||
case 'started':
|
case TaskTab.Started:
|
||||||
return $localize`started`
|
return $localize`started`
|
||||||
case 'completed':
|
case TaskTab.Completed:
|
||||||
return $localize`completed`
|
return $localize`completed`
|
||||||
case 'failed':
|
case TaskTab.Failed:
|
||||||
return $localize`failed`
|
return $localize`failed`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAutoRefresh(): void {
|
public resetFilter() {
|
||||||
if (this.autoRefreshInterval) {
|
this._filterText = ''
|
||||||
clearInterval(this.autoRefreshInterval)
|
}
|
||||||
this.autoRefreshInterval = null
|
|
||||||
} else {
|
filterInputKeyup(event: KeyboardEvent) {
|
||||||
this.autoRefreshInterval = setInterval(() => {
|
if (event.key == 'Enter') {
|
||||||
this.tasksService.reload()
|
this._filterText = (event.target as HTMLInputElement).value
|
||||||
}, 5000)
|
} else if (event.key === 'Escape') {
|
||||||
|
this.resetFilter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@if (isLoading) {
|
@if (loading) {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
@@ -47,15 +47,20 @@
|
|||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@for (document of documentsInTrash; track document.id) {
|
@for (document of documentsInTrash; track document.id) {
|
||||||
<tr (click)="toggleSelected(document); $event.stopPropagation();">
|
<tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()" class="data-row fade" [class.show]="show">
|
||||||
<td>
|
<td>
|
||||||
<div class="form-check m-0 ms-2 me-n2">
|
<div class="form-check m-0 ms-2 me-n2">
|
||||||
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
|
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
|
||||||
<label class="form-check-label" for="{{document.id}}"></label>
|
<label class="form-check-label" for="{{document.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td scope="row">{{ document.title }}</td>
|
<td scope="row">
|
||||||
<td scope="row" i18n>{{ getDaysRemaining(document) }} days</td>
|
{{ document.title }}
|
||||||
|
<pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
|
||||||
|
<i-bs name="eye"></i-bs>
|
||||||
|
</pngx-preview-popup>
|
||||||
|
</td>
|
||||||
|
<td scope="row" class="d-none d-sm-table-cell" i18n>{{ getDaysRemaining(document) }} days</td>
|
||||||
<td scope="row">
|
<td scope="row">
|
||||||
<div class="btn-group d-block d-sm-none">
|
<div class="btn-group d-block d-sm-none">
|
||||||
<div ngbDropdown container="body" class="d-inline-block">
|
<div ngbDropdown container="body" class="d-inline-block">
|
||||||
@@ -83,7 +88,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!isLoading) {
|
@if (!loading) {
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div>
|
<div>
|
||||||
<ng-container i18n>{totalDocuments, plural, =1 {One document in trash} other {{{totalDocuments || 0}} total documents in trash}}</ng-container>
|
<ng-container i18n>{totalDocuments, plural, =1 {One document in trash} other {{{totalDocuments || 0}} total documents in trash}}</ng-container>
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
// hide caret on mobile dropdown
|
||||||
|
.d-block.d-sm-none .dropdown-toggle::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@@ -1,21 +1,22 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
import { TrashComponent } from './trash.component'
|
|
||||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { TrashService } from 'src/app/services/trash.service'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { By } from '@angular/platform-browser'
|
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { TrashService } from 'src/app/services/trash.service'
|
||||||
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { TrashComponent } from './trash.component'
|
||||||
|
|
||||||
const documentsInTrash = [
|
const documentsInTrash = [
|
||||||
{
|
{
|
||||||
@@ -38,15 +39,10 @@ describe('TrashComponent', () => {
|
|||||||
let trashService: TrashService
|
let trashService: TrashService
|
||||||
let modalService: NgbModal
|
let modalService: NgbModal
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
|
let router: Router
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [
|
|
||||||
TrashComponent,
|
|
||||||
PageHeaderComponent,
|
|
||||||
ConfirmDialogComponent,
|
|
||||||
SafeHtmlPipe,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -54,6 +50,10 @@ describe('TrashComponent', () => {
|
|||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
TrashComponent,
|
||||||
|
PageHeaderComponent,
|
||||||
|
ConfirmDialogComponent,
|
||||||
|
SafeHtmlPipe,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@@ -61,11 +61,13 @@ describe('TrashComponent', () => {
|
|||||||
trashService = TestBed.inject(TrashService)
|
trashService = TestBed.inject(TrashService)
|
||||||
modalService = TestBed.inject(NgbModal)
|
modalService = TestBed.inject(NgbModal)
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
|
router = TestBed.inject(Router)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call correct service method on reload', () => {
|
it('should call correct service method on reload', () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
const trashSpy = jest.spyOn(trashService, 'getTrash')
|
const trashSpy = jest.spyOn(trashService, 'getTrash')
|
||||||
trashSpy.mockReturnValue(
|
trashSpy.mockReturnValue(
|
||||||
of({
|
of({
|
||||||
@@ -75,6 +77,7 @@ describe('TrashComponent', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
component.reload()
|
component.reload()
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(trashSpy).toHaveBeenCalled()
|
expect(trashSpy).toHaveBeenCalled()
|
||||||
expect(component.documentsInTrash).toEqual(documentsInTrash)
|
expect(component.documentsInTrash).toEqual(documentsInTrash)
|
||||||
})
|
})
|
||||||
@@ -161,6 +164,22 @@ describe('TrashComponent', () => {
|
|||||||
expect(restoreSpy).toHaveBeenCalledWith([1, 2])
|
expect(restoreSpy).toHaveBeenCalledWith([1, 2])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should offer link to restored document', () => {
|
||||||
|
let toasts
|
||||||
|
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||||
|
toastService.getToasts().subscribe((allToasts) => {
|
||||||
|
toasts = [...allToasts]
|
||||||
|
})
|
||||||
|
jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK'))
|
||||||
|
component.restore(documentsInTrash[0])
|
||||||
|
expect(toasts.length).toEqual(1)
|
||||||
|
toasts[0].action()
|
||||||
|
expect(navigateSpy).toHaveBeenCalledWith([
|
||||||
|
'documents',
|
||||||
|
documentsInTrash[0].id,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('should support toggle all items in view', () => {
|
it('should support toggle all items in view', () => {
|
||||||
component.documentsInTrash = documentsInTrash
|
component.documentsInTrash = documentsInTrash
|
||||||
expect(component.selectedDocuments.size).toEqual(0)
|
expect(component.selectedDocuments.size).toEqual(0)
|
||||||
|
@@ -1,49 +1,74 @@
|
|||||||
import { Component, OnDestroy } from '@angular/core'
|
import { Component, OnDestroy } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
|
import {
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbModal,
|
||||||
|
NgbPaginationModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { delay, takeUntil, tap } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { TrashService } from 'src/app/services/trash.service'
|
import { TrashService } from 'src/app/services/trash.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-trash',
|
selector: 'pngx-trash',
|
||||||
templateUrl: './trash.component.html',
|
templateUrl: './trash.component.html',
|
||||||
styleUrl: './trash.component.scss',
|
styleUrl: './trash.component.scss',
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
PreviewPopupComponent,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbPaginationModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class TrashComponent implements OnDestroy {
|
export class TrashComponent
|
||||||
|
extends LoadingComponentWithPermissions
|
||||||
|
implements OnDestroy
|
||||||
|
{
|
||||||
public documentsInTrash: Document[] = []
|
public documentsInTrash: Document[] = []
|
||||||
public selectedDocuments: Set<number> = new Set()
|
public selectedDocuments: Set<number> = new Set()
|
||||||
public allToggled: boolean = false
|
public allToggled: boolean = false
|
||||||
public page: number = 1
|
public page: number = 1
|
||||||
public totalDocuments: number
|
public totalDocuments: number
|
||||||
public isLoading: boolean = false
|
|
||||||
unsubscribeNotifier: Subject<void> = new Subject()
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private trashService: TrashService,
|
private trashService: TrashService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private settingsService: SettingsService
|
private settingsService: SettingsService,
|
||||||
|
private router: Router
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.reload()
|
this.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.unsubscribeNotifier.next()
|
|
||||||
this.unsubscribeNotifier.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
this.isLoading = true
|
this.loading = true
|
||||||
this.trashService.getTrash(this.page).subscribe((r) => {
|
this.trashService
|
||||||
this.documentsInTrash = r.results
|
.getTrash(this.page)
|
||||||
this.totalDocuments = r.count
|
.pipe(
|
||||||
this.isLoading = false
|
tap((r) => {
|
||||||
this.selectedDocuments.clear()
|
this.documentsInTrash = r.results
|
||||||
})
|
this.totalDocuments = r.count
|
||||||
|
this.selectedDocuments.clear()
|
||||||
|
this.loading = false
|
||||||
|
}),
|
||||||
|
delay(100)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.show = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(document: Document) {
|
delete(document: Document) {
|
||||||
@@ -110,7 +135,14 @@ export class TrashComponent implements OnDestroy {
|
|||||||
restore(document: Document) {
|
restore(document: Document) {
|
||||||
this.trashService.restoreDocuments([document.id]).subscribe({
|
this.trashService.restoreDocuments([document.id]).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.showInfo($localize`Document restored`)
|
this.toastService.show({
|
||||||
|
content: $localize`Document restored`,
|
||||||
|
delay: 5000,
|
||||||
|
actionName: $localize`Open document`,
|
||||||
|
action: () => {
|
||||||
|
this.router.navigate(['documents', document.id])
|
||||||
|
},
|
||||||
|
})
|
||||||
this.reload()
|
this.reload()
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
@@ -6,22 +7,13 @@ import {
|
|||||||
fakeAsync,
|
fakeAsync,
|
||||||
tick,
|
tick,
|
||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import {
|
import { of, throwError } from 'rxjs'
|
||||||
NgbModule,
|
import { Group } from 'src/app/data/group'
|
||||||
NgbAlertModule,
|
import { User } from 'src/app/data/user'
|
||||||
NgbModal,
|
|
||||||
NgbModalRef,
|
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
|
||||||
import { throwError, of } from 'rxjs'
|
|
||||||
import { routes } from 'src/app/app-routing.module'
|
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
@@ -30,21 +22,7 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
|
||||||
import { NumberComponent } from '../../common/input/number/number.component'
|
|
||||||
import { PasswordComponent } from '../../common/input/password/password.component'
|
|
||||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
|
||||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
|
||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
|
||||||
import { TagsComponent } from '../../common/input/tags/tags.component'
|
|
||||||
import { TextComponent } from '../../common/input/text/text.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { SettingsComponent } from '../settings/settings.component'
|
|
||||||
import { UsersAndGroupsComponent } from './users-groups.component'
|
import { UsersAndGroupsComponent } from './users-groups.component'
|
||||||
import { User } from 'src/app/data/user'
|
|
||||||
import { Group } from 'src/app/data/group'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
const users = [
|
const users = [
|
||||||
{ id: 1, username: 'user1', is_superuser: false },
|
{ id: 1, username: 'user1', is_superuser: false },
|
||||||
@@ -67,33 +45,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||||
UsersAndGroupsComponent,
|
|
||||||
SettingsComponent,
|
|
||||||
PageHeaderComponent,
|
|
||||||
IfPermissionsDirective,
|
|
||||||
CustomDatePipe,
|
|
||||||
ConfirmDialogComponent,
|
|
||||||
CheckComponent,
|
|
||||||
SafeHtmlPipe,
|
|
||||||
SelectComponent,
|
|
||||||
TextComponent,
|
|
||||||
PasswordComponent,
|
|
||||||
NumberComponent,
|
|
||||||
TagsComponent,
|
|
||||||
PermissionsUserComponent,
|
|
||||||
PermissionsGroupComponent,
|
|
||||||
IfOwnerDirective,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
NgbModule,
|
|
||||||
RouterTestingModule.withRoutes(routes),
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
NgbAlertModule,
|
|
||||||
NgSelectModule,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
|
@@ -1,23 +1,31 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { Subject, first, takeUntil } from 'rxjs'
|
import { Subject, first, takeUntil } from 'rxjs'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-users-groups',
|
selector: 'pngx-users-groups',
|
||||||
templateUrl: './users-groups.component.html',
|
templateUrl: './users-groups.component.html',
|
||||||
styleUrls: ['./users-groups.component.scss'],
|
styleUrls: ['./users-groups.component.scss'],
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class UsersAndGroupsComponent
|
export class UsersAndGroupsComponent
|
||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow-sm">
|
<nav class="navbar navbar-dark fixed-top bg-primary flex-md-nowrap p-0 shadow-sm">
|
||||||
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
@@ -199,6 +199,13 @@
|
|||||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
|
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
||||||
|
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<i-bs class="me-1" name="window-stack"></i-bs><span> <ng-container i18n>Saved Views</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item app-link"
|
<li class="nav-item app-link"
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
|
||||||
tourAnchor="tour.workflows">
|
tourAnchor="tour.workflows">
|
||||||
@@ -327,7 +334,7 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
<a *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
|
||||||
container="body">
|
container="body">
|
||||||
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
@import "node_modules/bootstrap/scss/functions";
|
|
||||||
@import "node_modules/bootstrap/scss/variables";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sidebar
|
* Sidebar
|
||||||
*/
|
*/
|
||||||
@@ -15,6 +12,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
--pngx-sidebar-width: 100%;
|
--pngx-sidebar-width: 100%;
|
||||||
max-width: var(--pngx-sidebar-width);
|
max-width: var(--pngx-sidebar-width);
|
||||||
|
transition: all .2s ease;
|
||||||
|
|
||||||
.sidebar-heading .spinner-border {
|
.sidebar-heading .spinner-border {
|
||||||
width: 0.8em;
|
width: 0.8em;
|
||||||
@@ -37,8 +35,6 @@
|
|||||||
@media (min-width: 2400px) {
|
@media (min-width: 2400px) {
|
||||||
--pngx-sidebar-width: 8.33333333%;
|
--pngx-sidebar-width: 8.33333333%;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: all .2s ease;
|
|
||||||
}
|
}
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@@ -48,6 +44,13 @@
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
transition: all .2s ease;
|
transition: all .2s ease;
|
||||||
|
padding-top: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
main {
|
||||||
|
padding-top: 56px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-slim-toggler {
|
.sidebar-slim-toggler {
|
||||||
|
@@ -1,43 +1,43 @@
|
|||||||
|
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import {
|
import {
|
||||||
HttpTestingController,
|
HttpTestingController,
|
||||||
provideHttpClientTesting,
|
provideHttpClientTesting,
|
||||||
} from '@angular/common/http/testing'
|
} from '@angular/common/http/testing'
|
||||||
import { AppFrameComponent } from './app-frame.component'
|
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
fakeAsync,
|
fakeAsync,
|
||||||
tick,
|
tick,
|
||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { BrowserModule } from '@angular/platform-browser'
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
|
||||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
|
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import {
|
import {
|
||||||
DjangoMessageLevel,
|
DjangoMessageLevel,
|
||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
import { environment } from 'src/environments/environment'
|
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||||
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
import { environment } from 'src/environments/environment'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
|
||||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||||
|
import { AppFrameComponent } from './app-frame.component'
|
||||||
import { GlobalSearchComponent } from './global-search/global-search.component'
|
import { GlobalSearchComponent } from './global-search/global-search.component'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
const saved_views = [
|
const saved_views = [
|
||||||
{
|
{
|
||||||
@@ -95,11 +95,6 @@ describe('AppFrameComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
|
||||||
AppFrameComponent,
|
|
||||||
IfPermissionsDirective,
|
|
||||||
GlobalSearchComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
RouterTestingModule.withRoutes(routes),
|
RouterTestingModule.withRoutes(routes),
|
||||||
@@ -109,6 +104,9 @@ describe('AppFrameComponent', () => {
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
AppFrameComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
GlobalSearchComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
@@ -343,6 +341,7 @@ describe('AppFrameComponent', () => {
|
|||||||
component.editProfile()
|
component.editProfile()
|
||||||
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
|
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
|
size: 'xl',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -1,45 +1,72 @@
|
|||||||
|
import {
|
||||||
|
CdkDragDrop,
|
||||||
|
CdkDragEnd,
|
||||||
|
CdkDragStart,
|
||||||
|
DragDropModule,
|
||||||
|
moveItemInArray,
|
||||||
|
} from '@angular/cdk/drag-drop'
|
||||||
|
import { NgClass } from '@angular/common'
|
||||||
import { Component, HostListener, OnInit } from '@angular/core'
|
import { Component, HostListener, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||||
|
import {
|
||||||
|
NgbCollapseModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbModal,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
|
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||||
import {
|
import {
|
||||||
DjangoMessageLevel,
|
DjangoMessageLevel,
|
||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { environment } from 'src/environments/environment'
|
|
||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
|
||||||
import {
|
|
||||||
RemoteVersionService,
|
|
||||||
AppRemoteVersion,
|
|
||||||
} from 'src/app/services/rest/remote-version.service'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
|
||||||
import { TasksService } from 'src/app/services/tasks.service'
|
|
||||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
|
||||||
import {
|
import {
|
||||||
CdkDragStart,
|
AppRemoteVersion,
|
||||||
CdkDragEnd,
|
RemoteVersionService,
|
||||||
CdkDragDrop,
|
} from 'src/app/services/rest/remote-version.service'
|
||||||
moveItemInArray,
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
} from '@angular/cdk/drag-drop'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { TasksService } from 'src/app/services/tasks.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
import { GlobalSearchComponent } from './global-search/global-search.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-app-frame',
|
selector: 'pngx-app-frame',
|
||||||
templateUrl: './app-frame.component.html',
|
templateUrl: './app-frame.component.html',
|
||||||
styleUrls: ['./app-frame.component.scss'],
|
styleUrls: ['./app-frame.component.scss'],
|
||||||
|
imports: [
|
||||||
|
GlobalSearchComponent,
|
||||||
|
DocumentTitlePipe,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
RouterModule,
|
||||||
|
NgClass,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgbCollapseModule,
|
||||||
|
NgbNavModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
DragDropModule,
|
||||||
|
TourNgBootstrapModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppFrameComponent
|
export class AppFrameComponent
|
||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
@@ -81,7 +108,14 @@ export class AppFrameComponent
|
|||||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||||
this.checkForUpdates()
|
this.checkForUpdates()
|
||||||
}
|
}
|
||||||
this.tasksService.reload()
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.PaperlessTask
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.tasksService.reload()
|
||||||
|
}
|
||||||
|
|
||||||
this.djangoMessagesService.get().forEach((message) => {
|
this.djangoMessagesService.get().forEach((message) => {
|
||||||
switch (message.level) {
|
switch (message.level) {
|
||||||
@@ -136,6 +170,7 @@ export class AppFrameComponent
|
|||||||
editProfile() {
|
editProfile() {
|
||||||
this.modalService.open(ProfileEditDialogComponent, {
|
this.modalService.open(ProfileEditDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
|
size: 'xl',
|
||||||
})
|
})
|
||||||
this.closeMenu()
|
this.closeMenu()
|
||||||
}
|
}
|
||||||
|
@@ -49,7 +49,7 @@
|
|||||||
[disabled]="disablePrimaryButton(type, item)"
|
[disabled]="disablePrimaryButton(type, item)"
|
||||||
(mouseenter)="onButtonHover($event)">
|
(mouseenter)="onButtonHover($event)">
|
||||||
@if (type === DataType.Document) {
|
@if (type === DataType.Document) {
|
||||||
<i-bs width="1em" height="1em" name="box-arrow-in-right"></i-bs>
|
<i-bs width="1em" height="1em" name="file-earmark-richtext"></i-bs>
|
||||||
<span> <ng-container i18n>Open</ng-container></span>
|
<span> <ng-container i18n>Open</ng-container></span>
|
||||||
} @else if (type === DataType.SavedView) {
|
} @else if (type === DataType.SavedView) {
|
||||||
<i-bs width="1em" height="1em" name="eye"></i-bs>
|
<i-bs width="1em" height="1em" name="eye"></i-bs>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<i-bs width="1em" height="1em" name="download"></i-bs>
|
<i-bs width="1em" height="1em" name="download"></i-bs>
|
||||||
<span> <ng-container i18n>Download</ng-container></span>
|
<span> <ng-container i18n>Download</ng-container></span>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs width="1em" height="1em" name="box-arrow-in-right"></i-bs>
|
<i-bs width="1em" height="1em" name="file-earmark-richtext"></i-bs>
|
||||||
<span> <ng-container i18n>Open</ng-container></span>
|
<span> <ng-container i18n>Open</ng-container></span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { ElementRef } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
fakeAsync,
|
fakeAsync,
|
||||||
tick,
|
tick,
|
||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { GlobalSearchComponent } from './global-search.component'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { of } from 'rxjs'
|
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
@@ -14,11 +15,9 @@ import {
|
|||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
import { of } from 'rxjs'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DataType } from 'src/app/data/datatype'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import {
|
import {
|
||||||
FILTER_FULLTEXT_QUERY,
|
FILTER_FULLTEXT_QUERY,
|
||||||
FILTER_HAS_CORRESPONDENT_ANY,
|
FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
@@ -27,20 +26,21 @@ import {
|
|||||||
FILTER_HAS_TAGS_ALL,
|
FILTER_HAS_TAGS_ALL,
|
||||||
FILTER_TITLE_CONTENT,
|
FILTER_TITLE_CONTENT,
|
||||||
} from 'src/app/data/filter-rule-type'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
|
||||||
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
|
||||||
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
|
||||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
|
||||||
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
|
||||||
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
|
||||||
import { ElementRef } from '@angular/core'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { DataType } from 'src/app/data/datatype'
|
|
||||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
|
||||||
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
||||||
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
|
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
|
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||||
|
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||||
|
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
|
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||||
|
import { GlobalSearchComponent } from './global-search.component'
|
||||||
|
|
||||||
const searchResults = {
|
const searchResults = {
|
||||||
total: 11,
|
total: 11,
|
||||||
@@ -138,13 +138,13 @@ describe('GlobalSearchComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [GlobalSearchComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
GlobalSearchComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
@@ -1,14 +1,23 @@
|
|||||||
|
import { NgTemplateOutlet } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ViewChild,
|
|
||||||
ElementRef,
|
ElementRef,
|
||||||
ViewChildren,
|
|
||||||
QueryList,
|
|
||||||
OnInit,
|
OnInit,
|
||||||
|
QueryList,
|
||||||
|
ViewChild,
|
||||||
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { NgbDropdown, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
import {
|
||||||
import { Subject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
|
NgbDropdown,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgbModal,
|
||||||
|
NgbModalRef,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { Subject, debounceTime, distinctUntilChanged, filter } from 'rxjs'
|
||||||
|
import { DataType } from 'src/app/data/datatype'
|
||||||
import {
|
import {
|
||||||
FILTER_FULLTEXT_QUERY,
|
FILTER_FULLTEXT_QUERY,
|
||||||
FILTER_HAS_CORRESPONDENT_ANY,
|
FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
@@ -17,19 +26,23 @@ import {
|
|||||||
FILTER_HAS_TAGS_ALL,
|
FILTER_HAS_TAGS_ALL,
|
||||||
FILTER_TITLE_CONTENT,
|
FILTER_TITLE_CONTENT,
|
||||||
} from 'src/app/data/filter-rule-type'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { DataType } from 'src/app/data/datatype'
|
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
|
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||||
import {
|
import {
|
||||||
PermissionsService,
|
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import {
|
import {
|
||||||
GlobalSearchResult,
|
GlobalSearchResult,
|
||||||
SearchService,
|
SearchService,
|
||||||
} from 'src/app/services/rest/search.service'
|
} from 'src/app/services/rest/search.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { paramsFromViewState } from 'src/app/utils/query-params'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
@@ -41,15 +54,19 @@ import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage
|
|||||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
|
||||||
import { paramsFromViewState } from 'src/app/utils/query-params'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
|
||||||
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-global-search',
|
selector: 'pngx-global-search',
|
||||||
templateUrl: './global-search.component.html',
|
templateUrl: './global-search.component.html',
|
||||||
styleUrl: './global-search.component.scss',
|
styleUrl: './global-search.component.scss',
|
||||||
|
imports: [
|
||||||
|
CustomDatePipe,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class GlobalSearchComponent implements OnInit {
|
export class GlobalSearchComponent implements OnInit {
|
||||||
public DataType = DataType
|
public DataType = DataType
|
||||||
@@ -89,7 +106,6 @@ export class GlobalSearchComponent implements OnInit {
|
|||||||
this.queryDebounce
|
this.queryDebounce
|
||||||
.pipe(
|
.pipe(
|
||||||
debounceTime(400),
|
debounceTime(400),
|
||||||
map((query) => query?.trim()),
|
|
||||||
filter((query) => !query?.length || query?.length > 2),
|
filter((query) => !query?.length || query?.length > 2),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
@@ -109,7 +125,7 @@ export class GlobalSearchComponent implements OnInit {
|
|||||||
|
|
||||||
private search(query: string) {
|
private search(query: string) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.searchService.globalSearch(query).subscribe((results) => {
|
this.searchService.globalSearch(query.trim()).subscribe((results) => {
|
||||||
this.searchResults = results
|
this.searchResults = results
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.resultsDropdown.open()
|
this.resultsDropdown.open()
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||||
|
|
||||||
describe('ClearableBadgeComponent', () => {
|
describe('ClearableBadgeComponent', () => {
|
||||||
let component: ClearableBadgeComponent
|
let component: ClearableBadgeComponent
|
||||||
@@ -8,8 +8,10 @@ describe('ClearableBadgeComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ClearableBadgeComponent],
|
imports: [
|
||||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
ClearableBadgeComponent,
|
||||||
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-clearable-badge',
|
selector: 'pngx-clearable-badge',
|
||||||
templateUrl: './clearable-badge.component.html',
|
templateUrl: './clearable-badge.component.html',
|
||||||
styleUrls: ['./clearable-badge.component.scss'],
|
styleUrls: ['./clearable-badge.component.scss'],
|
||||||
|
imports: [NgxBootstrapIconsModule],
|
||||||
})
|
})
|
||||||
export class ClearableBadgeComponent {
|
export class ClearableBadgeComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
import { ConfirmButtonComponent } from './confirm-button.component'
|
|
||||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { ConfirmButtonComponent } from './confirm-button.component'
|
||||||
|
|
||||||
describe('ConfirmButtonComponent', () => {
|
describe('ConfirmButtonComponent', () => {
|
||||||
let component: ConfirmButtonComponent
|
let component: ConfirmButtonComponent
|
||||||
@@ -10,8 +10,11 @@ describe('ConfirmButtonComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ConfirmButtonComponent],
|
imports: [
|
||||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule.pick(allIcons)],
|
NgbPopoverModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
ConfirmButtonComponent,
|
||||||
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ConfirmButtonComponent)
|
fixture = TestBed.createComponent(ConfirmButtonComponent)
|
||||||
|
@@ -5,12 +5,14 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-confirm-button',
|
selector: 'pngx-confirm-button',
|
||||||
templateUrl: './confirm-button.component.html',
|
templateUrl: './confirm-button.component.html',
|
||||||
styleUrl: './confirm-button.component.scss',
|
styleUrl: './confirm-button.component.scss',
|
||||||
|
imports: [NgbPopoverModule, NgxBootstrapIconsModule],
|
||||||
})
|
})
|
||||||
export class ConfirmButtonComponent {
|
export class ConfirmButtonComponent {
|
||||||
@Input()
|
@Input()
|
||||||
|
@@ -1,14 +1,8 @@
|
|||||||
import {
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
ComponentFixture,
|
|
||||||
TestBed,
|
|
||||||
discardPeriodicTasks,
|
|
||||||
fakeAsync,
|
|
||||||
tick,
|
|
||||||
} from '@angular/core/testing'
|
|
||||||
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
|
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
||||||
|
|
||||||
describe('ConfirmDialogComponent', () => {
|
describe('ConfirmDialogComponent', () => {
|
||||||
let component: ConfirmDialogComponent
|
let component: ConfirmDialogComponent
|
||||||
@@ -17,9 +11,8 @@ describe('ConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ConfirmDialogComponent, SafeHtmlPipe],
|
|
||||||
providers: [NgbActiveModal, SafeHtmlPipe],
|
providers: [NgbActiveModal, SafeHtmlPipe],
|
||||||
imports: [],
|
imports: [ConfirmDialogComponent, SafeHtmlPipe],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
modal = TestBed.inject(NgbActiveModal)
|
modal = TestBed.inject(NgbActiveModal)
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
|
import { DecimalPipe } from '@angular/common'
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { interval, Subject, take } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-confirm-dialog',
|
selector: 'pngx-confirm-dialog',
|
||||||
templateUrl: './confirm-dialog.component.html',
|
templateUrl: './confirm-dialog.component.html',
|
||||||
styleUrls: ['./confirm-dialog.component.scss'],
|
styleUrls: ['./confirm-dialog.component.scss'],
|
||||||
|
imports: [DecimalPipe, SafeHtmlPipe],
|
||||||
})
|
})
|
||||||
export class ConfirmDialogComponent {
|
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
||||||
constructor(public activeModal: NgbActiveModal) {}
|
constructor(public activeModal: NgbActiveModal) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
public confirmClicked = new EventEmitter()
|
public confirmClicked = new EventEmitter()
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
.pdf-viewer-container {
|
.pdf-viewer-container {
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
height: 350px;
|
height: 550px;
|
||||||
|
|
||||||
pdf-viewer {
|
pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
|
||||||
import { PdfViewerComponent } from 'ng2-pdf-viewer'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
describe('DeletePagesConfirmDialogComponent', () => {
|
describe('DeletePagesConfirmDialogComponent', () => {
|
||||||
let component: DeletePagesConfirmDialogComponent
|
let component: DeletePagesConfirmDialogComponent
|
||||||
@@ -14,11 +13,12 @@ describe('DeletePagesConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent],
|
declarations: [],
|
||||||
imports: [
|
imports: [
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
DeletePagesConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
|
@@ -1,13 +1,20 @@
|
|||||||
import { Component, TemplateRef, ViewChild } from '@angular/core'
|
import { Component, TemplateRef, ViewChild } from '@angular/core'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import {
|
||||||
|
PDFDocumentProxy,
|
||||||
|
PdfViewerComponent,
|
||||||
|
PdfViewerModule,
|
||||||
|
} from 'ng2-pdf-viewer'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
import { PDFDocumentProxy, PdfViewerComponent } from 'ng2-pdf-viewer'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-delete-pages-confirm-dialog',
|
selector: 'pngx-delete-pages-confirm-dialog',
|
||||||
templateUrl: './delete-pages-confirm-dialog.component.html',
|
templateUrl: './delete-pages-confirm-dialog.component.html',
|
||||||
styleUrl: './delete-pages-confirm-dialog.component.scss',
|
styleUrl: './delete-pages-confirm-dialog.component.scss',
|
||||||
|
imports: [PdfViewerModule, FormsModule, ReactiveFormsModule, SafeHtmlPipe],
|
||||||
})
|
})
|
||||||
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
|
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
public documentID: number
|
public documentID: number
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
describe('MergeConfirmDialogComponent', () => {
|
describe('MergeConfirmDialogComponent', () => {
|
||||||
let component: MergeConfirmDialogComponent
|
let component: MergeConfirmDialogComponent
|
||||||
@@ -15,11 +15,11 @@ describe('MergeConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [MergeConfirmDialogComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
MergeConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
|
@@ -1,16 +1,28 @@
|
|||||||
|
import {
|
||||||
|
CdkDragDrop,
|
||||||
|
DragDropModule,
|
||||||
|
moveItemInArray,
|
||||||
|
} from '@angular/cdk/drag-drop'
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { takeUntil } from 'rxjs'
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-merge-confirm-dialog',
|
selector: 'pngx-merge-confirm-dialog',
|
||||||
templateUrl: './merge-confirm-dialog.component.html',
|
templateUrl: './merge-confirm-dialog.component.html',
|
||||||
styleUrl: './merge-confirm-dialog.component.scss',
|
styleUrl: './merge-confirm-dialog.component.scss',
|
||||||
|
imports: [
|
||||||
|
DragDropModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class MergeConfirmDialogComponent
|
export class MergeConfirmDialogComponent
|
||||||
extends ConfirmDialogComponent
|
extends ConfirmDialogComponent
|
||||||
@@ -25,8 +37,6 @@ export class MergeConfirmDialogComponent
|
|||||||
|
|
||||||
public metadataDocumentID: number = -1
|
public metadataDocumentID: number = -1
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
activeModal: NgbActiveModal,
|
activeModal: NgbActiveModal,
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|
||||||
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
|
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
||||||
|
|
||||||
describe('RotateConfirmDialogComponent', () => {
|
describe('RotateConfirmDialogComponent', () => {
|
||||||
let component: RotateConfirmDialogComponent
|
let component: RotateConfirmDialogComponent
|
||||||
@@ -12,8 +12,11 @@ describe('RotateConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [RotateConfirmDialogComponent, SafeHtmlPipe],
|
imports: [
|
||||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
RotateConfirmDialogComponent,
|
||||||
|
SafeHtmlPipe,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
SafeHtmlPipe,
|
SafeHtmlPipe,
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
|
import { NgStyle } from '@angular/common'
|
||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-rotate-confirm-dialog',
|
selector: 'pngx-rotate-confirm-dialog',
|
||||||
templateUrl: './rotate-confirm-dialog.component.html',
|
templateUrl: './rotate-confirm-dialog.component.html',
|
||||||
styleUrl: './rotate-confirm-dialog.component.scss',
|
styleUrl: './rotate-confirm-dialog.component.scss',
|
||||||
|
imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe],
|
||||||
})
|
})
|
||||||
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
public documentID: number
|
public documentID: number
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>{{message}}</p>
|
<p>{{message}}</p>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-8">
|
<div class="col-7">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<div class="input-group-text" i18n>Page</div>
|
<div class="input-group-text" i18n>Page</div>
|
||||||
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
|
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</pdf-viewer>
|
</pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-5">
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
|
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
|
||||||
<i-bs name="plus-circle"></i-bs>
|
<i-bs name="plus-circle"></i-bs>
|
||||||
@@ -44,12 +44,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch mt-4">
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="form-check form-switch me-auto">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
|
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
|
||||||
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
|
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
.pdf-viewer-container {
|
.pdf-viewer-container {
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
height: 350px;
|
height: 500px;
|
||||||
|
|
||||||
pdf-viewer {
|
pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
|
||||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { of } from 'rxjs'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { of } from 'rxjs'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
|
||||||
|
|
||||||
describe('SplitConfirmDialogComponent', () => {
|
describe('SplitConfirmDialogComponent', () => {
|
||||||
let component: SplitConfirmDialogComponent
|
let component: SplitConfirmDialogComponent
|
||||||
@@ -17,12 +17,12 @@ describe('SplitConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [SplitConfirmDialogComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
PdfViewerModule,
|
PdfViewerModule,
|
||||||
|
SplitConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
|
@@ -1,15 +1,23 @@
|
|||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { Document } from 'src/app/data/document'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { Document } from 'src/app/data/document'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-split-confirm-dialog',
|
selector: 'pngx-split-confirm-dialog',
|
||||||
templateUrl: './split-confirm-dialog.component.html',
|
templateUrl: './split-confirm-dialog.component.html',
|
||||||
styleUrl: './split-confirm-dialog.component.scss',
|
styleUrl: './split-confirm-dialog.component.scss',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
PdfViewerModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class SplitConfirmDialogComponent
|
export class SplitConfirmDialogComponent
|
||||||
extends ConfirmDialogComponent
|
extends ConfirmDialogComponent
|
||||||
|
@@ -18,9 +18,11 @@
|
|||||||
@case (CustomFieldDataType.DocumentLink) {
|
@case (CustomFieldDataType.DocumentLink) {
|
||||||
<div [ngbTooltip]="nameTooltip" class="d-flex gap-1 flex-wrap">
|
<div [ngbTooltip]="nameTooltip" class="d-flex gap-1 flex-wrap">
|
||||||
@for (docId of value; track docId) {
|
@for (docId of value; track docId) {
|
||||||
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
|
@if (getDocumentTitle(docId)) {
|
||||||
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{ getDocumentTitle(docId) }}</span>
|
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
|
||||||
</a>
|
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{ getDocumentTitle(docId) }}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { DisplayField, Document } from 'src/app/data/document'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { CustomFieldDisplayComponent } from './custom-field-display.component'
|
import { CustomFieldDisplayComponent } from './custom-field-display.component'
|
||||||
import { DisplayField, Document } from 'src/app/data/document'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
|
|
||||||
const customFields: CustomField[] = [
|
const customFields: CustomField[] = [
|
||||||
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
|
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
|
||||||
@@ -17,7 +17,11 @@ const customFields: CustomField[] = [
|
|||||||
name: 'Field 4',
|
name: 'Field 4',
|
||||||
data_type: CustomFieldDataType.Select,
|
data_type: CustomFieldDataType.Select,
|
||||||
extra_data: {
|
extra_data: {
|
||||||
select_options: ['Option 1', 'Option 2', 'Option 3'],
|
select_options: [
|
||||||
|
{ label: 'Option 1', id: 'abc-123' },
|
||||||
|
{ label: 'Option 2', id: 'def-456' },
|
||||||
|
{ label: 'Option 3', id: 'ghi-789' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,8 +49,7 @@ describe('CustomFieldDisplayComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [CustomFieldDisplayComponent],
|
imports: [CustomFieldDisplayComponent],
|
||||||
imports: [],
|
|
||||||
providers: [
|
providers: [
|
||||||
DocumentService,
|
DocumentService,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
@@ -131,6 +134,8 @@ describe('CustomFieldDisplayComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show select value', () => {
|
it('should show select value', () => {
|
||||||
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
|
expect(component.getSelectValue(customFields[3], 'ghi-789')).toEqual(
|
||||||
|
'Option 3'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -1,25 +1,25 @@
|
|||||||
import { getLocaleCurrencyCode } from '@angular/common'
|
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
|
||||||
import {
|
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
|
||||||
Component,
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
Inject,
|
import { takeUntil } from 'rxjs'
|
||||||
Input,
|
|
||||||
LOCALE_ID,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
import { DisplayField, Document } from 'src/app/data/document'
|
import { DisplayField, Document } from 'src/app/data/document'
|
||||||
import { Results } from 'src/app/data/results'
|
import { Results } from 'src/app/data/results'
|
||||||
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-custom-field-display',
|
selector: 'pngx-custom-field-display',
|
||||||
templateUrl: './custom-field-display.component.html',
|
templateUrl: './custom-field-display.component.html',
|
||||||
styleUrl: './custom-field-display.component.scss',
|
styleUrl: './custom-field-display.component.scss',
|
||||||
|
imports: [CustomDatePipe, CurrencyPipe, NgbTooltipModule],
|
||||||
})
|
})
|
||||||
export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
export class CustomFieldDisplayComponent
|
||||||
|
extends LoadingComponentWithPermissions
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
CustomFieldDataType = CustomFieldDataType
|
CustomFieldDataType = CustomFieldDataType
|
||||||
|
|
||||||
private _document: Document
|
private _document: Document
|
||||||
@@ -61,7 +61,6 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private docLinkDocuments: Document[] = []
|
private docLinkDocuments: Document[] = []
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
|
||||||
private defaultCurrencyCode: any
|
private defaultCurrencyCode: any
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -69,6 +68,7 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
|||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
@Inject(LOCALE_ID) currentLocale: string
|
@Inject(LOCALE_ID) currentLocale: string
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
|
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
|
||||||
this.customFieldService.listAll().subscribe((r) => {
|
this.customFieldService.listAll().subscribe((r) => {
|
||||||
this.customFields = r.results
|
this.customFields = r.results
|
||||||
@@ -107,9 +107,9 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
|||||||
.getFew(this.value, { fields: 'id,title' })
|
.getFew(this.value, { fields: 'id,title' })
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result: Results<Document>) => {
|
.subscribe((result: Results<Document>) => {
|
||||||
this.docLinkDocuments = this.value.map((id) =>
|
this.docLinkDocuments = this.value
|
||||||
result.results.find((d) => d.id === id)
|
.map((id) => result.results.find((d) => d.id === id))
|
||||||
)
|
.filter((d) => d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,12 +117,7 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
|||||||
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSelectValue(field: CustomField, index: number): string {
|
public getSelectValue(field: CustomField, id: string): string {
|
||||||
return field.extra_data.select_options[index]
|
return field.extra_data.select_options?.find((o) => o.id === id)?.label
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.unsubscribeNotifier.next(true)
|
|
||||||
this.unsubscribeNotifier.complete()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +1,29 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
fakeAsync,
|
fakeAsync,
|
||||||
tick,
|
tick,
|
||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
|
||||||
import { of } from 'rxjs'
|
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
|
||||||
import { SelectComponent } from '../input/select/select.component'
|
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
import {
|
import {
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { By } from '@angular/platform-browser'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { of } from 'rxjs'
|
||||||
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
import { SelectComponent } from '../input/select/select.component'
|
||||||
|
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
|
||||||
|
|
||||||
const fields: CustomField[] = [
|
const fields: CustomField[] = [
|
||||||
{
|
{
|
||||||
@@ -43,10 +44,10 @@ describe('CustomFieldsDropdownComponent', () => {
|
|||||||
let customFieldService: CustomFieldsService
|
let customFieldService: CustomFieldsService
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
let modalService: NgbModal
|
let modalService: NgbModal
|
||||||
|
let settingsService: SettingsService
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [CustomFieldsDropdownComponent, SelectComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -54,6 +55,8 @@ describe('CustomFieldsDropdownComponent', () => {
|
|||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
CustomFieldsDropdownComponent,
|
||||||
|
SelectComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
@@ -70,6 +73,8 @@ describe('CustomFieldsDropdownComponent', () => {
|
|||||||
results: fields.concat([]),
|
results: fields.concat([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
settingsService = TestBed.inject(SettingsService)
|
||||||
|
settingsService.currentUser = { id: 1, username: 'test' }
|
||||||
fixture = TestBed.createComponent(CustomFieldsDropdownComponent)
|
fixture = TestBed.createComponent(CustomFieldsDropdownComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
|
@@ -3,31 +3,39 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
|
||||||
Output,
|
Output,
|
||||||
QueryList,
|
QueryList,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { Subject, first, takeUntil } from 'rxjs'
|
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { first, takeUntil } from 'rxjs'
|
||||||
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
||||||
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
|
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} 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 { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-custom-fields-dropdown',
|
selector: 'pngx-custom-fields-dropdown',
|
||||||
templateUrl: './custom-fields-dropdown.component.html',
|
templateUrl: './custom-fields-dropdown.component.html',
|
||||||
styleUrls: ['./custom-fields-dropdown.component.scss'],
|
styleUrls: ['./custom-fields-dropdown.component.scss'],
|
||||||
|
imports: [
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class CustomFieldsDropdownComponent implements OnDestroy {
|
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
|
||||||
@Input()
|
@Input()
|
||||||
documentId: number
|
documentId: number
|
||||||
|
|
||||||
@@ -60,8 +68,6 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
|
|||||||
|
|
||||||
public filterText: string
|
public filterText: string
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
|
||||||
|
|
||||||
get canCreateFields(): boolean {
|
get canCreateFields(): boolean {
|
||||||
return this.permissionsService.currentUserCan(
|
return this.permissionsService.currentUserCan(
|
||||||
PermissionAction.Add,
|
PermissionAction.Add,
|
||||||
@@ -75,14 +81,10 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
|
|||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private permissionsService: PermissionsService
|
private permissionsService: PermissionsService
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.getFields()
|
this.getFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.unsubscribeNotifier.next(this)
|
|
||||||
this.unsubscribeNotifier.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFields() {
|
private getFields() {
|
||||||
this.customFieldsService
|
this.customFieldsService
|
||||||
.listAll()
|
.listAll()
|
||||||
|
@@ -44,6 +44,8 @@
|
|||||||
<ng-select #fieldSelects
|
<ng-select #fieldSelects
|
||||||
class="paperless-input-select rounded-end"
|
class="paperless-input-select rounded-end"
|
||||||
[items]="getSelectOptionsForField(atom.field)"
|
[items]="getSelectOptionsForField(atom.field)"
|
||||||
|
bindLabel="label"
|
||||||
|
bindValue="id"
|
||||||
[(ngModel)]="atom.value"
|
[(ngModel)]="atom.value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
(mousedown)="$event.stopImmediatePropagation()"
|
(mousedown)="$event.stopImmediatePropagation()"
|
||||||
@@ -65,7 +67,9 @@
|
|||||||
(mousedown)="$event.stopImmediatePropagation()"
|
(mousedown)="$event.stopImmediatePropagation()"
|
||||||
></ng-select>
|
></ng-select>
|
||||||
<select class="w-25 form-select" [(ngModel)]="atom.operator" [disabled]="disabled">
|
<select class="w-25 form-select" [(ngModel)]="atom.operator" [disabled]="disabled">
|
||||||
<option *ngFor="let operator of getOperatorsForField(atom.field)" [ngValue]="operator.value">{{operator.label}}</option>
|
@for (operator of getOperatorsForField(atom.field); track operator.label) {
|
||||||
|
<option [ngValue]="operator.value">{{operator.label}}</option>
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
@switch (atom.operator) {
|
@switch (atom.operator) {
|
||||||
@case (CustomFieldQueryOperator.Exists) {
|
@case (CustomFieldQueryOperator.Exists) {
|
||||||
@@ -99,6 +103,8 @@
|
|||||||
<ng-select
|
<ng-select
|
||||||
class="paperless-input-select rounded-end"
|
class="paperless-input-select rounded-end"
|
||||||
[items]="getSelectOptionsForField(atom.field)"
|
[items]="getSelectOptionsForField(atom.field)"
|
||||||
|
bindLabel="label"
|
||||||
|
bindValue="id"
|
||||||
[(ngModel)]="atom.value"
|
[(ngModel)]="atom.value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user