mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'dev'
This commit is contained in:
commit
5821033e3d
@ -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",
|
||||
"service": "paperless-development",
|
||||
"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": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"mhutchie.git-graph",
|
||||
"ms-python.python"
|
||||
]
|
||||
"extensions": [
|
||||
"mhutchie.git-graph",
|
||||
"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
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
- ./redisdata:/data
|
||||
|
||||
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
|
||||
paperless-development:
|
||||
@ -43,14 +43,16 @@ services:
|
||||
volumes:
|
||||
- ..:/usr/src/paperless/paperless-ngx:delegated
|
||||
- ../.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/.pytest_cache
|
||||
- /usr/src/paperless/paperless-ngx/.ruff_cache
|
||||
- /usr/src/paperless/paperless-ngx/htmlcov
|
||||
- /usr/src/paperless/paperless-ngx/.coverage
|
||||
- data:/usr/src/paperless/paperless-ngx/data
|
||||
- media:/usr/src/paperless/paperless-ngx/media
|
||||
- ./data:/usr/src/paperless/paperless-ngx/data
|
||||
- ./media:/usr/src/paperless/paperless-ngx/media
|
||||
- ./consume:/usr/src/paperless/paperless-ngx/consume
|
||||
- ~/.gitconfig:/usr/src/paperless/.gitconfig:ro
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_TIKA_ENABLED: 1
|
||||
@ -78,7 +80,4 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
redisdata:
|
||||
pipenv:
|
||||
|
@ -2,42 +2,57 @@
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/manage.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": ["runserver"],
|
||||
"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",
|
||||
"args": [
|
||||
"runserver"
|
||||
],
|
||||
"django": true,
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"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": [
|
||||
"-A",
|
||||
"paperless",
|
||||
"worker",
|
||||
"-l",
|
||||
"DEBUG"
|
||||
]
|
||||
"document_consumer"
|
||||
],
|
||||
"django": true,
|
||||
"console": "integratedTerminal",
|
||||
"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",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "manage.py document_consumer",
|
||||
"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: Celery Worker",
|
||||
"description": "Start the Celery Worker which processes background and consume tasks",
|
||||
"type": "shell",
|
||||
"command": "pipenv run celery --app paperless worker -l DEBUG",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
},
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "custom",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": ".",
|
||||
"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",
|
||||
"command": "pipenv run python manage.py runserver",
|
||||
"group": "build",
|
||||
@ -37,100 +94,130 @@
|
||||
"options": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"label": "Maintenance: manage.py migrate",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -16,7 +16,7 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# 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
|
||||
DEFAULT_PYTHON_VERSION: "3.11"
|
||||
|
||||
@ -283,7 +283,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@ -299,7 +299,7 @@ jobs:
|
||||
path: src/
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@ -406,7 +406,7 @@ jobs:
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
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'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@ -414,7 +414,7 @@ jobs:
|
||||
-
|
||||
name: Login to Quay.io
|
||||
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'
|
||||
with:
|
||||
registry: quay.io
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -100,3 +100,9 @@ scripts/nuke
|
||||
|
||||
# celery schedule file
|
||||
celerybeat-schedule*
|
||||
|
||||
# ignore .devcontainer sub folders
|
||||
/.devcontainer/consume/
|
||||
/.devcontainer/data/
|
||||
/.devcontainer/media/
|
||||
/.devcontainer/redisdata/
|
||||
|
@ -5,7 +5,7 @@
|
||||
repos:
|
||||
# General hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
@ -46,9 +46,12 @@ repos:
|
||||
- ts
|
||||
- markdown
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
additional_dependencies:
|
||||
- prettier@3.3.3
|
||||
- 'prettier-plugin-organize-imports@4.1.0'
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.6.8'
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
- 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": ["docs/*.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
|
||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||
"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"]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"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"]
|
||||
"*/migrations/*.py" = ["E501", "SIM", "T201"]
|
||||
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001"]
|
||||
"src/documents/models.py" = ["SIM115"]
|
||||
|
||||
[lint.isort]
|
||||
force-single-line = true
|
||||
|
10
Dockerfile
10
Dockerfile
@ -39,7 +39,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& 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" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@ -233,11 +233,11 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.2-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 \
|
||||
--output psycopg_c-3.2.3-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 \
|
||||
--output psycopg_c-3.2.2-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 \
|
||||
--output psycopg_c-3.2.3-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 \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
|
14
Pipfile
14
Pipfile
@ -7,8 +7,8 @@ name = "pypi"
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=5.1.1"
|
||||
django-allauth = {extras = ["socialaccount"], version = "*"}
|
||||
django = "~=5.1.4"
|
||||
django-allauth = {extras = ["mfa", "socialaccount"], version = "*"}
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
@ -18,12 +18,12 @@ django-filter = "~=24.3"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
django-soft-delete = "*"
|
||||
djangorestframework = "==3.15.2"
|
||||
djangorestframework = "~=3.15.2"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
channels = "~=4.1"
|
||||
channels = "~=4.2"
|
||||
channels-redis = "*"
|
||||
concurrent-log-handler = "*"
|
||||
filelock = "*"
|
||||
@ -37,7 +37,7 @@ jinja2 = "~=3.1"
|
||||
langdetect = "*"
|
||||
mysqlclient = "*"
|
||||
nltk = "*"
|
||||
ocrmypdf = "~=16.5"
|
||||
ocrmypdf = "~=16.8"
|
||||
pathvalidate = "*"
|
||||
pdf2image = "*"
|
||||
psycopg = {version = "*", extras = ["c"]}
|
||||
@ -49,13 +49,13 @@ python-magic = "*"
|
||||
pyzbar = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.5"
|
||||
scikit-learn = "~=1.6"
|
||||
setproctitle = "*"
|
||||
tika-client = "*"
|
||||
tqdm = "*"
|
||||
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=4.0"
|
||||
watchdog = "~=6.0"
|
||||
whitenoise = "~=6.8"
|
||||
whoosh = "~=2.7"
|
||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
|
3926
Pipfile.lock
generated
3926
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,8 @@
|
||||
#
|
||||
# - Open portainer Stacks list and click 'Add stack'
|
||||
# - 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
|
||||
# - Open the list of containers, select paperless_webserver_1
|
||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||
@ -61,28 +63,8 @@ services:
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
# 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
|
||||
# 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
|
||||
env_file:
|
||||
- stack.env
|
||||
|
||||
volumes:
|
||||
data:
|
||||
|
@ -15,7 +15,8 @@ for command in decrypt_documents \
|
||||
document_sanity_checker \
|
||||
document_fuzzy_match \
|
||||
manage_superuser \
|
||||
convert_mariadb_uuid;
|
||||
convert_mariadb_uuid \
|
||||
prune_audit_logs;
|
||||
do
|
||||
echo "installing $command..."
|
||||
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
|
||||
|
@ -81,8 +81,8 @@ $ docker compose down
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
|
||||
```shell-session
|
||||
$ docker compose pull
|
||||
$ docker compose up
|
||||
docker compose pull
|
||||
docker compose up
|
||||
```
|
||||
|
||||
The Docker Compose files refer to the `latest` version, which is
|
||||
@ -91,9 +91,9 @@ $ docker compose down
|
||||
1. If you built the image yourself, do the following:
|
||||
|
||||
```shell-session
|
||||
$ git pull
|
||||
$ docker compose build
|
||||
$ docker compose up
|
||||
git pull
|
||||
docker compose build
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Running `docker compose up` will also apply any new database migrations.
|
||||
@ -155,7 +155,7 @@ following:
|
||||
environment before that, if you use one.
|
||||
|
||||
```shell-session
|
||||
$ pip install -r requirements.txt
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
!!! note
|
||||
@ -168,8 +168,8 @@ following:
|
||||
3. Migrate the database.
|
||||
|
||||
```shell-session
|
||||
$ cd src
|
||||
$ python3 manage.py migrate # (1)
|
||||
cd src
|
||||
python3 manage.py migrate # (1)
|
||||
```
|
||||
|
||||
1. Including `sudo -Hu <paperless_user>` may be required
|
||||
@ -241,6 +241,7 @@ document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z]
|
||||
|
||||
optional arguments:
|
||||
-c, --compare-checksums
|
||||
-cj, --compare-json
|
||||
-d, --delete
|
||||
-f, --use-filename-format
|
||||
-na, --no-archive
|
||||
@ -269,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
|
||||
"size". If that does not work out for you, specify `-c` or
|
||||
`--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
|
||||
you want paperless to also remove files that do not belong to the
|
||||
@ -622,3 +624,12 @@ document_fuzzy_match [--ratio] [--processes N]
|
||||
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
|
||||
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
|
||||
```
|
||||
|
17
docs/api.md
17
docs/api.md
@ -365,6 +365,10 @@ The endpoint supports the following optional form fields:
|
||||
- `custom_fields`: An array of custom field ids to assign (with an empty
|
||||
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
|
||||
process was started successfully, with the UUID of the consumption task
|
||||
as the data. No additional status information about the consumption process
|
||||
@ -473,6 +477,11 @@ The following methods are supported:
|
||||
- Requires `parameters`:
|
||||
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
|
||||
- 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
|
||||
|
||||
@ -556,3 +565,11 @@ Initial API version.
|
||||
|
||||
- Consumption templates were refactored to workflows and API endpoints
|
||||
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/`.
|
||||
|
@ -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).
|
||||
|
||||
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
|
||||
|
||||
@ -1523,7 +1523,7 @@ one pod).
|
||||
actual user ID on the host system, which you can get by executing
|
||||
|
||||
``` shell-session
|
||||
$ id -u
|
||||
id -u
|
||||
```
|
||||
|
||||
Paperless will change ownership on its folders to this user, so you
|
||||
@ -1538,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
|
||||
|
||||
``` shell-session
|
||||
$ id -g
|
||||
id -g
|
||||
```
|
||||
|
||||
Paperless will change ownership on its folders to this group, so you
|
||||
|
@ -69,13 +69,13 @@ first-time setup.
|
||||
3. Create `consume` and `media` directories:
|
||||
|
||||
```bash
|
||||
$ mkdir -p consume media
|
||||
mkdir -p consume media
|
||||
```
|
||||
|
||||
4. Install the Python dependencies:
|
||||
|
||||
```bash
|
||||
$ pipenv install --dev
|
||||
pipenv install --dev
|
||||
```
|
||||
|
||||
!!! note
|
||||
@ -85,7 +85,7 @@ first-time setup.
|
||||
5. Install pre-commit hooks:
|
||||
|
||||
```bash
|
||||
$ pre-commit install
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
6. Apply migrations and create a superuser for your development instance:
|
||||
@ -93,8 +93,8 @@ first-time setup.
|
||||
```bash
|
||||
# src/
|
||||
|
||||
$ python3 manage.py migrate
|
||||
$ python3 manage.py createsuperuser
|
||||
python3 manage.py migrate
|
||||
python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
7. You can now either ...
|
||||
@ -108,7 +108,7 @@ first-time setup.
|
||||
- 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 :-).
|
||||
@ -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:
|
||||
|
||||
```bash
|
||||
$ npm install -g @angular/cli
|
||||
npm install -g @angular/cli
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
npm install
|
||||
```
|
||||
|
||||
4. You can launch a development server by running:
|
||||
|
||||
```bash
|
||||
$ ng serve
|
||||
ng serve
|
||||
```
|
||||
|
||||
This will automatically update whenever you save. However, in-place
|
||||
@ -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:
|
||||
|
||||
```bash
|
||||
$ pipenv install --dev
|
||||
pipenv install --dev
|
||||
```
|
||||
|
||||
2. Build the documentation
|
||||
|
||||
```bash
|
||||
$ mkdocs build --config-file mkdocs.yml
|
||||
mkdocs build --config-file mkdocs.yml
|
||||
```
|
||||
|
||||
_alternatively..._
|
||||
@ -352,7 +352,7 @@ If you want to build the documentation locally, this is how you do it:
|
||||
something.
|
||||
|
||||
```bash
|
||||
$ mkdocs serve
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
## Building the Docker image
|
||||
@ -450,3 +450,26 @@ def myparser_consumer_declaration(sender, **kwargs):
|
||||
mime types have many extensions associated with them and the Python
|
||||
methods responsible for guessing the extension do not always return
|
||||
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**
|
||||
|
@ -30,7 +30,7 @@ account. The script essentially automatically performs the steps described in [D
|
||||
2. Download and run the installation script:
|
||||
|
||||
```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
|
||||
@ -135,13 +135,13 @@ account. The script essentially automatically performs the steps described in [D
|
||||
execute the following command:
|
||||
|
||||
```shell-session
|
||||
$ docker compose run --rm webserver createsuperuser
|
||||
docker compose run --rm webserver createsuperuser
|
||||
```
|
||||
|
||||
or using docker exec from within the container:
|
||||
|
||||
```shell-session
|
||||
$ python3 manage.py createsuperuser
|
||||
python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
This will guide you through the superuser setup.
|
||||
@ -188,7 +188,7 @@ account. The script essentially automatically performs the steps described in [D
|
||||
`docker compose pull` to pull the image, run
|
||||
|
||||
```shell-session
|
||||
$ docker compose build
|
||||
docker compose build
|
||||
```
|
||||
|
||||
instead to build the image.
|
||||
@ -557,8 +557,8 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
1. Stop paperless.
|
||||
|
||||
```bash
|
||||
$ cd /path/to/current/paperless
|
||||
$ docker compose down
|
||||
cd /path/to/current/paperless
|
||||
docker compose down
|
||||
```
|
||||
|
||||
2. Do a backup for two purposes: If something goes wrong, you still
|
||||
@ -582,7 +582,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
names of your volumes with
|
||||
|
||||
``` 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
|
||||
@ -603,7 +603,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
the search index:
|
||||
|
||||
```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
|
||||
@ -612,7 +612,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
8. Start paperless-ngx.
|
||||
|
||||
```bash
|
||||
$ docker compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This will run paperless in the background and automatically start it
|
||||
|
@ -18,7 +18,7 @@ Check for the following issues:
|
||||
automatically. Manually invoke the task processor by executing
|
||||
|
||||
```shell-session
|
||||
$ celery --app paperless worker
|
||||
celery --app paperless worker
|
||||
```
|
||||
|
||||
- Look at the output of paperless and inspect it for any errors.
|
||||
|
@ -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_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
|
||||
|
||||
!!! note
|
||||
@ -316,6 +322,8 @@ fields and permissions, which will be merged.
|
||||
|
||||
### Workflow Triggers
|
||||
|
||||
#### 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
|
||||
@ -325,8 +333,10 @@ Currently, there are three events that correspond to workflow trigger 'types':
|
||||
be used for filtering.
|
||||
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
|
||||
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
|
||||
flowchart TD
|
||||
@ -372,25 +382,49 @@ Workflows allow you to filter by:
|
||||
|
||||
### 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:
|
||||
|
||||
##### Assignment
|
||||
|
||||
"Assignment" actions can assign:
|
||||
|
||||
- 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
|
||||
|
||||
and "Removal" actions, which can remove either all of or specific sets of the following:
|
||||
##### Removal
|
||||
|
||||
"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
|
||||
|
||||
#### Title placeholders
|
||||
##### Email
|
||||
|
||||
Workflow titles 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 title is to be set), no automatic tags etc. have been
|
||||
"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:
|
||||
|
||||
- `{correspondent}`: assigned correspondent name
|
||||
@ -405,6 +439,7 @@ applied. You can use the following placeholders with any trigger type:
|
||||
- `{added_day}`: added day
|
||||
- `{added_time}`: added time in HH:MM format
|
||||
- `{original_filename}`: original file name without extension
|
||||
- `{filename}`: current file name without extension
|
||||
|
||||
The following placeholders are only available for "added" or "updated" triggers
|
||||
|
||||
@ -416,6 +451,7 @@ The following placeholders are only available for "added" or "updated" triggers
|
||||
- `{created_month_name_short}`: created month short name
|
||||
- `{created_day}`: created day
|
||||
- `{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
|
||||
|
||||
@ -752,8 +788,8 @@ Paperless-ngx consists of the following components:
|
||||
with paperless. You may start the webserver directly with
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless/src/
|
||||
$ gunicorn -c ../gunicorn.conf.py paperless.wsgi
|
||||
cd /path/to/paperless/src/
|
||||
gunicorn -c ../gunicorn.conf.py paperless.wsgi
|
||||
```
|
||||
|
||||
or by any other means such as Apache `mod_wsgi`.
|
||||
@ -768,8 +804,8 @@ Paperless-ngx consists of the following components:
|
||||
Start the consumer with the management command `document_consumer`:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless/src/
|
||||
$ python3 manage.py document_consumer
|
||||
cd /path/to/paperless/src/
|
||||
python3 manage.py document_consumer
|
||||
```
|
||||
|
||||
- **The task processor:** Paperless relies on [Celery - Distributed
|
||||
|
@ -330,8 +330,13 @@ SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!#$%&()*+,-./:;<=>?@[\]^_`{|}~' < /dev/ur
|
||||
|
||||
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
||||
|
||||
_split_langs="${OCR_LANGUAGE//+/ }"
|
||||
read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
|
||||
# OCR_LANG requires underscores, replace dashes if the user gave them with underscores
|
||||
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
|
||||
@ -344,10 +349,10 @@ read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
|
||||
echo "USERMAP_GID=$USERMAP_GID"
|
||||
fi
|
||||
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
||||
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
|
||||
echo "PAPERLESS_OCR_LANGUAGE=$ocr_langs"
|
||||
echo "PAPERLESS_SECRET_KEY='$SECRET_KEY'"
|
||||
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${OCR_LANGUAGES_ARRAY[*]} ]] ; then
|
||||
echo "PAPERLESS_OCR_LANGUAGES=${OCR_LANGUAGES_ARRAY[*]}"
|
||||
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${install_langs_array[*]} ]] ; then
|
||||
echo "PAPERLESS_OCR_LANGUAGES=${install_langs_array[*]}"
|
||||
fi
|
||||
} > docker-compose.env
|
||||
|
||||
|
@ -11,8 +11,7 @@
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"e2e/tsconfig.json"
|
||||
"tsconfig.json"
|
||||
],
|
||||
"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 ({
|
||||
page,
|
||||
@ -33,24 +34,3 @@ test('should apply appearance changes when set', async ({ page }) => {
|
||||
await page.getByLabel('Enable dark mode').click()
|
||||
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_HAR2 = 'e2e/dashboard/requests/api-dashboard2.har'
|
||||
const REQUESTS_HAR3 = 'e2e/dashboard/requests/api-dashboard3.har'
|
||||
const REQUESTS_HAR4 = 'e2e/dashboard/requests/api-dashboard4.har'
|
||||
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-dashboard1.har')
|
||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-dashboard2.har')
|
||||
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-dashboard3.har')
|
||||
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-dashboard4.har')
|
||||
|
||||
test('dashboard inbox link', async ({ page }) => {
|
||||
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_HAR2 = 'e2e/document-detail/requests/api-document-detail2.har'
|
||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
|
||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-detail2.har')
|
||||
|
||||
test('should activate / deactivate save button when changes are saved', async ({
|
||||
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_HAR2 = 'e2e/document-list/requests/api-document-list2.har'
|
||||
const REQUESTS_HAR3 = 'e2e/document-list/requests/api-document-list3.har'
|
||||
const REQUESTS_HAR4 = 'e2e/document-list/requests/api-document-list4.har'
|
||||
const REQUESTS_HAR5 = 'e2e/document-list/requests/api-document-list5.har'
|
||||
const REQUESTS_HAR6 = 'e2e/document-list/requests/api-document-list6.har'
|
||||
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-document-list1.har')
|
||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-list2.har')
|
||||
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-document-list3.har')
|
||||
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-document-list4.har')
|
||||
const REQUESTS_HAR5 = path.join(__dirname, 'requests/api-document-list5.har')
|
||||
const REQUESTS_HAR6 = path.join(__dirname, 'requests/api-document-list6.har')
|
||||
|
||||
test('basic filtering', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
@ -134,11 +135,11 @@ test('sorting', async ({ page }) => {
|
||||
test('change views', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
||||
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 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 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()
|
||||
})
|
||||
|
||||
|
@ -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 }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as webpack from 'webpack'
|
||||
import {
|
||||
CustomWebpackBrowserSchema,
|
||||
TargetOptions,
|
||||
} from '@angular-builders/custom-webpack'
|
||||
import * as webpack from 'webpack'
|
||||
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
|
||||
|
||||
export default (
|
||||
|
3158
src-ui/messages.xlf
3158
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
6085
src-ui/package-lock.json
generated
6085
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,46 +11,47 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^18.2.11",
|
||||
"@angular/common": "~18.2.10",
|
||||
"@angular/compiler": "~18.2.10",
|
||||
"@angular/core": "~18.2.10",
|
||||
"@angular/forms": "~18.2.10",
|
||||
"@angular/localize": "~18.2.10",
|
||||
"@angular/platform-browser": "~18.2.10",
|
||||
"@angular/platform-browser-dynamic": "~18.2.10",
|
||||
"@angular/router": "~18.2.10",
|
||||
"@ng-bootstrap/ng-bootstrap": "^17.0.1",
|
||||
"@ng-select/ng-select": "^13.9.1",
|
||||
"@angular/cdk": "^19.0.2",
|
||||
"@angular/common": "~19.0.3",
|
||||
"@angular/compiler": "~19.0.3",
|
||||
"@angular/core": "~19.0.3",
|
||||
"@angular/forms": "~19.0.3",
|
||||
"@angular/localize": "~19.0.3",
|
||||
"@angular/platform-browser": "~19.0.3",
|
||||
"@angular/platform-browser-dynamic": "~19.0.3",
|
||||
"@angular/router": "~19.0.3",
|
||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||
"@ng-select/ng-select": "^14.1.0",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^10.3.4",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^18.0.0",
|
||||
"ngx-cookie-service": "^19.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",
|
||||
"tslib": "^2.8.1",
|
||||
"utif": "^3.1.0",
|
||||
"uuid": "^11.0.2",
|
||||
"zone.js": "^0.14.8"
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^18.0.0",
|
||||
"@angular-builders/jest": "^18.0.0",
|
||||
"@angular-devkit/build-angular": "^18.2.2",
|
||||
"@angular-devkit/core": "^18.2.11",
|
||||
"@angular-devkit/schematics": "^18.2.11",
|
||||
"@angular-eslint/builder": "18.4.0",
|
||||
"@angular-eslint/eslint-plugin": "18.4.0",
|
||||
"@angular-eslint/eslint-plugin-template": "18.4.0",
|
||||
"@angular-eslint/schematics": "18.4.0",
|
||||
"@angular-eslint/template-parser": "18.4.0",
|
||||
"@angular/cli": "~18.2.11",
|
||||
"@angular/compiler-cli": "~18.2.2",
|
||||
"@angular-builders/custom-webpack": "^19.0.0-beta.0",
|
||||
"@angular-builders/jest": "^19.0.0-beta.1",
|
||||
"@angular-devkit/build-angular": "^19.0.4",
|
||||
"@angular-devkit/core": "^19.0.4",
|
||||
"@angular-devkit/schematics": "^19.0.4",
|
||||
"@angular-eslint/builder": "19.0.0",
|
||||
"@angular-eslint/eslint-plugin": "19.0.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.0",
|
||||
"@angular-eslint/schematics": "19.0.0",
|
||||
"@angular-eslint/template-parser": "19.0.0",
|
||||
"@angular/cli": "~19.0.4",
|
||||
"@angular/compiler-cli": "~19.0.3",
|
||||
"@codecov/webpack-plugin": "^1.2.1",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
@ -61,10 +62,12 @@
|
||||
"eslint": "^9.14.0",
|
||||
"jest": "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",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"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 { 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.TextDecoder = TextDecoder
|
||||
|
||||
|
@ -1,32 +1,33 @@
|
||||
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 { 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 { DocumentListComponent } from './components/document-list/document-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 { LogsComponent } from './components/admin/logs/logs.component'
|
||||
import { SettingsComponent } from './components/admin/settings/settings.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 { MailComponent } from './components/manage/mail/mail.component'
|
||||
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
import { TasksComponent } from './components/admin/tasks/tasks.component'
|
||||
import { PermissionsGuard } from './guards/permissions.guard'
|
||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||
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 { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
import { PermissionsGuard } from './guards/permissions.guard'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
} 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 = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@ -165,6 +166,10 @@ export const routes: Routes = [
|
||||
path: 'settings/usersgroups',
|
||||
redirectTo: '/usersgroups',
|
||||
},
|
||||
{
|
||||
path: 'settings/savedviews',
|
||||
redirectTo: '/savedviews',
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
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 {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
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 { routes } from './app-routing.module'
|
||||
import { AppComponent } from './app.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 {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} 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 { PermissionsGuard } from './guards/permissions.guard'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { PermissionsService } from './services/permissions.service'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { Toast, ToastService } from './services/toast.service'
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent
|
||||
@ -39,12 +40,15 @@ describe('AppComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent, ToastsComponent, FileDropComponent],
|
||||
imports: [
|
||||
TourNgBootstrapModule,
|
||||
RouterModule.forRoot(routes),
|
||||
NgxFileDropModule,
|
||||
NgbModalModule,
|
||||
AppComponent,
|
||||
ToastsComponent,
|
||||
FileDropComponent,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
providers: [
|
||||
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 { Router } from '@angular/router'
|
||||
import { Subscription, first } from 'rxjs'
|
||||
import { Router, RouterOutlet } from '@angular/router'
|
||||
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 { ToastService } from './services/toast.service'
|
||||
import { TasksService } from './services/tasks.service'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { HotKeyService } from './services/hot-key.service'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} 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({
|
||||
selector: 'pngx-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
imports: [
|
||||
FileDropComponent,
|
||||
ToastsComponent,
|
||||
TourNgBootstrapModule,
|
||||
RouterOutlet,
|
||||
],
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
newDocumentSubscription: Subscription
|
||||
@ -165,7 +173,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
[
|
||||
{
|
||||
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',
|
||||
delayAfterNavigation: 500,
|
||||
isOptional: false,
|
||||
@ -227,7 +235,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
{
|
||||
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',
|
||||
backdropConfig: {
|
||||
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 { ConfigComponent } from './config.component'
|
||||
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
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 { 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', () => {
|
||||
let component: ConfigComponent
|
||||
@ -29,15 +29,6 @@ describe('ConfigComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConfigComponent,
|
||||
TextComponent,
|
||||
SelectComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
FileComponent,
|
||||
PageHeaderComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
NgbModule,
|
||||
@ -45,6 +36,13 @@ describe('ConfigComponent', () => {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ConfigComponent,
|
||||
TextComponent,
|
||||
SelectComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
FileComponent,
|
||||
PageHeaderComponent,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
|
@ -1,33 +1,60 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
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 {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
Subject,
|
||||
Subscription,
|
||||
first,
|
||||
takeUntil,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
PaperlessConfigOptions,
|
||||
ConfigCategory,
|
||||
ConfigOption,
|
||||
ConfigOptionType,
|
||||
PaperlessConfig,
|
||||
PaperlessConfigOptions,
|
||||
} from 'src/app/data/paperless-config'
|
||||
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 { 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({
|
||||
selector: 'pngx-config',
|
||||
templateUrl: './config.component.html',
|
||||
styleUrl: './config.component.scss',
|
||||
imports: [
|
||||
PageHeaderComponent,
|
||||
SelectComponent,
|
||||
SwitchComponent,
|
||||
TextComponent,
|
||||
NumberComponent,
|
||||
FileComponent,
|
||||
AsyncPipe,
|
||||
NgbNavModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class ConfigComponent
|
||||
extends ComponentWithPermissions
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
public readonly ConfigOptionType = ConfigOptionType
|
||||
@ -45,15 +72,11 @@ export class ConfigComponent
|
||||
return PaperlessConfigOptions.filter((o) => o.category === category)
|
||||
}
|
||||
|
||||
public loading: boolean = false
|
||||
|
||||
initialConfig: PaperlessConfig
|
||||
store: BehaviorSubject<any>
|
||||
storeSub: Subscription
|
||||
isDirty$: Observable<boolean>
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService,
|
||||
@ -67,7 +90,6 @@ export class ConfigComponent
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true
|
||||
this.configService
|
||||
.getConfig()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
|
@ -4,7 +4,7 @@
|
||||
info="Review the log files for the application and for email checking."
|
||||
i18n-info>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
|
||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
@ -17,7 +17,7 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if (isLoading || !logFiles.length) {
|
||||
@if (loading || !logFiles.length) {
|
||||
<div class="ps-2 d-flex align-items-center">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
@if (!logFiles.length) {
|
||||
@ -30,15 +30,13 @@
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
||||
@if (isLoading && logFiles.length) {
|
||||
@if (loading && logFiles.length) {
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
}
|
||||
@for (log of logs; track log) {
|
||||
<p
|
||||
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
|
||||
>{{log}}</p>
|
||||
@for (log of logs; track $index) {
|
||||
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,18 +1,13 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { BrowserModule, By } from '@angular/platform-browser'
|
||||
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { LogService } from 'src/app/services/rest/log.service'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.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 = [
|
||||
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
||||
@ -37,11 +32,12 @@ describe('LogsComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogsComponent, PageHeaderComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
LogsComponent,
|
||||
PageHeaderComponent,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
@ -90,8 +86,7 @@ describe('LogsComponent', () => {
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
component.toggleAutoRefresh()
|
||||
expect(component.autoRefreshInterval).toBeNull()
|
||||
component.autoRefreshEnabled = false
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
@ -1,24 +1,39 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
} 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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
imports: [
|
||||
PageHeaderComponent,
|
||||
NgbNavModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class LogsComponent implements OnInit, OnDestroy {
|
||||
export class LogsComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private changedetectorRef: ChangeDetectorRef
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public logs: string[] = []
|
||||
|
||||
@ -26,50 +41,50 @@ export class LogsComponent implements OnInit, OnDestroy {
|
||||
|
||||
public activeLog: string
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
public isLoading: boolean = false
|
||||
|
||||
public autoRefreshInterval: any
|
||||
public autoRefreshEnabled: boolean = true
|
||||
|
||||
@ViewChild('logContainer') logContainer: ElementRef
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoading = true
|
||||
this.logService
|
||||
.list()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => {
|
||||
this.logFiles = result
|
||||
this.isLoading = false
|
||||
this.loading = false
|
||||
if (this.logFiles.length > 0) {
|
||||
this.activeLog = this.logFiles[0]
|
||||
this.reloadLogs()
|
||||
}
|
||||
this.toggleAutoRefresh()
|
||||
timer(5000, 5000)
|
||||
.pipe(
|
||||
filter(() => this.autoRefreshEnabled),
|
||||
takeUntil(this.unsubscribeNotifier)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reloadLogs()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
clearInterval(this.autoRefreshInterval)
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
reloadLogs() {
|
||||
this.isLoading = true
|
||||
this.loading = true
|
||||
this.logService
|
||||
.get(this.activeLog)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.logs = result
|
||||
this.isLoading = false
|
||||
this.loading = false
|
||||
this.scrollToBottom()
|
||||
},
|
||||
error: () => {
|
||||
this.logs = []
|
||||
this.isLoading = false
|
||||
this.loading = false
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -96,15 +111,4 @@ export class LogsComponent implements OnInit, OnDestroy {
|
||||
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
|
||||
title="Settings"
|
||||
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
|
||||
>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
||||
@ -39,193 +39,204 @@
|
||||
<li [ngbNavItem]="SettingsNavIDs.General">
|
||||
<a ngbNavLink i18n>General</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Appearance</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<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 class="row">
|
||||
<div class="col-xl-6 pe-xl-5">
|
||||
<h4 i18n>Appearance</h4>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Display language</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 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>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 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">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
<h4 class="mt-4" i18n>Global search</h4>
|
||||
<div class="row mb-3">
|
||||
<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>
|
||||
|
||||
@ -239,7 +250,7 @@
|
||||
<h4 i18n>Default Permissions</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<div class="col">
|
||||
<p i18n>
|
||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||
</p>
|
||||
@ -321,7 +332,7 @@
|
||||
<h4 i18n>Document processing</h4>
|
||||
|
||||
<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 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>
|
||||
@ -331,87 +342,6 @@
|
||||
|
||||
</ng-template>
|
||||
</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>
|
||||
|
||||
<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 { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
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 {
|
||||
NgbModule,
|
||||
NgbAlertModule,
|
||||
NgbNavLink,
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
NgbModule,
|
||||
NgbNavLink,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of, throwError } from 'rxjs'
|
||||
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 { 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 { 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 { 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 { 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 { CheckComponent } from '../../common/input/check/check.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 { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.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 { TextComponent } from '../../common/input/text/text.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 { SystemStatusService } from 'src/app/services/system-status.service'
|
||||
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'
|
||||
import { SettingsComponent } from './settings.component'
|
||||
|
||||
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 = [
|
||||
{ id: 1, username: 'user1', is_superuser: false },
|
||||
{ id: 2, username: 'user2', is_superuser: false },
|
||||
@ -70,7 +64,6 @@ describe('SettingsComponent', () => {
|
||||
let fixture: ComponentFixture<SettingsComponent>
|
||||
let router: Router
|
||||
let settingsService: SettingsService
|
||||
let savedViewService: SavedViewService
|
||||
let activatedRoute: ActivatedRoute
|
||||
let viewportScroller: ViewportScroller
|
||||
let toastService: ToastService
|
||||
@ -82,7 +75,16 @@ describe('SettingsComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
imports: [
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAlertModule,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgbModalModule,
|
||||
DragDropModule,
|
||||
SettingsComponent,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
@ -101,17 +103,6 @@ describe('SettingsComponent', () => {
|
||||
ConfirmButtonComponent,
|
||||
DragDropSelectComponent,
|
||||
],
|
||||
imports: [
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAlertModule,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgbModalModule,
|
||||
DragDropModule,
|
||||
],
|
||||
providers: [
|
||||
CustomDatePipe,
|
||||
DatePipe,
|
||||
@ -139,7 +130,6 @@ describe('SettingsComponent', () => {
|
||||
.spyOn(permissionsService, 'currentUserOwnsObject')
|
||||
.mockReturnValue(true)
|
||||
groupService = TestBed.inject(GroupService)
|
||||
savedViewService = TestBed.inject(SavedViewService)
|
||||
})
|
||||
|
||||
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)
|
||||
component = fixture.componentInstance
|
||||
@ -184,8 +165,6 @@ describe('SettingsComponent', () => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
|
||||
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
|
||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'savedviews'])
|
||||
|
||||
const initSpy = jest.spyOn(component, 'initialize')
|
||||
component.isDirty = true // mock dirty
|
||||
@ -213,90 +192,8 @@ describe('SettingsComponent', () => {
|
||||
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', () => {
|
||||
completeSetup()
|
||||
jest.spyOn(savedViewService, 'patchMany').mockReturnValue(of([]))
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
|
||||
@ -315,7 +212,7 @@ describe('SettingsComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(storeSpy).toHaveBeenCalled()
|
||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||
expect(setSpy).toHaveBeenCalledTimes(27)
|
||||
expect(setSpy).toHaveBeenCalledTimes(28)
|
||||
|
||||
// succeed
|
||||
storeSpy.mockReturnValueOnce(of(true))
|
||||
@ -326,7 +223,6 @@ describe('SettingsComponent', () => {
|
||||
|
||||
it('should offer reload if settings changes require', () => {
|
||||
completeSetup()
|
||||
jest.spyOn(savedViewService, 'patchMany').mockReturnValue(of([]))
|
||||
let toast: Toast
|
||||
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
||||
component.initialize(true) // reset
|
||||
@ -361,18 +257,6 @@ describe('SettingsComponent', () => {
|
||||
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', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
|
@ -1,56 +1,69 @@
|
||||
import { ViewportScroller } from '@angular/common'
|
||||
import { AsyncPipe, ViewportScroller } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
AfterViewInit,
|
||||
OnDestroy,
|
||||
Component,
|
||||
Inject,
|
||||
LOCALE_ID,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { FormGroup, FormControl } from '@angular/forms'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModalRef,
|
||||
NgbNavChangeEvent,
|
||||
NgbNavModule,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subscription,
|
||||
Observable,
|
||||
Subject,
|
||||
Subscription,
|
||||
first,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
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 { 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 {
|
||||
PermissionsService,
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
PermissionsService,
|
||||
} from 'src/app/services/permissions.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 {
|
||||
SettingsService,
|
||||
LanguageOption,
|
||||
SettingsService,
|
||||
} 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 {
|
||||
SystemStatusItemStatus,
|
||||
SystemStatus,
|
||||
} from 'src/app/data/system-status'
|
||||
import { DisplayMode } from 'src/app/data/document'
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||
import { CheckComponent } from '../../common/input/check/check.component'
|
||||
import { ColorComponent } from '../../common/input/color/color.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 { 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 {
|
||||
General = 1,
|
||||
@ -69,15 +82,28 @@ const systemDateFormat = {
|
||||
selector: 'pngx-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
imports: [
|
||||
PageHeaderComponent,
|
||||
CheckComponent,
|
||||
ColorComponent,
|
||||
SelectComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
CustomDatePipe,
|
||||
IfPermissionsDirective,
|
||||
AsyncPipe,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbNavModule,
|
||||
NgbPopoverModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class SettingsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
activeNavID: number
|
||||
DisplayMode = DisplayMode
|
||||
|
||||
savedViewGroup = new FormGroup({})
|
||||
|
||||
settingsForm = new FormGroup({
|
||||
bulkEditConfirmationDialogs: new FormControl(null),
|
||||
@ -88,7 +114,6 @@ export class SettingsComponent
|
||||
darkModeEnabled: new FormControl(null),
|
||||
darkModeInvertThumbs: new FormControl(null),
|
||||
themeColor: new FormControl(null),
|
||||
useNativePdfViewer: new FormControl(null),
|
||||
displayLanguage: new FormControl(null),
|
||||
dateLocale: new FormControl(null),
|
||||
dateFormat: new FormControl(null),
|
||||
@ -99,7 +124,9 @@ export class SettingsComponent
|
||||
defaultPermsViewGroups: new FormControl(null),
|
||||
defaultPermsEditUsers: new FormControl(null),
|
||||
defaultPermsEditGroups: new FormControl(null),
|
||||
useNativePdfViewer: new FormControl(null),
|
||||
documentEditingRemoveInboxTags: new FormControl(null),
|
||||
documentEditingOverlayThumbnail: new FormControl(null),
|
||||
searchDbOnly: new FormControl(null),
|
||||
searchLink: new FormControl(null),
|
||||
|
||||
@ -109,14 +136,9 @@ export class SettingsComponent
|
||||
notificationsConsumerSuppressOnDashboard: new FormControl(null),
|
||||
|
||||
savedViewsWarnOnUnsavedChange: new FormControl(null),
|
||||
savedViews: this.savedViewGroup,
|
||||
})
|
||||
|
||||
savedViews: SavedView[]
|
||||
SettingsNavIDs = SettingsNavIDs
|
||||
get displayFields() {
|
||||
return this.settings.allDisplayFields
|
||||
}
|
||||
|
||||
store: BehaviorSubject<any>
|
||||
storeSub: Subscription
|
||||
@ -151,7 +173,6 @@ export class SettingsComponent
|
||||
}
|
||||
|
||||
constructor(
|
||||
public savedViewService: SavedViewService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private toastService: ToastService,
|
||||
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) => {
|
||||
const section = paramMap.get('section')
|
||||
if (section) {
|
||||
@ -234,9 +243,6 @@ export class SettingsComponent
|
||||
if (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(
|
||||
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),
|
||||
searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE),
|
||||
savedViews: {},
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,15 +331,11 @@ export class SettingsComponent
|
||||
this.router
|
||||
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
||||
.then((navigated) => {
|
||||
this.settings.organizingSidebarSavedViews = false
|
||||
if (!navigated && this.isDirty) {
|
||||
this.activeNavID = navChangeEvent.activeId
|
||||
} else if (navigated && this.isDirty) {
|
||||
this.initialize()
|
||||
}
|
||||
if (this.activeNavID === SettingsNavIDs.SavedViews) {
|
||||
this.settings.organizingSidebarSavedViews = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -342,34 +346,6 @@ export class SettingsComponent
|
||||
|
||||
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.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() {
|
||||
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
|
||||
this.storeSub && this.storeSub.unsubscribe()
|
||||
this.settings.organizingSidebarSavedViews = false
|
||||
}
|
||||
|
||||
deleteSavedView(savedView: SavedView) {
|
||||
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() {
|
||||
public saveSettings() {
|
||||
this.savePending = true
|
||||
const reloadRequired =
|
||||
this.settingsForm.value.displayLanguage !=
|
||||
@ -539,6 +495,10 @@ export class SettingsComponent
|
||||
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
|
||||
this.settingsForm.value.documentEditingRemoveInboxTags
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL,
|
||||
this.settingsForm.value.documentEditingOverlayThumbnail
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.SEARCH_DB_ONLY,
|
||||
this.settingsForm.value.searchDbOnly
|
||||
@ -592,31 +552,6 @@ export class SettingsComponent
|
||||
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() {
|
||||
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."
|
||||
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">
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||
<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}}
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
|
||||
<div class="form-inline d-flex align-items-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,13 +143,13 @@
|
||||
</div>
|
||||
</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">
|
||||
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
||||
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="completed">
|
||||
@ -132,7 +157,7 @@
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="started">
|
||||
@ -140,7 +165,7 @@
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="queued">
|
||||
@ -148,7 +173,7 @@
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -26,3 +26,14 @@ pre {
|
||||
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModalRef,
|
||||
NgbModule,
|
||||
NgbNavItem,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskType,
|
||||
PaperlessTaskStatus,
|
||||
PaperlessTaskType,
|
||||
} from 'src/app/data/paperless-task'
|
||||
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 { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { TasksComponent } from './tasks.component'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { TasksComponent, TaskTab } from './tasks.component'
|
||||
|
||||
const tasks: PaperlessTask[] = [
|
||||
{
|
||||
@ -119,18 +119,16 @@ describe('TasksComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TasksComponent,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
ConfirmDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
FormsModule,
|
||||
TasksComponent,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
ConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@ -167,7 +165,7 @@ describe('TasksComponent', () => {
|
||||
let currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Failed
|
||||
).length
|
||||
component.activeTab = 'failed'
|
||||
component.activeTab = TaskTab.Failed
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[0].nativeElement.textContent).toEqual(
|
||||
`Failed${currentTasksLength}`
|
||||
@ -179,7 +177,7 @@ describe('TasksComponent', () => {
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Complete
|
||||
).length
|
||||
component.activeTab = 'completed'
|
||||
component.activeTab = TaskTab.Completed
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[1].nativeElement.textContent).toEqual(
|
||||
`Complete${currentTasksLength}`
|
||||
@ -188,7 +186,7 @@ describe('TasksComponent', () => {
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Started
|
||||
).length
|
||||
component.activeTab = 'started'
|
||||
component.activeTab = TaskTab.Started
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[2].nativeElement.textContent).toEqual(
|
||||
`Started${currentTasksLength}`
|
||||
@ -197,7 +195,7 @@ describe('TasksComponent', () => {
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Pending
|
||||
).length
|
||||
component.activeTab = 'queued'
|
||||
component.activeTab = TaskTab.Queued
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[3].nativeElement.textContent).toEqual(
|
||||
`Queued${currentTasksLength}`
|
||||
@ -206,7 +204,7 @@ describe('TasksComponent', () => {
|
||||
|
||||
it('should to go page 1 between tab switch', () => {
|
||||
component.page = 10
|
||||
component.duringTabChange(2)
|
||||
component.duringTabChange()
|
||||
expect(component.page).toEqual(1)
|
||||
})
|
||||
|
||||
@ -283,10 +281,63 @@ describe('TasksComponent', () => {
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1)
|
||||
jest.advanceTimersByTime(5000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
component.toggleAutoRefresh()
|
||||
expect(component.autoRefreshInterval).toBeNull()
|
||||
component.autoRefreshEnabled = false
|
||||
jest.advanceTimersByTime(6000)
|
||||
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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import {
|
||||
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 { 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 { 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({
|
||||
selector: 'pngx-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss'],
|
||||
imports: [
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
SlicePipe,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgTemplateOutlet,
|
||||
NgbCollapseModule,
|
||||
NgbDropdownModule,
|
||||
NgbNavModule,
|
||||
NgbPaginationModule,
|
||||
NgbPopoverModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class TasksComponent
|
||||
extends ComponentWithPermissions
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
public activeTab: string
|
||||
public activeTab: TaskTab
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
public expandedTask: number
|
||||
@ -24,7 +77,27 @@ export class TasksComponent
|
||||
public pageSize: number = 25
|
||||
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 {
|
||||
return this.selectedTasks.size > 0
|
||||
@ -42,12 +115,28 @@ export class TasksComponent
|
||||
|
||||
ngOnInit() {
|
||||
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() {
|
||||
super.ngOnDestroy()
|
||||
this.tasksService.cancelPending()
|
||||
clearInterval(this.autoRefreshInterval)
|
||||
}
|
||||
|
||||
dismissTask(task: PaperlessTask) {
|
||||
@ -96,19 +185,30 @@ export class TasksComponent
|
||||
get currentTasks(): PaperlessTask[] {
|
||||
let tasks: PaperlessTask[] = []
|
||||
switch (this.activeTab) {
|
||||
case 'queued':
|
||||
case TaskTab.Queued:
|
||||
tasks = this.tasksService.queuedFileTasks
|
||||
break
|
||||
case 'started':
|
||||
case TaskTab.Started:
|
||||
tasks = this.tasksService.startedFileTasks
|
||||
break
|
||||
case 'completed':
|
||||
case TaskTab.Completed:
|
||||
tasks = this.tasksService.completedFileTasks
|
||||
break
|
||||
case 'failed':
|
||||
case TaskTab.Failed:
|
||||
tasks = this.tasksService.failedFileTasks
|
||||
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
|
||||
}
|
||||
|
||||
@ -125,31 +225,37 @@ export class TasksComponent
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
|
||||
duringTabChange(navID: number) {
|
||||
duringTabChange() {
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
beforeTabChange() {
|
||||
this.resetFilter()
|
||||
this.filterTargetID = TaskFilterTargetID.Name
|
||||
}
|
||||
|
||||
get activeTabLocalized(): string {
|
||||
switch (this.activeTab) {
|
||||
case 'queued':
|
||||
case TaskTab.Queued:
|
||||
return $localize`queued`
|
||||
case 'started':
|
||||
case TaskTab.Started:
|
||||
return $localize`started`
|
||||
case 'completed':
|
||||
case TaskTab.Completed:
|
||||
return $localize`completed`
|
||||
case 'failed':
|
||||
case TaskTab.Failed:
|
||||
return $localize`failed`
|
||||
}
|
||||
}
|
||||
|
||||
toggleAutoRefresh(): void {
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval)
|
||||
this.autoRefreshInterval = null
|
||||
} else {
|
||||
this.autoRefreshInterval = setInterval(() => {
|
||||
this.tasksService.reload()
|
||||
}, 5000)
|
||||
public resetFilter() {
|
||||
this._filterText = ''
|
||||
}
|
||||
|
||||
filterInputKeyup(event: KeyboardEvent) {
|
||||
if (event.key == 'Enter') {
|
||||
this._filterText = (event.target as HTMLInputElement).value
|
||||
} else if (event.key === 'Escape') {
|
||||
this.resetFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (isLoading) {
|
||||
@if (loading) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
@ -47,15 +47,20 @@
|
||||
</tr>
|
||||
}
|
||||
@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>
|
||||
<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();">
|
||||
<label class="form-check-label" for="{{document.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td scope="row">{{ document.title }}</td>
|
||||
<td scope="row" i18n>{{ getDaysRemaining(document) }} days</td>
|
||||
<td scope="row">
|
||||
{{ 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">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
@ -83,7 +88,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (!isLoading) {
|
||||
@if (!loading) {
|
||||
<div class="d-flex mb-2">
|
||||
<div>
|
||||
<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 { TrashComponent } from './trash.component'
|
||||
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 {
|
||||
NgbModal,
|
||||
NgbPaginationModule,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
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 { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
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 = [
|
||||
{
|
||||
@ -38,15 +39,10 @@ describe('TrashComponent', () => {
|
||||
let trashService: TrashService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
let router: Router
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TrashComponent,
|
||||
PageHeaderComponent,
|
||||
ConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
@ -54,6 +50,10 @@ describe('TrashComponent', () => {
|
||||
NgbPopoverModule,
|
||||
NgbPaginationModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
TrashComponent,
|
||||
PageHeaderComponent,
|
||||
ConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@ -61,11 +61,13 @@ describe('TrashComponent', () => {
|
||||
trashService = TestBed.inject(TrashService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
router = TestBed.inject(Router)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should call correct service method on reload', () => {
|
||||
jest.useFakeTimers()
|
||||
const trashSpy = jest.spyOn(trashService, 'getTrash')
|
||||
trashSpy.mockReturnValue(
|
||||
of({
|
||||
@ -75,6 +77,7 @@ describe('TrashComponent', () => {
|
||||
})
|
||||
)
|
||||
component.reload()
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(trashSpy).toHaveBeenCalled()
|
||||
expect(component.documentsInTrash).toEqual(documentsInTrash)
|
||||
})
|
||||
@ -161,6 +164,22 @@ describe('TrashComponent', () => {
|
||||
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', () => {
|
||||
component.documentsInTrash = documentsInTrash
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
|
@ -1,49 +1,74 @@
|
||||
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 { 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 { TrashService } from 'src/app/services/trash.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-trash',
|
||||
templateUrl: './trash.component.html',
|
||||
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 selectedDocuments: Set<number> = new Set()
|
||||
public allToggled: boolean = false
|
||||
public page: number = 1
|
||||
public totalDocuments: number
|
||||
public isLoading: boolean = false
|
||||
unsubscribeNotifier: Subject<void> = new Subject()
|
||||
|
||||
constructor(
|
||||
private trashService: TrashService,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private settingsService: SettingsService
|
||||
private settingsService: SettingsService,
|
||||
private router: Router
|
||||
) {
|
||||
super()
|
||||
this.reload()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeNotifier.next()
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.isLoading = true
|
||||
this.trashService.getTrash(this.page).subscribe((r) => {
|
||||
this.documentsInTrash = r.results
|
||||
this.totalDocuments = r.count
|
||||
this.isLoading = false
|
||||
this.selectedDocuments.clear()
|
||||
})
|
||||
this.loading = true
|
||||
this.trashService
|
||||
.getTrash(this.page)
|
||||
.pipe(
|
||||
tap((r) => {
|
||||
this.documentsInTrash = r.results
|
||||
this.totalDocuments = r.count
|
||||
this.selectedDocuments.clear()
|
||||
this.loading = false
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
})
|
||||
}
|
||||
|
||||
delete(document: Document) {
|
||||
@ -110,7 +135,14 @@ export class TrashComponent implements OnDestroy {
|
||||
restore(document: Document) {
|
||||
this.trashService.restoreDocuments([document.id]).subscribe({
|
||||
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()
|
||||
},
|
||||
error: (err) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
@ -6,22 +7,13 @@ import {
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModule,
|
||||
NgbAlertModule,
|
||||
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 { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { Group } from 'src/app/data/group'
|
||||
import { User } from 'src/app/data/user'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
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 { GroupService } from 'src/app/services/rest/group.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 { 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 { 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 { 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 = [
|
||||
{ id: 1, username: 'user1', is_superuser: false },
|
||||
@ -67,33 +45,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
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),
|
||||
],
|
||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||
providers: [
|
||||
CustomDatePipe,
|
||||
DatePipe,
|
||||
|
@ -1,23 +1,31 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
import { Group } from 'src/app/data/group'
|
||||
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 { GroupService } from 'src/app/services/rest/group.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 { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-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 { 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 { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-users-groups',
|
||||
templateUrl: './users-groups.component.html',
|
||||
styleUrls: ['./users-groups.component.scss'],
|
||||
imports: [
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class UsersAndGroupsComponent
|
||||
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"
|
||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||
(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>
|
||||
</a>
|
||||
</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"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
|
||||
tourAnchor="tour.workflows">
|
||||
@ -327,7 +334,7 @@
|
||||
</a>
|
||||
}
|
||||
} @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"
|
||||
container="body">
|
||||
<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
|
||||
*/
|
||||
@ -15,6 +12,7 @@
|
||||
overflow-y: auto;
|
||||
--pngx-sidebar-width: 100%;
|
||||
max-width: var(--pngx-sidebar-width);
|
||||
transition: all .2s ease;
|
||||
|
||||
.sidebar-heading .spinner-border {
|
||||
width: 0.8em;
|
||||
@ -37,8 +35,6 @@
|
||||
@media (min-width: 2400px) {
|
||||
--pngx-sidebar-width: 8.33333333%;
|
||||
}
|
||||
|
||||
transition: all .2s ease;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
@ -48,6 +44,13 @@
|
||||
|
||||
main {
|
||||
transition: all .2s ease;
|
||||
padding-top: 110px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding-top: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-slim-toggler {
|
||||
|
@ -1,43 +1,43 @@
|
||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing'
|
||||
import { AppFrameComponent } from './app-frame.component'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} 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 { 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 { 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 {
|
||||
DjangoMessageLevel,
|
||||
DjangoMessagesService,
|
||||
} from 'src/app/services/django-messages.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
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 { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { SettingsService } from 'src/app/services/settings.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 { 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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
|
||||
const saved_views = [
|
||||
{
|
||||
@ -95,11 +95,6 @@ describe('AppFrameComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppFrameComponent,
|
||||
IfPermissionsDirective,
|
||||
GlobalSearchComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
@ -109,6 +104,9 @@ describe('AppFrameComponent', () => {
|
||||
DragDropModule,
|
||||
NgbModalModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
AppFrameComponent,
|
||||
IfPermissionsDirective,
|
||||
GlobalSearchComponent,
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
@ -343,6 +341,7 @@ describe('AppFrameComponent', () => {
|
||||
component.editProfile()
|
||||
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
|
||||
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 { 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 { first } from 'rxjs/operators'
|
||||
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 {
|
||||
DjangoMessageLevel,
|
||||
DjangoMessagesService,
|
||||
} from 'src/app/services/django-messages.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.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 { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import {
|
||||
CdkDragStart,
|
||||
CdkDragEnd,
|
||||
CdkDragDrop,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
AppRemoteVersion,
|
||||
RemoteVersionService,
|
||||
} from 'src/app/services/rest/remote-version.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
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 { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
import { GlobalSearchComponent } from './global-search/global-search.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-app-frame',
|
||||
templateUrl: './app-frame.component.html',
|
||||
styleUrls: ['./app-frame.component.scss'],
|
||||
imports: [
|
||||
GlobalSearchComponent,
|
||||
DocumentTitlePipe,
|
||||
IfPermissionsDirective,
|
||||
RouterModule,
|
||||
NgClass,
|
||||
NgbDropdownModule,
|
||||
NgbPopoverModule,
|
||||
NgbCollapseModule,
|
||||
NgbNavModule,
|
||||
NgxBootstrapIconsModule,
|
||||
DragDropModule,
|
||||
TourNgBootstrapModule,
|
||||
],
|
||||
})
|
||||
export class AppFrameComponent
|
||||
extends ComponentWithPermissions
|
||||
@ -81,7 +108,14 @@ export class AppFrameComponent
|
||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||
this.checkForUpdates()
|
||||
}
|
||||
this.tasksService.reload()
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.PaperlessTask
|
||||
)
|
||||
) {
|
||||
this.tasksService.reload()
|
||||
}
|
||||
|
||||
this.djangoMessagesService.get().forEach((message) => {
|
||||
switch (message.level) {
|
||||
@ -136,6 +170,7 @@ export class AppFrameComponent
|
||||
editProfile() {
|
||||
this.modalService.open(ProfileEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
this.closeMenu()
|
||||
}
|
||||
|
@ -49,7 +49,7 @@
|
||||
[disabled]="disablePrimaryButton(type, item)"
|
||||
(mouseenter)="onButtonHover($event)">
|
||||
@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>
|
||||
} @else if (type === DataType.SavedView) {
|
||||
<i-bs width="1em" height="1em" name="eye"></i-bs>
|
||||
@ -72,7 +72,7 @@
|
||||
<i-bs width="1em" height="1em" name="download"></i-bs>
|
||||
<span> <ng-container i18n>Download</ng-container></span>
|
||||
} @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>
|
||||
}
|
||||
</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 {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { GlobalSearchComponent } from './global-search.component'
|
||||
import { of } from 'rxjs'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
NgbDropdownModule,
|
||||
@ -14,11 +15,9 @@ import {
|
||||
NgbModalModule,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of } from 'rxjs'
|
||||
import { DataType } from 'src/app/data/datatype'
|
||||
import {
|
||||
FILTER_FULLTEXT_QUERY,
|
||||
FILTER_HAS_CORRESPONDENT_ANY,
|
||||
@ -27,20 +26,21 @@ import {
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_TITLE_CONTENT,
|
||||
} 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 { 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 = {
|
||||
total: 11,
|
||||
@ -138,13 +138,13 @@ describe('GlobalSearchComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [GlobalSearchComponent],
|
||||
imports: [
|
||||
NgbModalModule,
|
||||
NgbDropdownModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
GlobalSearchComponent,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
|
@ -1,14 +1,23 @@
|
||||
import { NgTemplateOutlet } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import { NgbDropdown, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
|
||||
import {
|
||||
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 {
|
||||
FILTER_FULLTEXT_QUERY,
|
||||
FILTER_HAS_CORRESPONDENT_ANY,
|
||||
@ -17,19 +26,23 @@ import {
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_TITLE_CONTENT,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { DataType } from 'src/app/data/datatype'
|
||||
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 { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import {
|
||||
GlobalSearchResult,
|
||||
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 { paramsFromViewState } 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 { 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 { 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 { 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({
|
||||
selector: 'pngx-global-search',
|
||||
templateUrl: './global-search.component.html',
|
||||
styleUrl: './global-search.component.scss',
|
||||
imports: [
|
||||
CustomDatePipe,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
NgbDropdownModule,
|
||||
NgTemplateOutlet,
|
||||
],
|
||||
})
|
||||
export class GlobalSearchComponent implements OnInit {
|
||||
public DataType = DataType
|
||||
@ -89,7 +106,6 @@ export class GlobalSearchComponent implements OnInit {
|
||||
this.queryDebounce
|
||||
.pipe(
|
||||
debounceTime(400),
|
||||
map((query) => query?.trim()),
|
||||
filter((query) => !query?.length || query?.length > 2),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
@ -109,7 +125,7 @@ export class GlobalSearchComponent implements OnInit {
|
||||
|
||||
private search(query: string) {
|
||||
this.loading = true
|
||||
this.searchService.globalSearch(query).subscribe((results) => {
|
||||
this.searchService.globalSearch(query.trim()).subscribe((results) => {
|
||||
this.searchResults = results
|
||||
this.loading = false
|
||||
this.resultsDropdown.open()
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||
|
||||
describe('ClearableBadgeComponent', () => {
|
||||
let component: ClearableBadgeComponent
|
||||
@ -8,8 +8,10 @@ describe('ClearableBadgeComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ClearableBadgeComponent],
|
||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ClearableBadgeComponent,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
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({
|
||||
selector: 'pngx-clearable-badge',
|
||||
templateUrl: './clearable-badge.component.html',
|
||||
styleUrls: ['./clearable-badge.component.scss'],
|
||||
imports: [NgxBootstrapIconsModule],
|
||||
})
|
||||
export class ClearableBadgeComponent {
|
||||
constructor() {}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { ConfirmButtonComponent } from './confirm-button.component'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmButtonComponent } from './confirm-button.component'
|
||||
|
||||
describe('ConfirmButtonComponent', () => {
|
||||
let component: ConfirmButtonComponent
|
||||
@ -10,8 +10,11 @@ describe('ConfirmButtonComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ConfirmButtonComponent],
|
||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule.pick(allIcons)],
|
||||
imports: [
|
||||
NgbPopoverModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ConfirmButtonComponent,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmButtonComponent)
|
||||
|
@ -5,12 +5,14 @@ import {
|
||||
Output,
|
||||
ViewChild,
|
||||
} 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({
|
||||
selector: 'pngx-confirm-button',
|
||||
templateUrl: './confirm-button.component.html',
|
||||
styleUrl: './confirm-button.component.scss',
|
||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class ConfirmButtonComponent {
|
||||
@Input()
|
||||
|
@ -1,14 +1,8 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { Subject } from 'rxjs'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
||||
|
||||
describe('ConfirmDialogComponent', () => {
|
||||
let component: ConfirmDialogComponent
|
||||
@ -17,9 +11,8 @@ describe('ConfirmDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ConfirmDialogComponent, SafeHtmlPipe],
|
||||
providers: [NgbActiveModal, SafeHtmlPipe],
|
||||
imports: [],
|
||||
imports: [ConfirmDialogComponent, SafeHtmlPipe],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { DecimalPipe } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
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({
|
||||
selector: 'pngx-confirm-dialog',
|
||||
templateUrl: './confirm-dialog.component.html',
|
||||
styleUrls: ['./confirm-dialog.component.scss'],
|
||||
imports: [DecimalPipe, SafeHtmlPipe],
|
||||
})
|
||||
export class ConfirmDialogComponent {
|
||||
constructor(public activeModal: NgbActiveModal) {}
|
||||
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
||||
constructor(public activeModal: NgbActiveModal) {
|
||||
super()
|
||||
}
|
||||
|
||||
@Output()
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
@ -1,6 +1,6 @@
|
||||
.pdf-viewer-container {
|
||||
background-color: gray;
|
||||
height: 350px;
|
||||
height: 550px;
|
||||
|
||||
pdf-viewer {
|
||||
width: 100%;
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
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 { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PdfViewerComponent } from 'ng2-pdf-viewer'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
|
||||
|
||||
describe('DeletePagesConfirmDialogComponent', () => {
|
||||
let component: DeletePagesConfirmDialogComponent
|
||||
@ -14,11 +13,12 @@ describe('DeletePagesConfirmDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent],
|
||||
declarations: [],
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
DeletePagesConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
|
@ -1,13 +1,20 @@
|
||||
import { Component, TemplateRef, ViewChild } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
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 { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { PDFDocumentProxy, PdfViewerComponent } from 'ng2-pdf-viewer'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-delete-pages-confirm-dialog',
|
||||
templateUrl: './delete-pages-confirm-dialog.component.html',
|
||||
styleUrl: './delete-pages-confirm-dialog.component.scss',
|
||||
imports: [PdfViewerModule, FormsModule, ReactiveFormsModule, SafeHtmlPipe],
|
||||
})
|
||||
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
public documentID: number
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
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 { of } from 'rxjs'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
|
||||
|
||||
describe('MergeConfirmDialogComponent', () => {
|
||||
let component: MergeConfirmDialogComponent
|
||||
@ -15,11 +15,11 @@ describe('MergeConfirmDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [MergeConfirmDialogComponent],
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MergeConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { takeUntil } from 'rxjs'
|
||||
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({
|
||||
selector: 'pngx-merge-confirm-dialog',
|
||||
templateUrl: './merge-confirm-dialog.component.html',
|
||||
styleUrl: './merge-confirm-dialog.component.scss',
|
||||
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class MergeConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
@ -25,8 +28,6 @@ export class MergeConfirmDialogComponent
|
||||
|
||||
public metadataDocumentID: number = -1
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
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 { 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', () => {
|
||||
let component: RotateConfirmDialogComponent
|
||||
@ -12,8 +12,11 @@ describe('RotateConfirmDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RotateConfirmDialogComponent, SafeHtmlPipe],
|
||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
RotateConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
SafeHtmlPipe,
|
||||
|
@ -1,12 +1,16 @@
|
||||
import { NgStyle } from '@angular/common'
|
||||
import { Component } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
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 { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-rotate-confirm-dialog',
|
||||
templateUrl: './rotate-confirm-dialog.component.html',
|
||||
styleUrl: './rotate-confirm-dialog.component.scss',
|
||||
imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe],
|
||||
})
|
||||
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
public documentID: number
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="modal-body">
|
||||
<p>{{message}}</p>
|
||||
<div class="row mb-2">
|
||||
<div class="col-8">
|
||||
<div class="col-7">
|
||||
<div class="input-group input-group-sm">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
|
||||
@ -21,7 +21,7 @@
|
||||
</pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col-5">
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
|
||||
<i-bs name="plus-circle"></i-bs>
|
||||
@ -44,12 +44,12 @@
|
||||
</ul>
|
||||
</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">
|
||||
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||
</button>
|
||||
|
@ -1,6 +1,6 @@
|
||||
.pdf-viewer-container {
|
||||
background-color: gray;
|
||||
height: 350px;
|
||||
height: 500px;
|
||||
|
||||
pdf-viewer {
|
||||
width: 100%;
|
||||
|
@ -1,14 +1,14 @@
|
||||
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 { 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', () => {
|
||||
let component: SplitConfirmDialogComponent
|
||||
@ -17,12 +17,12 @@ describe('SplitConfirmDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SplitConfirmDialogComponent],
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
PdfViewerModule,
|
||||
SplitConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
|
@ -1,15 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
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 { PDFDocumentProxy } from 'ng2-pdf-viewer'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-split-confirm-dialog',
|
||||
templateUrl: './split-confirm-dialog.component.html',
|
||||
styleUrl: './split-confirm-dialog.component.scss',
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
PdfViewerModule,
|
||||
],
|
||||
})
|
||||
export class SplitConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
|
@ -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 { of } from 'rxjs'
|
||||
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 { 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[] = [
|
||||
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
|
||||
@ -17,7 +17,11 @@ const customFields: CustomField[] = [
|
||||
name: 'Field 4',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
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 () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CustomFieldDisplayComponent],
|
||||
imports: [],
|
||||
imports: [CustomFieldDisplayComponent],
|
||||
providers: [
|
||||
DocumentService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
@ -131,6 +134,8 @@ describe('CustomFieldDisplayComponent', () => {
|
||||
})
|
||||
|
||||
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,24 @@
|
||||
import { getLocaleCurrencyCode } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
LOCALE_ID,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { DisplayField, Document } from 'src/app/data/document'
|
||||
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 { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-custom-field-display',
|
||||
templateUrl: './custom-field-display.component.html',
|
||||
styleUrl: './custom-field-display.component.scss',
|
||||
imports: [CustomDatePipe, CurrencyPipe],
|
||||
})
|
||||
export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
export class CustomFieldDisplayComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
private _document: Document
|
||||
@ -61,7 +60,6 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
|
||||
private docLinkDocuments: Document[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
private defaultCurrencyCode: any
|
||||
|
||||
constructor(
|
||||
@ -69,6 +67,7 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
private documentService: DocumentService,
|
||||
@Inject(LOCALE_ID) currentLocale: string
|
||||
) {
|
||||
super()
|
||||
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
|
||||
this.customFieldService.listAll().subscribe((r) => {
|
||||
this.customFields = r.results
|
||||
@ -117,12 +116,7 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
||||
}
|
||||
|
||||
public getSelectValue(field: CustomField, index: number): string {
|
||||
return field.extra_data.select_options[index]
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
public getSelectValue(field: CustomField, id: string): string {
|
||||
return field.extra_data.select_options?.find((o) => o.id === id)?.label
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,29 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} 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 { By } from '@angular/platform-browser'
|
||||
import {
|
||||
NgbDropdownModule,
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
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[] = [
|
||||
{
|
||||
@ -43,10 +44,10 @@ describe('CustomFieldsDropdownComponent', () => {
|
||||
let customFieldService: CustomFieldsService
|
||||
let toastService: ToastService
|
||||
let modalService: NgbModal
|
||||
let settingsService: SettingsService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CustomFieldsDropdownComponent, SelectComponent],
|
||||
imports: [
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
@ -54,6 +55,8 @@ describe('CustomFieldsDropdownComponent', () => {
|
||||
NgbModalModule,
|
||||
NgbDropdownModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
CustomFieldsDropdownComponent,
|
||||
SelectComponent,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
@ -70,6 +73,8 @@ describe('CustomFieldsDropdownComponent', () => {
|
||||
results: fields.concat([]),
|
||||
})
|
||||
)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 1, username: 'test' }
|
||||
fixture = TestBed.createComponent(CustomFieldsDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
|
@ -3,31 +3,39 @@ import {
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
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 { 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 {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
PermissionsService,
|
||||
} 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({
|
||||
selector: 'pngx-custom-fields-dropdown',
|
||||
templateUrl: './custom-fields-dropdown.component.html',
|
||||
styleUrls: ['./custom-fields-dropdown.component.scss'],
|
||||
imports: [
|
||||
NgbDropdownModule,
|
||||
NgxBootstrapIconsModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class CustomFieldsDropdownComponent implements OnDestroy {
|
||||
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
|
||||
@Input()
|
||||
documentId: number
|
||||
|
||||
@ -60,8 +68,6 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
|
||||
|
||||
public filterText: string
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
get canCreateFields(): boolean {
|
||||
return this.permissionsService.currentUserCan(
|
||||
PermissionAction.Add,
|
||||
@ -75,14 +81,10 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
this.getFields()
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(this)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
private getFields() {
|
||||
this.customFieldsService
|
||||
.listAll()
|
||||
|
@ -44,6 +44,8 @@
|
||||
<ng-select #fieldSelects
|
||||
class="paperless-input-select rounded-end"
|
||||
[items]="getSelectOptionsForField(atom.field)"
|
||||
bindLabel="label"
|
||||
bindValue="id"
|
||||
[(ngModel)]="atom.value"
|
||||
[disabled]="disabled"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
@ -65,7 +67,9 @@
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
></ng-select>
|
||||
<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>
|
||||
@switch (atom.operator) {
|
||||
@case (CustomFieldQueryOperator.Exists) {
|
||||
@ -99,6 +103,8 @@
|
||||
<ng-select
|
||||
class="paperless-input-select rounded-end"
|
||||
[items]="getSelectOptionsForField(atom.field)"
|
||||
bindLabel="label"
|
||||
bindValue="id"
|
||||
[(ngModel)]="atom.value"
|
||||
[disabled]="disabled"
|
||||
[multiple]="true"
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
CustomFieldQueriesModel,
|
||||
CustomFieldsQueryDropdownComponent,
|
||||
} from './custom-fields-query-dropdown.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { of } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import {
|
||||
@ -16,17 +17,16 @@ import {
|
||||
CustomFieldQueryLogicalOperator,
|
||||
CustomFieldQueryOperatorGroups,
|
||||
} from 'src/app/data/custom-field-query'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import {
|
||||
CustomFieldQueryExpression,
|
||||
CustomFieldQueryAtom,
|
||||
CustomFieldQueryElement,
|
||||
CustomFieldQueryExpression,
|
||||
} from 'src/app/utils/custom-field-query-element'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
CustomFieldQueriesModel,
|
||||
CustomFieldsQueryDropdownComponent,
|
||||
} from './custom-fields-query-dropdown.component'
|
||||
|
||||
const customFields = [
|
||||
{
|
||||
@ -39,7 +39,12 @@ const customFields = [
|
||||
id: 2,
|
||||
name: 'Test Select Field',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
||||
extra_data: {
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@ -50,13 +55,13 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CustomFieldsQueryDropdownComponent],
|
||||
imports: [
|
||||
NgbDropdownModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CustomFieldsQueryDropdownComponent,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
@ -128,11 +133,19 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
||||
id: 1,
|
||||
name: 'Test Field',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
||||
extra_data: {
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
],
|
||||
},
|
||||
}
|
||||
component.customFields = [field]
|
||||
const options = component.getSelectOptionsForField(1)
|
||||
expect(options).toEqual(['Option 1', 'Option 2'])
|
||||
expect(options).toEqual([
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
])
|
||||
|
||||
// Fallback to empty array if field is not found
|
||||
const options2 = component.getSelectOptionsForField(2)
|
||||
|
@ -1,34 +1,38 @@
|
||||
import { NgTemplateOutlet } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, Subject, takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import {
|
||||
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
||||
CUSTOM_FIELD_QUERY_MAX_DEPTH,
|
||||
CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
|
||||
CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
|
||||
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
|
||||
CustomFieldQueryElementType,
|
||||
CustomFieldQueryOperator,
|
||||
CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
|
||||
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
|
||||
CustomFieldQueryOperatorGroups,
|
||||
CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
|
||||
CUSTOM_FIELD_QUERY_MAX_DEPTH,
|
||||
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
||||
} from 'src/app/data/custom-field-query'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import {
|
||||
CustomFieldQueryAtom,
|
||||
CustomFieldQueryElement,
|
||||
CustomFieldQueryExpression,
|
||||
CustomFieldQueryAtom,
|
||||
} from 'src/app/utils/custom-field-query-element'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
|
||||
export class CustomFieldQueriesModel {
|
||||
public queries: CustomFieldQueryElement[] = []
|
||||
@ -156,8 +160,17 @@ export class CustomFieldQueriesModel {
|
||||
selector: 'pngx-custom-fields-query-dropdown',
|
||||
templateUrl: './custom-fields-query-dropdown.component.html',
|
||||
styleUrls: ['./custom-fields-query-dropdown.component.scss'],
|
||||
imports: [
|
||||
ClearableBadgeComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgTemplateOutlet,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule,
|
||||
NgbDropdownModule,
|
||||
],
|
||||
})
|
||||
export class CustomFieldsQueryDropdownComponent implements OnDestroy {
|
||||
export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
|
||||
public CustomFieldQueryComponentType = CustomFieldQueryElementType
|
||||
public CustomFieldQueryOperator = CustomFieldQueryOperator
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
@ -223,19 +236,13 @@ export class CustomFieldsQueryDropdownComponent implements OnDestroy {
|
||||
|
||||
customFields: CustomField[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(protected customFieldsService: CustomFieldsService) {
|
||||
super()
|
||||
this.selectionModel = new CustomFieldQueriesModel()
|
||||
this.getFields()
|
||||
this.reset()
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(this)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
public onOpenChange(open: boolean) {
|
||||
if (open) {
|
||||
if (this.selectionModel.queries.length === 0) {
|
||||
@ -311,7 +318,9 @@ export class CustomFieldsQueryDropdownComponent implements OnDestroy {
|
||||
}))
|
||||
}
|
||||
|
||||
getSelectOptionsForField(fieldID: number): string[] {
|
||||
getSelectOptionsForField(
|
||||
fieldID: number
|
||||
): Array<{ label: string; id: string }> {
|
||||
const field = this.customFields.find((field) => field.id === fieldID)
|
||||
if (field) {
|
||||
return field.extra_data['select_options']
|
||||
|
@ -1,24 +1,24 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
let fixture: ComponentFixture<DatesDropdownComponent>
|
||||
import {
|
||||
DatesDropdownComponent,
|
||||
DateSelection,
|
||||
RelativeDate,
|
||||
} from './dates-dropdown.component'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
DateSelection,
|
||||
DatesDropdownComponent,
|
||||
RelativeDate,
|
||||
} from './dates-dropdown.component'
|
||||
let fixture: ComponentFixture<DatesDropdownComponent>
|
||||
|
||||
describe('DatesDropdownComponent', () => {
|
||||
let component: DatesDropdownComponent
|
||||
@ -27,16 +27,14 @@ describe('DatesDropdownComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DatesDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
CustomDatePipe,
|
||||
],
|
||||
imports: [
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
DatesDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
CustomDatePipe,
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
|
@ -1,17 +1,26 @@
|
||||
import { NgClass } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
NgbDateAdapter,
|
||||
NgbDatepickerModule,
|
||||
NgbDropdownModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
|
||||
export interface DateSelection {
|
||||
createdBefore?: string
|
||||
@ -34,6 +43,16 @@ export enum RelativeDate {
|
||||
templateUrl: './dates-dropdown.component.html',
|
||||
styleUrls: ['./dates-dropdown.component.scss'],
|
||||
providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }],
|
||||
imports: [
|
||||
ClearableBadgeComponent,
|
||||
CustomDatePipe,
|
||||
NgxBootstrapIconsModule,
|
||||
NgbDatepickerModule,
|
||||
NgbDropdownModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgClass,
|
||||
],
|
||||
})
|
||||
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
public popperOptions = popperOptionsReenablePreventOverflow
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
@ -11,7 +12,6 @@ import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
|
||||
describe('CorrespondentEditDialogComponent', () => {
|
||||
let component: CorrespondentEditDialogComponent
|
||||
@ -20,7 +20,11 @@ describe('CorrespondentEditDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
CorrespondentEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
@ -28,7 +32,6 @@ describe('CorrespondentEditDialogComponent', () => {
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
|
@ -1,17 +1,34 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-correspondent-edit-dialog',
|
||||
templateUrl: './correspondent-edit-dialog.component.html',
|
||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||
imports: [
|
||||
SelectComponent,
|
||||
PermissionsFormComponent,
|
||||
TextComponent,
|
||||
IfOwnerDirective,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<Correspondent> {
|
||||
constructor(
|
||||
|
@ -21,12 +21,16 @@
|
||||
</button>
|
||||
<div formArrayName="select_options">
|
||||
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
|
||||
<div class="input-group input-group-sm my-2">
|
||||
<input #selectOption type="text" class="form-control" [formControl]="option" autocomplete="off">
|
||||
<div class="input-group input-group-sm my-2" [formGroup]="objectForm.controls.extra_data.controls.select_options.controls[i]">
|
||||
<input #selectOption type="text" class="form-control" formControlName="label" autocomplete="off">
|
||||
<input type="hidden" formControlName="id">
|
||||
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (object?.id) {
|
||||
<small class="d-block mt-2" i18n>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</small>
|
||||
}
|
||||
}
|
||||
@case (CustomFieldDataType.Monetary) {
|
||||
<div class="my-3">
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { CustomFieldEditDialogComponent } from './custom-field-edit-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ElementRef, QueryList } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
@ -12,10 +15,7 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { ElementRef, QueryList } from '@angular/core'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomFieldEditDialogComponent } from './custom-field-edit-dialog.component'
|
||||
|
||||
describe('CustomFieldEditDialogComponent', () => {
|
||||
let component: CustomFieldEditDialogComponent
|
||||
@ -24,20 +24,18 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
CustomFieldEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
CustomFieldEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
@ -80,7 +78,11 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
name: 'Field 1',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: {
|
||||
select_options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: '123-xyz' },
|
||||
{ label: 'Option 2', id: '456-abc' },
|
||||
{ label: 'Option 3', id: '789-123' },
|
||||
],
|
||||
},
|
||||
}
|
||||
fixture.detectChanges()
|
||||
@ -94,6 +96,10 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
fixture.detectChanges()
|
||||
component.ngOnInit()
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(0)
|
||||
component.addSelectOption()
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(1)
|
||||
@ -101,14 +107,10 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(2)
|
||||
component.addSelectOption()
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(3)
|
||||
component.removeSelectOption(0)
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(2)
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('should focus on last select option input', () => {
|
||||
|
@ -2,40 +2,53 @@ import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { FormGroup, FormControl, FormArray } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
DATA_TYPE_LABELS,
|
||||
FormArray,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import {
|
||||
CustomField,
|
||||
CustomFieldDataType,
|
||||
DATA_TYPE_LABELS,
|
||||
} from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-custom-field-edit-dialog',
|
||||
templateUrl: './custom-field-edit-dialog.component.html',
|
||||
styleUrls: ['./custom-field-edit-dialog.component.scss'],
|
||||
imports: [
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class CustomFieldEditDialogComponent
|
||||
extends EditDialogComponent<CustomField>
|
||||
implements OnInit, AfterViewInit, OnDestroy
|
||||
implements OnInit, AfterViewInit
|
||||
{
|
||||
CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
@ViewChildren('selectOption')
|
||||
private selectOptionInputs: QueryList<ElementRef>
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
private get selectOptions(): FormArray {
|
||||
return (this.objectForm.controls.extra_data as FormGroup).controls
|
||||
.select_options as FormArray
|
||||
@ -57,9 +70,16 @@ export class CustomFieldEditDialogComponent
|
||||
}
|
||||
if (this.object?.data_type === CustomFieldDataType.Select) {
|
||||
this.selectOptions.clear()
|
||||
this.object.extra_data.select_options.forEach((option) =>
|
||||
this.selectOptions.push(new FormControl(option))
|
||||
)
|
||||
this.object.extra_data.select_options
|
||||
.filter((option) => option)
|
||||
.forEach((option) =>
|
||||
this.selectOptions.push(
|
||||
new FormGroup({
|
||||
label: new FormControl(option.label),
|
||||
id: new FormControl(option.id),
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,11 +91,6 @@ export class CustomFieldEditDialogComponent
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new custom field`
|
||||
}
|
||||
@ -89,7 +104,7 @@ export class CustomFieldEditDialogComponent
|
||||
name: new FormControl(null),
|
||||
data_type: new FormControl(null),
|
||||
extra_data: new FormGroup({
|
||||
select_options: new FormArray([new FormControl(null)]),
|
||||
select_options: new FormArray([]),
|
||||
default_currency: new FormControl(null),
|
||||
}),
|
||||
})
|
||||
@ -104,7 +119,9 @@ export class CustomFieldEditDialogComponent
|
||||
}
|
||||
|
||||
public addSelectOption() {
|
||||
this.selectOptions.push(new FormControl(''))
|
||||
this.selectOptions.push(
|
||||
new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
|
||||
)
|
||||
}
|
||||
|
||||
public removeSelectOption(index: number) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
@ -11,7 +12,6 @@ import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
|
||||
describe('DocumentTypeEditDialogComponent', () => {
|
||||
let component: DocumentTypeEditDialogComponent
|
||||
@ -20,7 +20,11 @@ describe('DocumentTypeEditDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
DocumentTypeEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
@ -28,7 +32,6 @@ describe('DocumentTypeEditDialogComponent', () => {
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
|
@ -1,17 +1,34 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-document-type-edit-dialog',
|
||||
templateUrl: './document-type-edit-dialog.component.html',
|
||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||
imports: [
|
||||
SelectComponent,
|
||||
PermissionsFormComponent,
|
||||
TextComponent,
|
||||
IfOwnerDirective,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<DocumentType> {
|
||||
constructor(
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
@ -10,8 +11,8 @@ import {
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormGroup,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
@ -19,9 +20,9 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { of } from 'rxjs'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
MATCH_AUTO,
|
||||
MATCH_NONE,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
@ -30,7 +31,6 @@ import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@ -38,6 +38,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
<h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
|
||||
</div>
|
||||
`,
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
})
|
||||
class TestComponent extends EditDialogComponent<Tag> {
|
||||
constructor(
|
||||
@ -96,8 +97,7 @@ describe('EditDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, TestComponent],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
|
@ -9,12 +9,13 @@ import {
|
||||
} from 'src/app/data/matching-model'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { User } from 'src/app/data/user'
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
export enum EditDialogMode {
|
||||
CREATE = 0,
|
||||
@ -23,15 +24,19 @@ export enum EditDialogMode {
|
||||
|
||||
@Directive()
|
||||
export abstract class EditDialogComponent<
|
||||
T extends ObjectWithPermissions | ObjectWithId,
|
||||
> implements OnInit
|
||||
T extends ObjectWithPermissions | ObjectWithId,
|
||||
>
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
constructor(
|
||||
protected service: AbstractPaperlessService<T>,
|
||||
private activeModal: NgbActiveModal,
|
||||
private userService: UserService,
|
||||
private settingsService: SettingsService
|
||||
) {}
|
||||
protected settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
users: User[]
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
@ -12,8 +14,6 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { GroupEditDialogComponent } from './group-edit-dialog.component'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('GroupEditDialogComponent', () => {
|
||||
let component: GroupEditDialogComponent
|
||||
@ -22,7 +22,12 @@ describe('GroupEditDialogComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
GroupEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
@ -31,13 +36,6 @@ describe('GroupEditDialogComponent', () => {
|
||||
PermissionsFormComponent,
|
||||
PermissionsSelectComponent,
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user