mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-18 17:34:39 -05:00
Compare commits
109 Commits
3fa4fad811
...
87e5d82c46
Author | SHA1 | Date | |
---|---|---|---|
![]() |
87e5d82c46 | ||
![]() |
476844f32a | ||
![]() |
01285c96d4 | ||
![]() |
3e6ba34c5e | ||
![]() |
d9cbd3652a | ||
![]() |
90bd878cf2 | ||
![]() |
62e04ab2fe | ||
![]() |
dbdc67da7a | ||
![]() |
11a4e0d5ba | ||
![]() |
c4b431f5a6 | ||
![]() |
d31f4669a2 | ||
![]() |
483f1e9438 | ||
![]() |
d7a358d39d | ||
![]() |
b94a60d607 | ||
![]() |
e6d8cd6547 | ||
![]() |
e2fc7f596d | ||
![]() |
20e7f01cec | ||
![]() |
96daa5eb18 | ||
![]() |
84e17535fc | ||
![]() |
77db0c399c | ||
![]() |
e51c7a27bb | ||
![]() |
a3455c8373 | ||
![]() |
cce9dfd5b8 | ||
![]() |
3a9257f10a | ||
![]() |
3b921da6c3 | ||
![]() |
ad8519482c | ||
![]() |
fe205b31c2 | ||
![]() |
13ab148c7e | ||
![]() |
559caf72c2 | ||
![]() |
2481a66544 | ||
![]() |
f6a3882199 | ||
![]() |
8d48d398eb | ||
![]() |
b3b9a8fb5b | ||
![]() |
4cdc629e3d | ||
![]() |
5195a97e4c | ||
![]() |
96fa522394 | ||
![]() |
dd1da9f072 | ||
![]() |
d99f2d6160 | ||
![]() |
ebd46f08e5 | ||
![]() |
6f0c6f39b1 | ||
![]() |
0690fd36c5 | ||
![]() |
0052f21cea | ||
![]() |
c809a65571 | ||
![]() |
bb3336f7bc | ||
![]() |
a9ed46de11 | ||
![]() |
1ccaf66869 | ||
![]() |
e864a51497 | ||
![]() |
4a28be233e | ||
![]() |
9183bfc0a4 | ||
![]() |
5f26139a5f | ||
![]() |
ccfc7d98b1 | ||
![]() |
d1bd2af49c | ||
![]() |
e2eec6dc71 | ||
![]() |
42e3684211 | ||
![]() |
df8f07555f | ||
![]() |
3660336bcf | ||
![]() |
aeceaf60a2 | ||
![]() |
959ebdbb85 | ||
![]() |
eb1c49090b | ||
![]() |
9f8b8a9f20 | ||
![]() |
f5fc04cfe2 | ||
![]() |
3186550fd7 | ||
![]() |
74aaf18630 | ||
![]() |
e6a147079d | ||
![]() |
105b823fd9 | ||
![]() |
be20c48588 | ||
![]() |
377dcc39f5 | ||
![]() |
767118fa8a | ||
![]() |
339612f4ec | ||
![]() |
e7592c6269 | ||
![]() |
ffc0b936f3 | ||
![]() |
1a6540e8ed | ||
![]() |
abbf9060d0 | ||
![]() |
11a3dfe890 | ||
![]() |
faa5d3e5b9 | ||
![]() |
8d1a8c2c42 | ||
![]() |
01dc3cc17c | ||
![]() |
cfbd5af820 | ||
![]() |
e8090fd030 | ||
![]() |
05896d5b70 | ||
![]() |
65b8a74166 | ||
![]() |
56b1c7adeb | ||
![]() |
55cb9cedc7 | ||
![]() |
3180ccf4cb | ||
![]() |
43abb0541b | ||
![]() |
a3a405354f | ||
![]() |
09e98d600e | ||
![]() |
01a39b9bb4 | ||
![]() |
3b0b40f071 | ||
![]() |
6dce83865f | ||
![]() |
18252a19d7 | ||
![]() |
733a9674d6 | ||
![]() |
f3b6e15321 | ||
![]() |
6591d5da63 | ||
![]() |
c974dc9400 | ||
![]() |
1671d49d44 | ||
![]() |
6b248ef140 | ||
![]() |
735681d294 | ||
![]() |
a9085c65c5 | ||
![]() |
e312425b1c | ||
![]() |
13fe064f6e | ||
![]() |
958f98d7e5 | ||
![]() |
dfad3c4d8e | ||
![]() |
37267f3f04 | ||
![]() |
b34538d991 | ||
![]() |
fc97bd1315 | ||
![]() |
dbf3721ec2 | ||
![]() |
59afbe09b1 | ||
![]() |
bfeaa1b119 |
55
.github/DISCUSSION_TEMPLATE/support.yml
vendored
Normal file
55
.github/DISCUSSION_TEMPLATE/support.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
title: "[Support] "
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What's your question or issue?
|
||||
description: Provide a clear and concise description of what you're trying to do, and what's going wrong.
|
||||
placeholder: |
|
||||
I'm trying to...
|
||||
|
||||
[Include screenshots if helpful]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What have you tried?
|
||||
description: Describe any steps you've already taken to troubleshoot or solve the issue.
|
||||
placeholder: |
|
||||
- I checked the logs and saw...
|
||||
- I followed the install guide and tried...
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Paperless-ngx version
|
||||
placeholder: e.g. 1.14.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: host-os
|
||||
attributes:
|
||||
label: Host OS
|
||||
description: Include architecture if relevant.
|
||||
placeholder: e.g. Ubuntu 22.04 / Raspberry Pi arm64
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Installation method
|
||||
options:
|
||||
- Docker - official image
|
||||
- Docker - linuxserver.io image
|
||||
- Bare metal
|
||||
- Other (please describe above)
|
||||
- type: textarea
|
||||
id: system-status
|
||||
attributes:
|
||||
label: System status
|
||||
description: If available, copy & paste the system status output from Settings > System Status > Copy
|
||||
render: json
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs or output
|
||||
description: If you have logs, errors that might help, paste it here.
|
||||
render: bash
|
2
.github/workflows/translate-strings.yml
vendored
2
.github/workflows/translate-strings.yml
vendored
@ -61,7 +61,7 @@ jobs:
|
||||
cd src-ui
|
||||
pnpm run ng extract-i18n
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
uses: stefanzweifel/git-auto-commit-action@v6
|
||||
with:
|
||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||
commit_message: "Auto translate strings"
|
||||
|
@ -11,6 +11,7 @@ for command in decrypt_documents \
|
||||
mail_fetcher \
|
||||
document_create_classifier \
|
||||
document_index \
|
||||
document_llmindex \
|
||||
document_renamer \
|
||||
document_retagger \
|
||||
document_thumbnails \
|
||||
|
14
docker/rootfs/usr/local/bin/document_llmindex
Executable file
14
docker/rootfs/usr/local/bin/document_llmindex
Executable file
@ -0,0 +1,14 @@
|
||||
#!/command/with-contenv /usr/bin/bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "${PAPERLESS_SRC_DIR}"
|
||||
|
||||
if [[ $(id -u) == 0 ]]; then
|
||||
s6-setuidgid paperless python3 manage.py document_llmindex "$@"
|
||||
elif [[ $(id -un) == "paperless" ]]; then
|
||||
python3 manage.py document_llmindex "$@"
|
||||
else
|
||||
echo "Unknown user."
|
||||
fi
|
@ -457,6 +457,22 @@ of the index and usually makes queries faster and also ensures that the
|
||||
autocompletion works properly. This command is regularly invoked by the
|
||||
task scheduler.
|
||||
|
||||
### Clearing the database read cache
|
||||
|
||||
If the database read cache is enabled, **you must run this command** after making any changes to the database outside the application context.
|
||||
This includes operations such as restoring a database backup or executing SQL statements like UPDATE, INSERT, DELETE, ALTER, CREATE, or DROP.
|
||||
|
||||
Failing to invalidate the cache after such modifications can lead to stale data being served from the cache, and **may cause data corruption** or inconsistent behavior in the application.
|
||||
|
||||
Use the following management command to clear the cache:
|
||||
|
||||
```
|
||||
invalidate_cachalot
|
||||
```
|
||||
|
||||
!!! info
|
||||
The database read cache is based on Django-Cachalot. You can refer to their [documentation](https://django-cachalot.readthedocs.io/en/latest/quickstart.html#manage-py-command).
|
||||
|
||||
### Managing filenames {#renamer}
|
||||
|
||||
If you use paperless' feature to
|
||||
|
@ -6007,7 +6007,6 @@ primarily.
|
||||
a very good job at ocr'ing a document with the default
|
||||
language. Certain language specifics such as umlauts may not get
|
||||
picked up properly.
|
||||
- `PAPERLESS_DEBUG` defaults to `false`.
|
||||
- The presence of `PAPERLESS_DBHOST` now determines whether to use
|
||||
PostgreSQL or SQLite.
|
||||
- `PAPERLESS_OCR_THREADS` is gone and replaced with
|
||||
|
@ -159,6 +159,41 @@ Available options are `postgresql` and `mariadb`.
|
||||
|
||||
Defaults to unset, which uses Django’s built-in defaults.
|
||||
|
||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
||||
|
||||
: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.
|
||||
|
||||
Defaults to `false`.
|
||||
|
||||
!!! danger
|
||||
|
||||
**Do not modify the database outside the application while it is running.**
|
||||
This includes actions such as restoring a backup, upgrading the database, or performing manual inserts. All external modifications must be done **only when the application is stopped**.
|
||||
After making any such changes, you **must invalidate the DB read cache** using the `invalidate_cachalot` management command.
|
||||
|
||||
#### [`PAPERLESS_READ_CACHE_TTL=<int>`](#PAPERLESS_READ_CACHE_TTL) {#PAPERLESS_READ_CACHE_TTL}
|
||||
|
||||
: Specifies how long (in seconds) read data should be cached.
|
||||
|
||||
Allowed values are between `1` (one second) and `31536000` (one year). Defaults to `3600` (one hour).
|
||||
|
||||
!!! warning
|
||||
|
||||
A high TTL increases memory usage over time. Memory may be used until end of TTL, even if the cache is invalidated with the `invalidate_cachalot` command.
|
||||
|
||||
In case of an out-of-memory (OOM) situation, Redis may stop accepting new data — including cache entries, scheduled tasks, and documents to consume.
|
||||
If your system has limited RAM, consider configuring a dedicated Redis instance for the read cache, with a memory limit and the eviction policy set to `allkeys-lru`.
|
||||
For more details, refer to the [Redis eviction policy documentation](https://redis.io/docs/latest/develop/reference/eviction/), and see the `PAPERLESS_READ_CACHE_REDIS_URL` setting to specify a separate Redis broker.
|
||||
|
||||
#### [`PAPERLESS_READ_CACHE_REDIS_URL=<url>`](#PAPERLESS_READ_CACHE_REDIS_URL) {#PAPERLESS_READ_CACHE_REDIS_URL}
|
||||
|
||||
: Defines the Redis instance used for the read cache.
|
||||
|
||||
Defaults to `None`.
|
||||
|
||||
!!! Note
|
||||
If this value is not set, the same Redis instance used for scheduled tasks will be used for caching as well.
|
||||
|
||||
## Optional Services
|
||||
|
||||
### Tika {#tika}
|
||||
@ -968,6 +1003,22 @@ still perform some basic text pre-processing before matching.
|
||||
|
||||
Defaults to 1.
|
||||
|
||||
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
||||
|
||||
Specifies which language Paperless should use when parsing dates from documents.
|
||||
|
||||
This should be a language code supported by the dateparser library,
|
||||
for example: "en", or a combination such as "en+de".
|
||||
Locales are also supported (e.g., "en-AU").
|
||||
Multiple languages can be combined using "+", for example: "en+de" or "en-AU+de".
|
||||
For valid values, refer to the list of supported languages and locales in the [dateparser documentation](https://dateparser.readthedocs.io/en/latest/supported_locales.html).
|
||||
|
||||
Set this to match the languages in which most of your documents are written.
|
||||
If not set, Paperless will attempt to infer the language(s) from the OCR configuration (`PAPERLESS_OCR_LANGUAGE`).
|
||||
|
||||
!!! note
|
||||
This format differs from the `PAPERLESS_OCR_LANGUAGE` setting, which uses ISO 639-2 codes (3 letters, e.g., "eng+deu" for Tesseract OCR).
|
||||
|
||||
#### [`PAPERLESS_EMAIL_TASK_CRON=<cron expression>`](#PAPERLESS_EMAIL_TASK_CRON) {#PAPERLESS_EMAIL_TASK_CRON}
|
||||
|
||||
: Configures the scheduled email fetching frequency. The value
|
||||
@ -1708,3 +1759,67 @@ password. All of these options come from their similarly-named [Django settings]
|
||||
#### [`PAPERLESS_EMAIL_USE_SSL=<bool>`](#PAPERLESS_EMAIL_USE_SSL) {#PAPERLESS_EMAIL_USE_SSL}
|
||||
|
||||
: Defaults to false.
|
||||
|
||||
## AI {#ai}
|
||||
|
||||
#### [`PAPERLESS_ENABLE_AI=<bool>`](#PAPERLESS_ENABLE_AI) {#PAPERLESS_ENABLE_AI}
|
||||
|
||||
: Enables the AI features in Paperless. This includes the AI-based
|
||||
suggestions. This setting is required to be set to true in order to use the AI features.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
#### [`PAPERLESS_LLM_EMBEDDING_BACKEND=<str>`](#PAPERLESS_LLM_EMBEDDING_BACKEND) {#PAPERLESS_LLM_EMBEDDING_BACKEND}
|
||||
|
||||
: The embedding backend to use for RAG. This can be either "openai" or "huggingface".
|
||||
|
||||
Defaults to None.
|
||||
|
||||
#### [`PAPERLESS_LLM_EMBEDDING_MODEL=<str>`](#PAPERLESS_LLM_EMBEDDING_MODEL) {#PAPERLESS_LLM_EMBEDDING_MODEL}
|
||||
|
||||
: The model to use for the embedding backend for RAG. This can be set to any of the embedding models supported by the current embedding backend. If not supplied, defaults to "text-embedding-3-small" for OpenAI and "sentence-transformers/all-MiniLM-L6-v2" for Huggingface.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
#### [`PAPERLESS_AI_BACKEND=<str>`](#PAPERLESS_AI_BACKEND) {#PAPERLESS_AI_BACKEND}
|
||||
|
||||
: The AI backend to use. This can be either "openai" or "ollama". If set to "ollama", the AI
|
||||
features will be run locally on your machine. If set to "openai", the AI features will be run
|
||||
using the OpenAI API. This setting is required to be set to use the AI features.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
!!! note
|
||||
|
||||
The OpenAI API is a paid service. You will need to set up an OpenAI account and
|
||||
will be charged for usage incurred by Paperless-ngx features and your document data
|
||||
will (of course) be sent to the OpenAI API. Paperless-ngx does not endorse the use of the
|
||||
OpenAI API in any way.
|
||||
|
||||
Refer to the OpenAI terms of service, and use at your own risk.
|
||||
|
||||
#### [`PAPERLESS_LLM_MODEL=<str>`](#PAPERLESS_LLM_MODEL) {#PAPERLESS_LLM_MODEL}
|
||||
|
||||
: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported by the
|
||||
current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3" for Ollama.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
#### [`PAPERLESS_LLM_API_KEY=<str>`](#PAPERLESS_LLM_API_KEY) {#PAPERLESS_LLM_API_KEY}
|
||||
|
||||
: The API key to use for the AI backend. This is required for the OpenAI backend only.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
#### [`PAPERLESS_LLM_URL=<str>`](#PAPERLESS_LLM_URL) {#PAPERLESS_LLM_URL}
|
||||
|
||||
: The URL to use for the AI backend. This is required for the Ollama backend only.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
#### [`PAPERLESS_LLM_INDEX_TASK_CRON=<cron expression>`](#PAPERLESS_LLM_INDEX_TASK_CRON) {#PAPERLESS_LLM_INDEX_TASK_CRON}
|
||||
|
||||
: Configures the schedule to update the AI embeddings for all documents. Only performed if
|
||||
AI is enabled and the LLM embedding backend is set.
|
||||
|
||||
Defaults to `10 2 * * *`, once per day.
|
||||
|
@ -25,11 +25,12 @@ physical documents into a searchable online archive so you can keep, well, _less
|
||||
## Features
|
||||
|
||||
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
|
||||
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way.
|
||||
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way, unless you explicitly choose to do so.
|
||||
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
|
||||
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
|
||||
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
|
||||
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
|
||||
- **New**: Paperless-ngx can now leverage AI (Large Language Models or LLMs) for document suggestions. This is an optional feature that can be enabled (and is disabled by default).
|
||||
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents)[^1] and more.
|
||||
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents.
|
||||
- **Beautiful, modern web application** that features:
|
||||
|
@ -261,6 +261,22 @@ Once setup, navigating to the email settings page in Paperless-ngx will allow yo
|
||||
You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads)
|
||||
for details.
|
||||
|
||||
## Document Suggestions
|
||||
|
||||
Paperless-ngx can suggest tags, correspondents, document types and storage paths for documents based on the content of the document. This is done using a machine learning model that is trained on the documents in your database. The suggestions are shown in the document detail page and can be accepted or rejected by the user.
|
||||
|
||||
## AI Features
|
||||
|
||||
Paperless-ngx includes several features that use AI to enhance the document management experience. These features are optional and can be enabled or disabled in the settings. If you are using the AI features, you may want to also enable the "LLM index" feature, which supports Retrieval-Augmented Generation (RAG) designed to improve the quality of AI responses. The LLM index feature is not enabled by default and requires additional configuration.
|
||||
|
||||
### Document Chat
|
||||
|
||||
Paperless-ngx can use an AI LLM model to answer questions about a document or across multiple documents. Again, this feature works best when RAG is enabled. The chat feature is available in the upper app toolbar and will switch between chatting across multiple documents or a single document based on the current view.
|
||||
|
||||
### AI-Enhanced Suggestions
|
||||
|
||||
If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags, correspondents and document types for documents. This feature will always be "opt-in" and does not disable the existing classifier-based suggestion system. Currently, both remote (via the OpenAI API) and local (via Ollama) models are supported, see [configuration](configuration.md#ai) for details.
|
||||
|
||||
## Sharing documents from Paperless-ngx
|
||||
|
||||
Paperless-ngx supports sharing documents with other users by assigning them [permissions](#object-permissions)
|
||||
|
@ -1,10 +1,6 @@
|
||||
# Have a look at the docs for documentation.
|
||||
# https://docs.paperless-ngx.com/configuration/
|
||||
|
||||
# Debug. Only enable this for development.
|
||||
|
||||
#PAPERLESS_DEBUG=false
|
||||
|
||||
# Required services
|
||||
|
||||
#PAPERLESS_REDIS=redis://localhost:6379
|
||||
|
@ -26,6 +26,7 @@ dependencies = [
|
||||
"django~=5.1.7",
|
||||
"django-allauth[socialaccount,mfa]~=65.4.0",
|
||||
"django-auditlog~=3.1.2",
|
||||
"django-cachalot~=2.8.0",
|
||||
"django-celery-results~=2.6.0",
|
||||
"django-compression-middleware~=0.5.0",
|
||||
"django-cors-headers~=4.7.0",
|
||||
@ -39,6 +40,7 @@ dependencies = [
|
||||
"drf-spectacular~=0.28",
|
||||
"drf-spectacular-sidecar~=2025.4.1",
|
||||
"drf-writable-nested~=0.7.1",
|
||||
"faiss-cpu>=1.10",
|
||||
"filelock~=3.18.0",
|
||||
"flower~=2.0.1",
|
||||
"gotenberg-client~=0.10.0",
|
||||
@ -47,8 +49,15 @@ dependencies = [
|
||||
"inotifyrecursive~=0.3",
|
||||
"jinja2~=3.1.5",
|
||||
"langdetect~=1.0.9",
|
||||
"llama-index-core>=0.12.33.post1",
|
||||
"llama-index-embeddings-huggingface>=0.5.3",
|
||||
"llama-index-embeddings-openai>=0.3.1",
|
||||
"llama-index-llms-ollama>=0.5.4",
|
||||
"llama-index-llms-openai>=0.3.38",
|
||||
"llama-index-vector-stores-faiss>=0.3",
|
||||
"nltk~=3.9.1",
|
||||
"ocrmypdf~=16.10.0",
|
||||
"openai>=1.76",
|
||||
"pathvalidate~=3.2.3",
|
||||
"pdf2image~=1.17.0",
|
||||
"python-dateutil~=2.9.0",
|
||||
@ -60,6 +69,7 @@ dependencies = [
|
||||
"rapidfuzz~=3.13.0",
|
||||
"redis[hiredis]~=5.2.1",
|
||||
"scikit-learn~=1.6.1",
|
||||
"sentence-transformers>=4.1",
|
||||
"setproctitle~=1.3.4",
|
||||
"tika-client~=0.9.0",
|
||||
"tqdm~=4.67.1",
|
||||
@ -221,9 +231,6 @@ lint.per-file-ignores."src/documents/parsers.py" = [
|
||||
lint.per-file-ignores."src/documents/signals/handlers.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless/settings.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
||||
"RUF001",
|
||||
]
|
||||
@ -240,6 +247,7 @@ testpaths = [
|
||||
"src/paperless_mail/tests/",
|
||||
"src/paperless_tesseract/tests/",
|
||||
"src/paperless_tika/tests",
|
||||
"src/paperless_ai/tests",
|
||||
]
|
||||
addopts = [
|
||||
"--pythonwarnings=all",
|
||||
|
@ -60,10 +60,12 @@
|
||||
"path": "./extra-webpack.config.ts"
|
||||
},
|
||||
"outputPath": "dist/paperless-ui",
|
||||
"main": "src/main.ts",
|
||||
"outputHashing": "none",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"localize": true,
|
||||
"assets": [
|
||||
@ -86,12 +88,15 @@
|
||||
"file-saver",
|
||||
"utif"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
"namedChunks": true,
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"."
|
||||
]
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@ -107,8 +112,6 @@
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
@ -188,6 +191,30 @@
|
||||
},
|
||||
"@angular-eslint/schematics:library": {
|
||||
"setParserOptionsProject": true
|
||||
},
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,28 +11,28 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.2.14",
|
||||
"@angular/common": "~19.2.14",
|
||||
"@angular/compiler": "~19.2.14",
|
||||
"@angular/core": "~19.2.14",
|
||||
"@angular/forms": "~19.2.14",
|
||||
"@angular/localize": "~19.2.14",
|
||||
"@angular/platform-browser": "~19.2.14",
|
||||
"@angular/platform-browser-dynamic": "~19.2.14",
|
||||
"@angular/router": "~19.2.14",
|
||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||
"@ng-select/ng-select": "^14.9.0",
|
||||
"@angular/cdk": "^20.0.4",
|
||||
"@angular/common": "~20.0.6",
|
||||
"@angular/compiler": "~20.0.6",
|
||||
"@angular/core": "~20.0.6",
|
||||
"@angular/forms": "~20.0.6",
|
||||
"@angular/localize": "~20.0.6",
|
||||
"@angular/platform-browser": "~20.0.6",
|
||||
"@angular/platform-browser-dynamic": "~20.0.6",
|
||||
"@angular/router": "~20.0.6",
|
||||
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
||||
"@ng-select/ng-select": "^15.1.3",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.6",
|
||||
"bootstrap": "^5.3.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^10.0.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"ngx-device-detector": "^9.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
"ngx-device-detector": "^10.0.2",
|
||||
"ngx-ui-tour-ng-bootstrap": "^17.0.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"utif": "^3.1.0",
|
||||
@ -40,26 +40,26 @@
|
||||
"zone.js": "^0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^19.0.1",
|
||||
"@angular-builders/jest": "^19.0.1",
|
||||
"@angular-devkit/build-angular": "^19.2.14",
|
||||
"@angular-devkit/core": "^19.2.14",
|
||||
"@angular-devkit/schematics": "^19.2.14",
|
||||
"@angular-eslint/builder": "19.7.0",
|
||||
"@angular-eslint/eslint-plugin": "19.7.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.7.0",
|
||||
"@angular-eslint/schematics": "19.7.0",
|
||||
"@angular-eslint/template-parser": "19.7.0",
|
||||
"@angular/cli": "~19.2.14",
|
||||
"@angular/compiler-cli": "~19.2.14",
|
||||
"@angular-builders/custom-webpack": "^20.0.0",
|
||||
"@angular-builders/jest": "^20.0.0",
|
||||
"@angular-devkit/core": "^20.0.4",
|
||||
"@angular-devkit/schematics": "^20.0.4",
|
||||
"@angular-eslint/builder": "20.1.1",
|
||||
"@angular-eslint/eslint-plugin": "20.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "20.1.1",
|
||||
"@angular-eslint/schematics": "20.1.1",
|
||||
"@angular-eslint/template-parser": "20.1.1",
|
||||
"@angular/build": "^20.0.4",
|
||||
"@angular/cli": "~20.0.4",
|
||||
"@angular/compiler-cli": "~20.0.6",
|
||||
"@codecov/webpack-plugin": "^1.9.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.29",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||
"@typescript-eslint/parser": "^8.33.0",
|
||||
"@typescript-eslint/utils": "^8.33.0",
|
||||
"eslint": "^9.28.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"@typescript-eslint/utils": "^8.35.1",
|
||||
"eslint": "^9.30.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
@ -67,7 +67,8 @@
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
@ -77,6 +78,5 @@
|
||||
"lmdb",
|
||||
"msgpackr-extract"
|
||||
]
|
||||
},
|
||||
"typings": "./src/typings.d.ts"
|
||||
}
|
||||
}
|
||||
|
4074
src-ui/pnpm-lock.yaml
generated
4074
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||
import { Router, RouterOutlet } from '@angular/router'
|
||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { first, Subscription } from 'rxjs'
|
||||
@ -29,22 +29,22 @@ import { WebsocketStatusService } from './services/websocket-status.service'
|
||||
],
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
private settings = inject(SettingsService)
|
||||
private websocketStatusService = inject(WebsocketStatusService)
|
||||
private toastService = inject(ToastService)
|
||||
private router = inject(Router)
|
||||
private tasksService = inject(TasksService)
|
||||
tourService = inject(TourService)
|
||||
private renderer = inject(Renderer2)
|
||||
private permissionsService = inject(PermissionsService)
|
||||
private hotKeyService = inject(HotKeyService)
|
||||
private componentRouterService = inject(ComponentRouterService)
|
||||
|
||||
newDocumentSubscription: Subscription
|
||||
successSubscription: Subscription
|
||||
failedSubscription: Subscription
|
||||
|
||||
constructor(
|
||||
private settings: SettingsService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
private toastService: ToastService,
|
||||
private router: Router,
|
||||
private tasksService: TasksService,
|
||||
public tourService: TourService,
|
||||
private renderer: Renderer2,
|
||||
private permissionsService: PermissionsService,
|
||||
private hotKeyService: HotKeyService,
|
||||
private componentRouterService: ComponentRouterService
|
||||
) {
|
||||
constructor() {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
|
||||
this.settings.updateAppearanceSettings()
|
||||
|
@ -35,6 +35,7 @@
|
||||
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
||||
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormControl,
|
||||
@ -29,6 +29,7 @@ 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 { PasswordComponent } from '../../common/input/password/password.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'
|
||||
@ -46,6 +47,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
TextComponent,
|
||||
NumberComponent,
|
||||
FileComponent,
|
||||
PasswordComponent,
|
||||
AsyncPipe,
|
||||
NgbNavModule,
|
||||
FormsModule,
|
||||
@ -57,6 +59,10 @@ export class ConfigComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
private configService = inject(ConfigService)
|
||||
private toastService = inject(ToastService)
|
||||
private settingsService = inject(SettingsService)
|
||||
|
||||
public readonly ConfigOptionType = ConfigOptionType
|
||||
|
||||
// generated dynamically
|
||||
@ -77,11 +83,7 @@ export class ConfigComponent
|
||||
storeSub: Subscription
|
||||
isDirty$: Observable<boolean>
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService,
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
this.configForm.addControl('id', new FormControl())
|
||||
PaperlessConfigOptions.forEach((option) => {
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
@ -28,12 +29,8 @@ export class LogsComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private changedetectorRef: ChangeDetectorRef
|
||||
) {
|
||||
super()
|
||||
}
|
||||
private logService = inject(LogService)
|
||||
private changedetectorRef = inject(ChangeDetectorRef)
|
||||
|
||||
public logs: string[] = []
|
||||
|
||||
|
@ -314,6 +314,9 @@ describe('SettingsComponent', () => {
|
||||
sanity_check_status: SystemStatusItemStatus.ERROR,
|
||||
sanity_check_last_run: new Date().toISOString(),
|
||||
sanity_check_error: 'Error running sanity check.',
|
||||
llmindex_status: SystemStatusItemStatus.DISABLED,
|
||||
llmindex_last_modified: new Date().toISOString(),
|
||||
llmindex_error: null,
|
||||
},
|
||||
}
|
||||
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||
|
@ -2,10 +2,10 @@ import { AsyncPipe, ViewportScroller } from '@angular/common'
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
Inject,
|
||||
LOCALE_ID,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
@ -104,6 +104,20 @@ export class SettingsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
private documentListViewService = inject(DocumentListViewService)
|
||||
private toastService = inject(ToastService)
|
||||
private settings = inject(SettingsService)
|
||||
currentLocale = inject(LOCALE_ID)
|
||||
private viewportScroller = inject(ViewportScroller)
|
||||
private activatedRoute = inject(ActivatedRoute)
|
||||
readonly tourService = inject(TourService)
|
||||
private usersService = inject(UserService)
|
||||
private groupsService = inject(GroupService)
|
||||
private router = inject(Router)
|
||||
permissionsService = inject(PermissionsService)
|
||||
private modalService = inject(NgbModal)
|
||||
private systemStatusService = inject(SystemStatusService)
|
||||
|
||||
activeNavID: number
|
||||
|
||||
settingsForm = new FormGroup({
|
||||
@ -179,21 +193,7 @@ export class SettingsComponent
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService,
|
||||
@Inject(LOCALE_ID) public currentLocale: string,
|
||||
private viewportScroller: ViewportScroller,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
public readonly tourService: TourService,
|
||||
private usersService: UserService,
|
||||
private groupsService: GroupService,
|
||||
private router: Router,
|
||||
public permissionsService: PermissionsService,
|
||||
private modalService: NgbModal,
|
||||
private systemStatusService: SystemStatusService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
this.settings.settingsSaved.subscribe(() => {
|
||||
if (!this.savePending) this.initialize()
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NgTemplateOutlet, SlicePipe } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
@ -69,6 +69,10 @@ export class TasksComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tasksService = inject(TasksService)
|
||||
private modalService = inject(NgbModal)
|
||||
private readonly router = inject(Router)
|
||||
|
||||
public activeTab: TaskTab
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
@ -105,14 +109,6 @@ export class TasksComponent
|
||||
: $localize`Dismiss all`
|
||||
}
|
||||
|
||||
constructor(
|
||||
public tasksService: TasksService,
|
||||
private modalService: NgbModal,
|
||||
private readonly router: Router
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.tasksService.reload()
|
||||
timer(5000, 5000)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy } from '@angular/core'
|
||||
import { Component, OnDestroy, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
@ -36,19 +36,19 @@ export class TrashComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnDestroy
|
||||
{
|
||||
private trashService = inject(TrashService)
|
||||
private toastService = inject(ToastService)
|
||||
private modalService = inject(NgbModal)
|
||||
private settingsService = inject(SettingsService)
|
||||
private router = inject(Router)
|
||||
|
||||
public documentsInTrash: Document[] = []
|
||||
public selectedDocuments: Set<number> = new Set()
|
||||
public allToggled: boolean = false
|
||||
public page: number = 1
|
||||
public totalDocuments: number
|
||||
|
||||
constructor(
|
||||
private trashService: TrashService,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private settingsService: SettingsService,
|
||||
private router: Router
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
this.reload()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
@ -31,22 +31,18 @@ export class UsersAndGroupsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private usersService = inject(UserService)
|
||||
private groupsService = inject(GroupService)
|
||||
private toastService = inject(ToastService)
|
||||
private modalService = inject(NgbModal)
|
||||
permissionsService = inject(PermissionsService)
|
||||
private settings = inject(SettingsService)
|
||||
|
||||
users: User[]
|
||||
groups: Group[]
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private groupsService: GroupService,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
public permissionsService: PermissionsService,
|
||||
private settings: SettingsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.usersService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
|
@ -30,6 +30,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul ngbNav class="order-sm-3">
|
||||
@if (aiEnabled) {
|
||||
<pngx-chat></pngx-chat>
|
||||
}
|
||||
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
||||
<li ngbDropdown class="nav-item dropdown">
|
||||
<button class="btn ps-1 border-0" id="userDropdown" ngbDropdownToggle>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgClass } from '@angular/common'
|
||||
import { Component, HostListener, OnInit } from '@angular/core'
|
||||
import { Component, HostListener, inject, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbCollapseModule,
|
||||
@ -44,6 +44,7 @@ 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 { ChatComponent } from '../chat/chat/chat.component'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
@ -59,6 +60,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
|
||||
DocumentTitlePipe,
|
||||
IfPermissionsDirective,
|
||||
ToastsDropdownComponent,
|
||||
ChatComponent,
|
||||
RouterModule,
|
||||
NgClass,
|
||||
NgbDropdownModule,
|
||||
@ -74,26 +76,27 @@ export class AppFrameComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, ComponentCanDeactivate
|
||||
{
|
||||
router = inject(Router)
|
||||
private activatedRoute = inject(ActivatedRoute)
|
||||
private openDocumentsService = inject(OpenDocumentsService)
|
||||
savedViewService = inject(SavedViewService)
|
||||
private remoteVersionService = inject(RemoteVersionService)
|
||||
settingsService = inject(SettingsService)
|
||||
tasksService = inject(TasksService)
|
||||
private readonly toastService = inject(ToastService)
|
||||
private modalService = inject(NgbModal)
|
||||
permissionsService = inject(PermissionsService)
|
||||
private djangoMessagesService = inject(DjangoMessagesService)
|
||||
|
||||
appRemoteVersion: AppRemoteVersion
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
|
||||
slimSidebarAnimating: boolean = false
|
||||
|
||||
constructor(
|
||||
public router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private openDocumentsService: OpenDocumentsService,
|
||||
public savedViewService: SavedViewService,
|
||||
private remoteVersionService: RemoteVersionService,
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
public permissionsService: PermissionsService,
|
||||
private djangoMessagesService: DjangoMessagesService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
const permissionsService = this.permissionsService
|
||||
|
||||
if (
|
||||
permissionsService.currentUserCan(
|
||||
@ -168,6 +171,10 @@ export class AppFrameComponent
|
||||
})
|
||||
}
|
||||
|
||||
get aiEnabled(): boolean {
|
||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
this.isMenuCollapsed = true
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
@ -69,6 +70,17 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e
|
||||
],
|
||||
})
|
||||
export class GlobalSearchComponent implements OnInit {
|
||||
searchService = inject(SearchService)
|
||||
private router = inject(Router)
|
||||
private modalService = inject(NgbModal)
|
||||
private documentService = inject(DocumentService)
|
||||
private documentListViewService = inject(DocumentListViewService)
|
||||
private permissionsService = inject(PermissionsService)
|
||||
private toastService = inject(ToastService)
|
||||
private hotkeyService = inject(HotKeyService)
|
||||
private settingsService = inject(SettingsService)
|
||||
private locationStrategy = inject(LocationStrategy)
|
||||
|
||||
public DataType = DataType
|
||||
public query: string
|
||||
public queryDebounce: Subject<string>
|
||||
@ -90,18 +102,7 @@ export class GlobalSearchComponent implements OnInit {
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public searchService: SearchService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal,
|
||||
private documentService: DocumentService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private permissionsService: PermissionsService,
|
||||
private toastService: ToastService,
|
||||
private hotkeyService: HotKeyService,
|
||||
private settingsService: SettingsService,
|
||||
private locationStrategy: LocationStrategy
|
||||
) {
|
||||
constructor() {
|
||||
this.queryDebounce = new Subject<string>()
|
||||
|
||||
this.queryDebounce
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
<li ngbDropdown class="nav-item" (openChange)="onOpenChange($event)">
|
||||
<li ngbDropdown class="nav-item mx-1" (openChange)="onOpenChange($event)">
|
||||
@if (toasts.length) {
|
||||
<span class="badge rounded-pill z-3 pe-none bg-secondary me-2 position-absolute top-0 left-0">{{ toasts.length }}</span>
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
NgbDropdownModule,
|
||||
NgbProgressbarModule,
|
||||
@ -20,7 +20,7 @@ import { ToastComponent } from '../../common/toast/toast.component'
|
||||
],
|
||||
})
|
||||
export class ToastsDropdownComponent implements OnInit, OnDestroy {
|
||||
constructor(public toastService: ToastService) {}
|
||||
toastService = inject(ToastService)
|
||||
|
||||
private subscription: Subscription
|
||||
|
||||
|
35
src-ui/src/app/components/chat/chat/chat.component.html
Normal file
35
src-ui/src/app/components/chat/chat/chat.component.html
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
<li ngbDropdown class="nav-item me-n2" (openChange)="onOpenChange($event)">
|
||||
<button class="btn border-0" id="chatDropdown" ngbDropdownToggle>
|
||||
<i-bs width="1.3em" height="1.3em" name="chatSquareDots"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu class="dropdown-menu-end shadow p-3" aria-labelledby="chatDropdown">
|
||||
<div class="chat-container bg-light p-2">
|
||||
<div class="chat-messages font-monospace small">
|
||||
@for (message of messages; track message) {
|
||||
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
|
||||
<span class="p-2 m-2" [class.bg-dark]="message.role === 'user'">
|
||||
{{ message.content }}
|
||||
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
<div #scrollAnchor></div>
|
||||
</div>
|
||||
|
||||
<form class="chat-input">
|
||||
<div class="input-group">
|
||||
<input
|
||||
#chatInput
|
||||
class="form-control form-control-sm" name="chatInput" type="text"
|
||||
[placeholder]="placeholder"
|
||||
[disabled]="loading"
|
||||
[(ngModel)]="input"
|
||||
(keydown)="searchInputKeyDown($event)"
|
||||
/>
|
||||
<button class="btn btn-sm btn-secondary" type="button" (click)="sendMessage()" [disabled]="loading">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
37
src-ui/src/app/components/chat/chat/chat.component.scss
Normal file
37
src-ui/src/app/components/chat/chat/chat.component.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.dropdown-menu {
|
||||
width: var(--pngx-toast-max-width);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
:host ::ng-deep .dropdown-menu-end {
|
||||
right: -3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.blinking-cursor {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
from, to {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
132
src-ui/src/app/components/chat/chat/chat.component.spec.ts
Normal file
132
src-ui/src/app/components/chat/chat/chat.component.spec.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ElementRef } from '@angular/core'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NavigationEnd, Router } from '@angular/router'
|
||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject } from 'rxjs'
|
||||
import { ChatService } from 'src/app/services/chat.service'
|
||||
import { ChatComponent } from './chat.component'
|
||||
|
||||
describe('ChatComponent', () => {
|
||||
let component: ChatComponent
|
||||
let fixture: ComponentFixture<ChatComponent>
|
||||
let chatService: ChatService
|
||||
let router: Router
|
||||
let routerEvents$: Subject<NavigationEnd>
|
||||
let mockStream$: Subject<string>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NgxBootstrapIconsModule.pick(allIcons), ChatComponent],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ChatComponent)
|
||||
router = TestBed.inject(Router)
|
||||
routerEvents$ = new Subject<any>()
|
||||
jest
|
||||
.spyOn(router, 'events', 'get')
|
||||
.mockReturnValue(routerEvents$.asObservable())
|
||||
chatService = TestBed.inject(ChatService)
|
||||
mockStream$ = new Subject<string>()
|
||||
jest
|
||||
.spyOn(chatService, 'streamChat')
|
||||
.mockReturnValue(mockStream$.asObservable())
|
||||
component = fixture.componentInstance
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
fixture.detectChanges()
|
||||
|
||||
component.scrollAnchor.nativeElement.scrollIntoView = jest.fn()
|
||||
})
|
||||
|
||||
it('should update documentId on initialization', () => {
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123')
|
||||
component.ngOnInit()
|
||||
expect(component.documentId).toBe(123)
|
||||
})
|
||||
|
||||
it('should update documentId on navigation', () => {
|
||||
component.ngOnInit()
|
||||
routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456'))
|
||||
expect(component.documentId).toBe(456)
|
||||
})
|
||||
|
||||
it('should return correct placeholder based on documentId', () => {
|
||||
component.documentId = 123
|
||||
expect(component.placeholder).toBe('Ask a question about this document...')
|
||||
component.documentId = undefined
|
||||
expect(component.placeholder).toBe('Ask a question about a document...')
|
||||
})
|
||||
|
||||
it('should send a message and handle streaming response', () => {
|
||||
component.input = 'Hello'
|
||||
component.sendMessage()
|
||||
|
||||
expect(component.messages.length).toBe(2)
|
||||
expect(component.messages[0].content).toBe('Hello')
|
||||
expect(component.loading).toBe(true)
|
||||
|
||||
mockStream$.next('Hi')
|
||||
expect(component.messages[1].content).toBe('H')
|
||||
mockStream$.next('Hi there')
|
||||
// advance time to process the typewriter effect
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(component.messages[1].content).toBe('Hi there')
|
||||
|
||||
mockStream$.complete()
|
||||
expect(component.loading).toBe(false)
|
||||
expect(component.messages[1].isStreaming).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle errors during streaming', () => {
|
||||
component.input = 'Hello'
|
||||
component.sendMessage()
|
||||
|
||||
mockStream$.error('Error')
|
||||
expect(component.messages[1].content).toContain(
|
||||
'⚠️ Error receiving response.'
|
||||
)
|
||||
expect(component.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('should enqueue typewriter chunks correctly', () => {
|
||||
const message = { content: '', role: 'assistant', isStreaming: true }
|
||||
component.enqueueTypewriter(null, message as any) // coverage for null
|
||||
component.enqueueTypewriter('Hello', message as any)
|
||||
expect(component['typewriterBuffer'].length).toBe(4)
|
||||
})
|
||||
|
||||
it('should scroll to bottom after sending a message', () => {
|
||||
const scrollSpy = jest.spyOn(
|
||||
ChatComponent.prototype as any,
|
||||
'scrollToBottom'
|
||||
)
|
||||
component.input = 'Test'
|
||||
component.sendMessage()
|
||||
expect(scrollSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should focus chat input when dropdown is opened', () => {
|
||||
const focus = jest.fn()
|
||||
component.chatInput = {
|
||||
nativeElement: { focus: focus },
|
||||
} as unknown as ElementRef<HTMLInputElement>
|
||||
|
||||
component.onOpenChange(true)
|
||||
jest.advanceTimersByTime(15)
|
||||
expect(focus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should send message on Enter key press', () => {
|
||||
jest.spyOn(component, 'sendMessage')
|
||||
const event = new KeyboardEvent('keydown', { key: 'Enter' })
|
||||
component.searchInputKeyDown(event)
|
||||
expect(component.sendMessage).toHaveBeenCalled()
|
||||
})
|
||||
})
|
140
src-ui/src/app/components/chat/chat/chat.component.ts
Normal file
140
src-ui/src/app/components/chat/chat/chat.component.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NavigationEnd, Router } from '@angular/router'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { ChatMessage, ChatService } from 'src/app/services/chat.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-chat',
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
NgbDropdownModule,
|
||||
],
|
||||
templateUrl: './chat.component.html',
|
||||
styleUrl: './chat.component.scss',
|
||||
})
|
||||
export class ChatComponent implements OnInit {
|
||||
public messages: ChatMessage[] = []
|
||||
public loading = false
|
||||
public input: string = ''
|
||||
public documentId!: number
|
||||
|
||||
private chatService: ChatService = inject(ChatService)
|
||||
private router: Router = inject(Router)
|
||||
|
||||
@ViewChild('scrollAnchor') scrollAnchor!: ElementRef<HTMLDivElement>
|
||||
@ViewChild('chatInput') chatInput!: ElementRef<HTMLInputElement>
|
||||
|
||||
private typewriterBuffer: string[] = []
|
||||
private typewriterActive = false
|
||||
|
||||
public get placeholder(): string {
|
||||
return this.documentId
|
||||
? $localize`Ask a question about this document...`
|
||||
: $localize`Ask a question about a document...`
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateDocumentId(this.router.url)
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).url)
|
||||
)
|
||||
.subscribe((url) => {
|
||||
this.updateDocumentId(url)
|
||||
})
|
||||
}
|
||||
|
||||
private updateDocumentId(url: string): void {
|
||||
const docIdRe = url.match(/^\/documents\/(\d+)/)
|
||||
this.documentId = docIdRe ? +docIdRe[1] : undefined
|
||||
}
|
||||
|
||||
sendMessage(): void {
|
||||
if (!this.input.trim()) return
|
||||
|
||||
const userMessage: ChatMessage = { role: 'user', content: this.input }
|
||||
this.messages.push(userMessage)
|
||||
this.scrollToBottom()
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
isStreaming: true,
|
||||
}
|
||||
this.messages.push(assistantMessage)
|
||||
this.loading = true
|
||||
|
||||
let lastPartialLength = 0
|
||||
|
||||
this.chatService.streamChat(this.documentId, this.input).subscribe({
|
||||
next: (chunk) => {
|
||||
const delta = chunk.substring(lastPartialLength)
|
||||
lastPartialLength = chunk.length
|
||||
this.enqueueTypewriter(delta, assistantMessage)
|
||||
},
|
||||
error: () => {
|
||||
assistantMessage.content += '\n\n⚠️ Error receiving response.'
|
||||
assistantMessage.isStreaming = false
|
||||
this.loading = false
|
||||
},
|
||||
complete: () => {
|
||||
assistantMessage.isStreaming = false
|
||||
this.loading = false
|
||||
this.scrollToBottom()
|
||||
},
|
||||
})
|
||||
|
||||
this.input = ''
|
||||
}
|
||||
|
||||
enqueueTypewriter(chunk: string, message: ChatMessage): void {
|
||||
if (!chunk) return
|
||||
|
||||
this.typewriterBuffer.push(...chunk.split(''))
|
||||
|
||||
if (!this.typewriterActive) {
|
||||
this.typewriterActive = true
|
||||
this.playTypewriter(message)
|
||||
}
|
||||
}
|
||||
|
||||
playTypewriter(message: ChatMessage): void {
|
||||
if (this.typewriterBuffer.length === 0) {
|
||||
this.typewriterActive = false
|
||||
return
|
||||
}
|
||||
|
||||
const nextChar = this.typewriterBuffer.shift()!
|
||||
message.content += nextChar
|
||||
this.scrollToBottom()
|
||||
|
||||
setTimeout(() => this.playTypewriter(message), 10) // 10ms per character
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
setTimeout(() => {
|
||||
this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 50)
|
||||
}
|
||||
|
||||
public onOpenChange(open: boolean): void {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
this.chatInput.nativeElement.focus()
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
|
||||
public searchInputKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
this.sendMessage()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { DecimalPipe } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject } from 'rxjs'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
@ -12,9 +12,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
imports: [DecimalPipe, SafeHtmlPipe],
|
||||
})
|
||||
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
||||
constructor(public activeModal: NgbActiveModal) {
|
||||
super()
|
||||
}
|
||||
activeModal = inject(NgbActiveModal)
|
||||
|
||||
@Output()
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Component, TemplateRef, ViewChild } from '@angular/core'
|
||||
import { Component, TemplateRef, ViewChild, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
PDFDocumentProxy,
|
||||
PdfViewerComponent,
|
||||
@ -17,6 +16,8 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
imports: [PdfViewerModule, FormsModule, ReactiveFormsModule, SafeHtmlPipe],
|
||||
})
|
||||
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
private documentService = inject(DocumentService)
|
||||
|
||||
public documentID: number
|
||||
public pages: number[] = []
|
||||
public currentPage: number = 1
|
||||
@ -34,11 +35,8 @@ export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
return this.documentService.getPreviewUrl(this.documentID)
|
||||
}
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
) {
|
||||
super(activeModal)
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
||||
|
@ -3,9 +3,8 @@ import {
|
||||
DragDropModule,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } 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'
|
||||
@ -28,6 +27,9 @@ export class MergeConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
private documentService = inject(DocumentService)
|
||||
private permissionService = inject(PermissionsService)
|
||||
|
||||
public documentIDs: number[] = []
|
||||
public archiveFallback: boolean = false
|
||||
public deleteOriginals: boolean = false
|
||||
@ -38,12 +40,8 @@ export class MergeConfirmDialogComponent
|
||||
|
||||
public metadataDocumentID: number = -1
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super(activeModal)
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { NgStyle } from '@angular/common'
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
@ -13,6 +12,8 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe],
|
||||
})
|
||||
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
documentService = inject(DocumentService)
|
||||
|
||||
public documentID: number
|
||||
public showPDFNote: boolean = true
|
||||
|
||||
@ -25,11 +26,8 @@ export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
return degrees
|
||||
}
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
public documentService: DocumentService
|
||||
) {
|
||||
super(activeModal)
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
rotate(clockwise: boolean = true) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Document } from 'src/app/data/document'
|
||||
@ -23,6 +22,9 @@ export class SplitConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
private documentService = inject(DocumentService)
|
||||
private permissionService = inject(PermissionsService)
|
||||
|
||||
public get pagesString(): string {
|
||||
let pagesStr = ''
|
||||
|
||||
@ -62,12 +64,8 @@ export class SplitConfirmDialogComponent
|
||||
return this.documentService.getPreviewUrl(this.documentID)
|
||||
}
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super(activeModal)
|
||||
constructor() {
|
||||
super()
|
||||
this.confirmButtonEnabled = this.pages.size > 0
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
|
||||
import { Component, Input, LOCALE_ID, OnInit, inject } from '@angular/core'
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
@ -20,6 +20,9 @@ export class CustomFieldDisplayComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
private customFieldService = inject(CustomFieldsService)
|
||||
private documentService = inject(DocumentService)
|
||||
|
||||
CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
private _document: Document
|
||||
@ -63,11 +66,9 @@ export class CustomFieldDisplayComponent
|
||||
|
||||
private defaultCurrencyCode: any
|
||||
|
||||
constructor(
|
||||
private customFieldService: CustomFieldsService,
|
||||
private documentService: DocumentService,
|
||||
@Inject(LOCALE_ID) currentLocale: string
|
||||
) {
|
||||
constructor() {
|
||||
const currentLocale = inject(LOCALE_ID)
|
||||
|
||||
super()
|
||||
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
|
||||
this.customFieldService.listAll().subscribe((r) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions" placement="bottom-end">
|
||||
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||
<i-bs name="ui-radios"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||
<div class="d-none d-lg-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
||||
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
@ -37,6 +38,11 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
|
||||
],
|
||||
})
|
||||
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
|
||||
private customFieldsService = inject(CustomFieldsService)
|
||||
private modalService = inject(NgbModal)
|
||||
private toastService = inject(ToastService)
|
||||
private permissionsService = inject(PermissionsService)
|
||||
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
@Input()
|
||||
@ -78,12 +84,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
private customFieldsService: CustomFieldsService,
|
||||
private modalService: NgbModal,
|
||||
private toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
this.getFields()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { NgTemplateOutlet } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
QueryList,
|
||||
@ -178,6 +179,8 @@ export class CustomFieldQueriesModel {
|
||||
],
|
||||
})
|
||||
export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
|
||||
protected customFieldsService = inject(CustomFieldsService)
|
||||
|
||||
public CustomFieldQueryComponentType = CustomFieldQueryElementType
|
||||
public CustomFieldQueryOperator = CustomFieldQueryOperator
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
@ -245,7 +248,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
||||
|
||||
public readonly today: string = new Date().toISOString().split('T')[0]
|
||||
|
||||
constructor(protected customFieldsService: CustomFieldsService) {
|
||||
constructor() {
|
||||
super()
|
||||
this.selectionModel = new CustomFieldQueriesModel()
|
||||
this.getFields()
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
@ -63,7 +64,9 @@ export enum RelativeDate {
|
||||
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
constructor(settings: SettingsService) {
|
||||
constructor() {
|
||||
const settings = inject(SettingsService)
|
||||
|
||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,6 @@
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
}
|
||||
@if (patternRequired) {
|
||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
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 { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
@ -13,6 +12,7 @@ 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 { CheckComponent } from '../../input/check/check.component'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
@ -22,6 +22,7 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
templateUrl: './correspondent-edit-dialog.component.html',
|
||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||
imports: [
|
||||
CheckComponent,
|
||||
SelectComponent,
|
||||
PermissionsFormComponent,
|
||||
TextComponent,
|
||||
@ -31,13 +32,11 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
],
|
||||
})
|
||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<Correspondent> {
|
||||
constructor(
|
||||
service: CorrespondentService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(CorrespondentService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChildren,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
FormArray,
|
||||
@ -13,7 +14,6 @@ import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import {
|
||||
@ -54,13 +54,11 @@ export class CustomFieldEditDialogComponent
|
||||
.select_options as FormArray
|
||||
}
|
||||
|
||||
constructor(
|
||||
service: CustomFieldsService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(CustomFieldsService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -14,8 +14,6 @@
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
}
|
||||
@if (patternRequired) {
|
||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
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 { DocumentType } from 'src/app/data/document-type'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
@ -13,6 +12,7 @@ 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 { CheckComponent } from '../../input/check/check.component'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
@ -22,6 +22,7 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
templateUrl: './document-type-edit-dialog.component.html',
|
||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||
imports: [
|
||||
CheckComponent,
|
||||
SelectComponent,
|
||||
PermissionsFormComponent,
|
||||
TextComponent,
|
||||
@ -31,13 +32,11 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
],
|
||||
})
|
||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<DocumentType> {
|
||||
constructor(
|
||||
service: DocumentTypeService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(DocumentTypeService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -41,13 +41,9 @@ import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
})
|
||||
class TestComponent extends EditDialogComponent<Tag> {
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = TestBed.inject(TagService)
|
||||
}
|
||||
|
||||
getForm(): FormGroup<any> {
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import {
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Observable } from 'rxjs'
|
||||
@ -29,14 +36,12 @@ export abstract class EditDialogComponent<
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
constructor(
|
||||
protected service: AbstractPaperlessService<T>,
|
||||
private activeModal: NgbActiveModal,
|
||||
private userService: UserService,
|
||||
protected settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
protected service = inject<AbstractPaperlessService<T>>(
|
||||
AbstractPaperlessService
|
||||
)
|
||||
protected activeModal = inject(NgbActiveModal)
|
||||
protected userService = inject(UserService)
|
||||
protected settingsService = inject(SettingsService)
|
||||
|
||||
users: User[]
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
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 { Group } from 'src/app/data/group'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
@ -26,13 +25,11 @@ import { PermissionsSelectComponent } from '../../permissions-select/permissions
|
||||
],
|
||||
})
|
||||
export class GroupEditDialogComponent extends EditDialogComponent<Group> {
|
||||
constructor(
|
||||
service: GroupService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(GroupService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { Component, ViewChild, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbAlert,
|
||||
NgbAlertModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbAlert, NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { IMAPSecurity, MailAccount } from 'src/app/data/mail-account'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
@ -47,13 +43,11 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<MailAcco
|
||||
|
||||
@ViewChild('testResultAlert', { static: false }) testResultAlert: NgbAlert
|
||||
|
||||
constructor(
|
||||
service: MailAccountService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(MailAccountService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
@ -155,32 +154,34 @@ const METADATA_CORRESPONDENT_OPTIONS = [
|
||||
],
|
||||
})
|
||||
export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
|
||||
private accountService: MailAccountService
|
||||
private correspondentService: CorrespondentService
|
||||
private documentTypeService: DocumentTypeService
|
||||
|
||||
accounts: MailAccount[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
|
||||
constructor(
|
||||
service: MailRuleService,
|
||||
activeModal: NgbActiveModal,
|
||||
accountService: MailAccountService,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(MailRuleService)
|
||||
this.accountService = inject(MailAccountService)
|
||||
this.correspondentService = inject(CorrespondentService)
|
||||
this.documentTypeService = inject(DocumentTypeService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
|
||||
accountService
|
||||
this.accountService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.accounts = result.results))
|
||||
|
||||
correspondentService
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
@ -64,8 +64,6 @@
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
}
|
||||
@if (patternRequired) {
|
||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, OnDestroy } from '@angular/core'
|
||||
import { Component, OnDestroy, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||
import {
|
||||
Observable,
|
||||
@ -60,6 +60,8 @@ export class StoragePathEditDialogComponent
|
||||
extends EditDialogComponent<StoragePath>
|
||||
implements OnDestroy
|
||||
{
|
||||
private documentsService = inject(DocumentService)
|
||||
|
||||
public documentsInput$ = new Subject<string>()
|
||||
public foundDocuments$: Observable<Document[]>
|
||||
private testDocument: Document
|
||||
@ -68,14 +70,11 @@ export class StoragePathEditDialogComponent
|
||||
public loading = false
|
||||
public testLoading = false
|
||||
|
||||
constructor(
|
||||
service: StoragePathService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService,
|
||||
private documentsService: DocumentService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(StoragePathService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
this.initPathObservables()
|
||||
}
|
||||
|
||||
|
@ -16,8 +16,6 @@
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
}
|
||||
@if (patternRequired) {
|
||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
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 { Tag } from 'src/app/data/tag'
|
||||
@ -36,13 +35,11 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
],
|
||||
})
|
||||
export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(TagService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { Group } from 'src/app/data/group'
|
||||
@ -37,21 +36,21 @@ export class UserEditDialogComponent
|
||||
extends EditDialogComponent<User>
|
||||
implements OnInit
|
||||
{
|
||||
private toastService = inject(ToastService)
|
||||
private permissionsService = inject(PermissionsService)
|
||||
private groupsService: GroupService
|
||||
|
||||
groups: Group[]
|
||||
passwordIsSet: boolean = false
|
||||
public totpLoading: boolean = false
|
||||
|
||||
constructor(
|
||||
service: UserService,
|
||||
activeModal: NgbActiveModal,
|
||||
groupsService: GroupService,
|
||||
settingsService: SettingsService,
|
||||
private toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
) {
|
||||
super(service, activeModal, service, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(UserService)
|
||||
this.groupsService = inject(GroupService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
|
||||
groupsService
|
||||
this.groupsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.groups = result.results))
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
FormArray,
|
||||
FormControl,
|
||||
@ -12,7 +12,7 @@ import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first } from 'rxjs'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
@ -171,6 +171,12 @@ export class WorkflowEditDialogComponent
|
||||
public WorkflowTriggerType = WorkflowTriggerType
|
||||
public WorkflowActionType = WorkflowActionType
|
||||
|
||||
private correspondentService: CorrespondentService
|
||||
private documentTypeService: DocumentTypeService
|
||||
private storagePathService: StoragePathService
|
||||
private mailRuleService: MailRuleService
|
||||
private customFieldsService: CustomFieldsService
|
||||
|
||||
templates: Workflow[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
@ -183,40 +189,38 @@ export class WorkflowEditDialogComponent
|
||||
|
||||
private allowedActionTypes = []
|
||||
|
||||
constructor(
|
||||
service: WorkflowService,
|
||||
activeModal: NgbActiveModal,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService,
|
||||
storagePathService: StoragePathService,
|
||||
mailRuleService: MailRuleService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService,
|
||||
customFieldsService: CustomFieldsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
constructor() {
|
||||
super()
|
||||
this.service = inject(WorkflowService)
|
||||
this.correspondentService = inject(CorrespondentService)
|
||||
this.documentTypeService = inject(DocumentTypeService)
|
||||
this.storagePathService = inject(StoragePathService)
|
||||
this.mailRuleService = inject(MailRuleService)
|
||||
this.userService = inject(UserService)
|
||||
this.settingsService = inject(SettingsService)
|
||||
this.customFieldsService = inject(CustomFieldsService)
|
||||
|
||||
correspondentService
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
storagePathService
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
mailRuleService
|
||||
this.mailRuleService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.mailRules = result.results))
|
||||
|
||||
customFieldsService
|
||||
this.customFieldsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Component, Input, inject } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
@ -13,6 +13,10 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
imports: [FormsModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class EmailDocumentDialogComponent extends LoadingComponentWithPermissions {
|
||||
private activeModal = inject(NgbActiveModal)
|
||||
private documentService = inject(DocumentService)
|
||||
private toastService = inject(ToastService)
|
||||
|
||||
@Input()
|
||||
title = $localize`Email Document`
|
||||
|
||||
@ -37,11 +41,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
public emailSubject: string = ''
|
||||
public emailMessage: string = ''
|
||||
|
||||
constructor(
|
||||
private activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService,
|
||||
private toastService: ToastService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
this.loading = false
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
@ -434,6 +435,9 @@ export class FilterableDropdownComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
private filterPipe = inject(FilterPipe)
|
||||
private hotkeyService = inject(HotKeyService)
|
||||
|
||||
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||
@ViewChild('buttonItems') buttonItems: ElementRef
|
||||
@ -536,10 +540,7 @@ export class FilterableDropdownComponent
|
||||
|
||||
private keyboardIndex: number
|
||||
|
||||
constructor(
|
||||
private filterPipe: FilterPipe,
|
||||
private hotkeyService: HotKeyService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
this.selectionModelChange.subscribe((updatedModel) => {
|
||||
this.modelIsDirty = updatedModel.isDirty()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
const SYMBOLS = {
|
||||
@ -19,11 +19,11 @@ const SYMBOLS = {
|
||||
styleUrl: './hotkey-dialog.component.scss',
|
||||
})
|
||||
export class HotkeyDialogComponent {
|
||||
activeModal = inject(NgbActiveModal)
|
||||
|
||||
public title: string = $localize`Keyboard shortcuts`
|
||||
public hotkeys: Map<string, string> = new Map()
|
||||
|
||||
constructor(public activeModal: NgbActiveModal) {}
|
||||
|
||||
public close(): void {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
@ -55,7 +56,9 @@ import { UrlComponent } from '../url/url.component'
|
||||
export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> {
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
constructor(customFieldsService: CustomFieldsService) {
|
||||
constructor() {
|
||||
const customFieldsService = inject(CustomFieldsService)
|
||||
|
||||
super()
|
||||
customFieldsService.listAll().subscribe((items) => {
|
||||
this.fields = items.results
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
@ -45,13 +46,9 @@ export class DateComponent
|
||||
extends AbstractInputComponent<string>
|
||||
implements OnInit
|
||||
{
|
||||
constructor(
|
||||
private settings: SettingsService,
|
||||
private ngbDateParserFormatter: NgbDateParserFormatter,
|
||||
private isoDateAdapter: NgbDateAdapter<string>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
private settings = inject(SettingsService)
|
||||
private ngbDateParserFormatter = inject(NgbDateParserFormatter)
|
||||
private isoDateAdapter = inject<NgbDateAdapter<string>>(NgbDateAdapter)
|
||||
|
||||
@Input()
|
||||
suggestions: string[]
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'
|
||||
import {
|
||||
Component,
|
||||
forwardRef,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@ -52,6 +59,8 @@ export class DocumentLinkComponent
|
||||
extends AbstractInputComponent<any[]>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private documentsService = inject(DocumentService)
|
||||
|
||||
documentsInput$ = new Subject<string>()
|
||||
foundDocuments$: Observable<Document[]>
|
||||
loading = false
|
||||
@ -75,10 +84,6 @@ export class DocumentLinkComponent
|
||||
return this.selectedDocuments.map((d) => d.id)
|
||||
}
|
||||
|
||||
constructor(private documentsService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadDocs()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { LOCALE_ID } from '@angular/core'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { MonetaryComponent } from './monetary.component'
|
||||
@ -41,8 +42,6 @@ describe('MonetaryComponent', () => {
|
||||
|
||||
it('should set the default currency code based on LOCALE_ID', () => {
|
||||
expect(component.defaultCurrencyCode).toEqual('USD') // default
|
||||
component = new MonetaryComponent('pt-BR')
|
||||
expect(component.defaultCurrencyCode).toEqual('BRL')
|
||||
})
|
||||
|
||||
it('should support setting a default currency code', () => {
|
||||
@ -87,3 +86,28 @@ describe('MonetaryComponent', () => {
|
||||
expect(component.value).toEqual('USD0.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('MonetaryComponent (Alternate Locale)', () => {
|
||||
let component: MonetaryComponent
|
||||
let fixture: ComponentFixture<MonetaryComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MonetaryComponent],
|
||||
providers: [
|
||||
{ provide: LOCALE_ID, useValue: 'pt-BR' }, // Brazilian Portuguese
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MonetaryComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should set the default currency code based on LOCALE_ID', () => {
|
||||
expect(component.defaultCurrencyCode).toEqual('BRL')
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
|
||||
import { Component, forwardRef, Inject, Input, LOCALE_ID } from '@angular/core'
|
||||
import { Component, forwardRef, inject, Input, LOCALE_ID } from '@angular/core'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@ -27,6 +27,8 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
],
|
||||
})
|
||||
export class MonetaryComponent extends AbstractInputComponent<string> {
|
||||
currentLocale = inject(LOCALE_ID)
|
||||
|
||||
public currency: string = ''
|
||||
|
||||
public _monetaryValue: string = ''
|
||||
@ -45,11 +47,10 @@ export class MonetaryComponent extends AbstractInputComponent<string> {
|
||||
if (currency) this.defaultCurrencyCode = currency
|
||||
}
|
||||
|
||||
constructor(@Inject(LOCALE_ID) currentLocale: string) {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.currency = this.defaultCurrencyCode =
|
||||
this.defaultCurrency ?? getLocaleCurrencyCode(currentLocale)
|
||||
this.defaultCurrency ?? getLocaleCurrencyCode(this.currentLocale)
|
||||
}
|
||||
|
||||
writeValue(newValue: any): void {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef, Input } from '@angular/core'
|
||||
import { Component, forwardRef, inject, Input } from '@angular/core'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@ -22,16 +22,14 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class NumberComponent extends AbstractInputComponent<number> {
|
||||
private documentService = inject(DocumentService)
|
||||
|
||||
@Input()
|
||||
showAdd: boolean = true
|
||||
|
||||
@Input()
|
||||
step: number = 1
|
||||
|
||||
constructor(private documentService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
|
||||
nextAsn() {
|
||||
if (this.value) {
|
||||
return
|
||||
|
@ -1,5 +1,11 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
@if (title) {
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
</div>
|
||||
<div class="position-relative" [class.col-md-9]="horizontal">
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
||||
@if (showReveal) {
|
||||
@ -14,4 +20,5 @@
|
||||
@if (hint) {
|
||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, forwardRef, inject } from '@angular/core'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@ -26,7 +26,9 @@ import { AbstractInputComponent } from '../../abstract-input'
|
||||
export class PermissionsGroupComponent extends AbstractInputComponent<Group> {
|
||||
groups: Group[]
|
||||
|
||||
constructor(groupService: GroupService) {
|
||||
constructor() {
|
||||
const groupService = inject(GroupService)
|
||||
|
||||
super()
|
||||
groupService
|
||||
.listAll()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, forwardRef, inject } from '@angular/core'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@ -8,7 +8,6 @@ import { NgSelectComponent } from '@ng-select/ng-select'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { User } from 'src/app/data/user'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { AbstractInputComponent } from '../../abstract-input'
|
||||
|
||||
@Component({
|
||||
@ -27,7 +26,9 @@ import { AbstractInputComponent } from '../../abstract-input'
|
||||
export class PermissionsUserComponent extends AbstractInputComponent<User[]> {
|
||||
users: User[]
|
||||
|
||||
constructor(userService: UserService, settings: SettingsService) {
|
||||
constructor() {
|
||||
const userService = inject(UserService)
|
||||
|
||||
super()
|
||||
userService
|
||||
.listAll()
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
@ -45,10 +46,10 @@ import { TagComponent } from '../../tag/tag.component'
|
||||
],
|
||||
})
|
||||
export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
constructor(
|
||||
private tagService: TagService,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
private tagService = inject(TagService)
|
||||
private modalService = inject(NgbModal)
|
||||
|
||||
constructor() {
|
||||
this.createTagRef = this.createTag.bind(this)
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,12 @@
|
||||
@if (hint) {
|
||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
}
|
||||
@if (getSuggestion()?.length > 0) {
|
||||
<small>
|
||||
<span i18n>Suggestion:</span>
|
||||
<a (click)="applySuggestion(s)" [routerLink]="[]">{{getSuggestion()}}</a>
|
||||
</small>
|
||||
}
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
</div>
|
||||
|
@ -26,10 +26,20 @@ describe('TextComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
input.value = 'foo'
|
||||
input.dispatchEvent(new Event('input'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBe('foo')
|
||||
})
|
||||
|
||||
it('should support suggestion', () => {
|
||||
component.value = 'foo'
|
||||
component.suggestion = 'foo'
|
||||
expect(component.getSuggestion()).toBe('')
|
||||
component.value = 'bar'
|
||||
expect(component.getSuggestion()).toBe('foo')
|
||||
component.applySuggestion()
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBe('foo')
|
||||
})
|
||||
})
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
@ -24,6 +25,7 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
ReactiveFormsModule,
|
||||
SafeHtmlPipe,
|
||||
NgxBootstrapIconsModule,
|
||||
RouterLink,
|
||||
],
|
||||
})
|
||||
export class TextComponent extends AbstractInputComponent<string> {
|
||||
@ -33,7 +35,19 @@ export class TextComponent extends AbstractInputComponent<string> {
|
||||
@Input()
|
||||
placeholder: string = ''
|
||||
|
||||
@Input()
|
||||
suggestion: string = ''
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
getSuggestion() {
|
||||
return this.value !== this.suggestion ? this.suggestion : ''
|
||||
}
|
||||
|
||||
applySuggestion() {
|
||||
this.value = this.suggestion
|
||||
this.onChange(this.value)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Component, Input, inject } from '@angular/core'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
@ -9,6 +9,8 @@ import { environment } from 'src/environments/environment'
|
||||
styleUrls: ['./logo.component.scss'],
|
||||
})
|
||||
export class LogoComponent {
|
||||
private settingsService = inject(SettingsService)
|
||||
|
||||
@Input()
|
||||
extra_classes: string
|
||||
|
||||
@ -24,8 +26,6 @@ export class LogoComponent {
|
||||
: null
|
||||
}
|
||||
|
||||
constructor(private settingsService: SettingsService) {}
|
||||
|
||||
getClasses() {
|
||||
return ['logo'].concat(this.extra_classes).join(' ')
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Component, Input, inject } from '@angular/core'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
@ -12,7 +12,7 @@ import { environment } from 'src/environments/environment'
|
||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrapModule],
|
||||
})
|
||||
export class PageHeaderComponent {
|
||||
constructor(private titleService: Title) {}
|
||||
private titleService = inject(Title)
|
||||
|
||||
_title = ''
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
@ -24,13 +24,13 @@ import { SwitchComponent } from '../input/switch/switch.component'
|
||||
],
|
||||
})
|
||||
export class PermissionsDialogComponent {
|
||||
activeModal = inject(NgbActiveModal)
|
||||
private userService = inject(UserService)
|
||||
|
||||
users: User[]
|
||||
private o: ObjectWithPermissions = undefined
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private userService: UserService
|
||||
) {
|
||||
constructor() {
|
||||
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NgClass } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||
@ -58,6 +58,9 @@ export enum OwnerFilterType {
|
||||
],
|
||||
})
|
||||
export class PermissionsFilterDropdownComponent extends ComponentWithPermissions {
|
||||
permissionsService = inject(PermissionsService)
|
||||
private settingsService = inject(SettingsService)
|
||||
|
||||
public OwnerFilterType = OwnerFilterType
|
||||
|
||||
@Input()
|
||||
@ -83,12 +86,12 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public permissionsService: PermissionsService,
|
||||
userService: UserService,
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
constructor() {
|
||||
const userService = inject(UserService)
|
||||
|
||||
super()
|
||||
const permissionsService = this.permissionsService
|
||||
|
||||
if (
|
||||
permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { Component, forwardRef, inject, Input, OnInit } from '@angular/core'
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
@ -43,6 +43,9 @@ export class PermissionsSelectComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, ControlValueAccessor
|
||||
{
|
||||
private readonly permissionsService = inject(PermissionsService)
|
||||
private readonly settingsService = inject(SettingsService)
|
||||
|
||||
@Input()
|
||||
title: string = 'Permissions'
|
||||
|
||||
@ -76,10 +79,7 @@ export class PermissionsSelectComponent
|
||||
|
||||
public allowedTypes = Object.keys(PermissionType)
|
||||
|
||||
constructor(
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly settingsService: SettingsService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
if (!this.settingsService.get(SETTINGS_KEYS.AUDITLOG_ENABLED)) {
|
||||
this.allowedTypes.splice(this.allowedTypes.indexOf('History'), 1)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Component, Input, OnDestroy, ViewChild } from '@angular/core'
|
||||
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
|
||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PdfViewerComponent, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
@ -24,6 +24,10 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
],
|
||||
})
|
||||
export class PreviewPopupComponent implements OnDestroy {
|
||||
private settingsService = inject(SettingsService)
|
||||
private documentService = inject(DocumentService)
|
||||
private http = inject(HttpClient)
|
||||
|
||||
private _document: Document
|
||||
@Input()
|
||||
set document(document: Document) {
|
||||
@ -82,12 +86,6 @@ export class PreviewPopupComponent implements OnDestroy {
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
private documentService: DocumentService,
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(this)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
@ -46,6 +46,11 @@ export class ProfileEditDialogComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
private profileService = inject(ProfileService)
|
||||
activeModal = inject(NgbActiveModal)
|
||||
private toastService = inject(ToastService)
|
||||
private clipboard = inject(Clipboard)
|
||||
|
||||
public networkActive: boolean = false
|
||||
public error: any
|
||||
|
||||
@ -83,15 +88,6 @@ export class ProfileEditDialogComponent
|
||||
public socialAccounts: SocialAccount[] = []
|
||||
public socialAccountProviders: SocialAccountProvider[] = []
|
||||
|
||||
constructor(
|
||||
private profileService: ProfileService,
|
||||
public activeModal: NgbActiveModal,
|
||||
private toastService: ToastService,
|
||||
private clipboard: Clipboard
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.networkActive = true
|
||||
this.profileService
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
@ -33,6 +34,8 @@ describe('ShareLinksDialogComponent', () => {
|
||||
imports: [
|
||||
ShareLinksDialogComponent,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
@ -223,16 +226,18 @@ describe('ShareLinksDialogComponent', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should disable archive switch & option if no archive available', () => {
|
||||
it('should disable archive switch & option if no archive available', (done) => {
|
||||
component.hasArchiveVersion = false
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
expect(component.useArchiveVersion).toBeFalsy()
|
||||
expect(
|
||||
fixture.debugElement.query(By.css("input[type='checkbox']")).attributes[
|
||||
'ng-reflect-is-disabled'
|
||||
]
|
||||
).toBeTruthy()
|
||||
setTimeout(() => {
|
||||
// some stupid change detection issue
|
||||
const inputEl = fixture.debugElement.query(By.css('#versionSwitch'))
|
||||
.nativeElement as HTMLInputElement
|
||||
expect(inputEl.disabled).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support close', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
@ -16,6 +16,11 @@ import { environment } from 'src/environments/environment'
|
||||
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class ShareLinksDialogComponent implements OnInit {
|
||||
private activeModal = inject(NgbActiveModal)
|
||||
private shareLinkService = inject(ShareLinkService)
|
||||
private toastService = inject(ToastService)
|
||||
private clipboard = inject(Clipboard)
|
||||
|
||||
EXPIRATION_OPTIONS = [
|
||||
{ label: $localize`1 day`, value: 1 },
|
||||
{ label: $localize`7 days`, value: 7 },
|
||||
@ -58,13 +63,6 @@ export class ShareLinksDialogComponent implements OnInit {
|
||||
|
||||
useArchiveVersion: boolean = true
|
||||
|
||||
constructor(
|
||||
private activeModal: NgbActiveModal,
|
||||
private shareLinkService: ShareLinkService,
|
||||
private toastService: ToastService,
|
||||
private clipboard: Clipboard
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this._documentId !== undefined) this.refresh()
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="loading || (suggestions && !aiEnabled)">
|
||||
@if (loading) {
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
} @else {
|
||||
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs>
|
||||
}
|
||||
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
|
||||
@if (totalSuggestions > 0) {
|
||||
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (aiEnabled) {
|
||||
<div class="btn-group" ngbDropdown #dropdown="ngbDropdown" [popperOptions]="popperOptions">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" ngbDropdownToggle [disabled]="loading || !suggestions" aria-expanded="false" aria-controls="suggestionsDropdown" aria-label="Suggestions dropdown">
|
||||
<span class="visually-hidden" i18n>Show suggestions</span>
|
||||
</button>
|
||||
|
||||
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
||||
<div class="list-group list-group-flush small pb-0">
|
||||
@if (!suggestions?.suggested_tags && !suggestions?.suggested_document_types && !suggestions?.suggested_correspondents) {
|
||||
<div class="list-group-item text-muted fst-italic">
|
||||
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
||||
</div>
|
||||
}
|
||||
@if (suggestions?.suggested_tags.length > 0) {
|
||||
<small class="list-group-item text-uppercase text-muted small">Tags</small>
|
||||
@for (tag of suggestions.suggested_tags; track tag) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)" i18n>{{ tag }}</button>
|
||||
}
|
||||
}
|
||||
@if (suggestions?.suggested_document_types.length > 0) {
|
||||
<div class="list-group-item text-uppercase text-muted small">Document Types</div>
|
||||
@for (type of suggestions.suggested_document_types; track type) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)" i18n>{{ type }}</button>
|
||||
}
|
||||
}
|
||||
@if (suggestions?.suggested_correspondents.length > 0) {
|
||||
<div class="list-group-item text-uppercase text-muted small">Correspondents</div>
|
||||
@for (correspondent of suggestions.suggested_correspondents; track correspondent) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)" i18n>{{ correspondent }}</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
.suggestions-dropdown {
|
||||
min-width: 250px;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { SuggestionsDropdownComponent } from './suggestions-dropdown.component'
|
||||
|
||||
describe('SuggestionsDropdownComponent', () => {
|
||||
let component: SuggestionsDropdownComponent
|
||||
let fixture: ComponentFixture<SuggestionsDropdownComponent>
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbDropdownModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
SuggestionsDropdownComponent,
|
||||
],
|
||||
providers: [],
|
||||
})
|
||||
fixture = TestBed.createComponent(SuggestionsDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should calculate totalSuggestions', () => {
|
||||
component.suggestions = {
|
||||
suggested_correspondents: ['John Doe'],
|
||||
suggested_tags: ['Tag1', 'Tag2'],
|
||||
suggested_document_types: ['Type1'],
|
||||
}
|
||||
expect(component.totalSuggestions).toBe(4)
|
||||
})
|
||||
|
||||
it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => {
|
||||
jest.spyOn(component.getSuggestions, 'emit')
|
||||
component.suggestions = null
|
||||
component.clickSuggest()
|
||||
expect(component.getSuggestions.emit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => {
|
||||
component.aiEnabled = true
|
||||
fixture.detectChanges()
|
||||
component.suggestions = {
|
||||
suggested_correspondents: [],
|
||||
suggested_tags: [],
|
||||
suggested_document_types: [],
|
||||
}
|
||||
component.clickSuggest()
|
||||
expect(component.dropdown.open).toBeTruthy()
|
||||
})
|
||||
})
|
@ -0,0 +1,64 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-suggestions-dropdown',
|
||||
imports: [NgbDropdownModule, NgxBootstrapIconsModule],
|
||||
templateUrl: './suggestions-dropdown.component.html',
|
||||
styleUrl: './suggestions-dropdown.component.scss',
|
||||
})
|
||||
export class SuggestionsDropdownComponent {
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||
|
||||
@Input()
|
||||
suggestions: DocumentSuggestions = null
|
||||
|
||||
@Input()
|
||||
aiEnabled: boolean = false
|
||||
|
||||
@Input()
|
||||
loading: boolean = false
|
||||
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
@Output()
|
||||
getSuggestions: EventEmitter<SuggestionsDropdownComponent> =
|
||||
new EventEmitter()
|
||||
|
||||
@Output()
|
||||
addTag: EventEmitter<string> = new EventEmitter()
|
||||
|
||||
@Output()
|
||||
addDocumentType: EventEmitter<string> = new EventEmitter()
|
||||
|
||||
@Output()
|
||||
addCorrespondent: EventEmitter<string> = new EventEmitter()
|
||||
|
||||
public clickSuggest(): void {
|
||||
if (!this.suggestions) {
|
||||
this.getSuggestions.emit(this)
|
||||
} else {
|
||||
this.dropdown?.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
get totalSuggestions(): number {
|
||||
return (
|
||||
this.suggestions?.suggested_correspondents?.length +
|
||||
this.suggestions?.suggested_tags?.length +
|
||||
this.suggestions?.suggested_document_types?.length || 0
|
||||
)
|
||||
}
|
||||
}
|
@ -254,6 +254,43 @@
|
||||
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
@if (aiEnabled) {
|
||||
<dt i18n>AI Index</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="llmIndexStatus" triggers="click mouseenter:mouseleave">
|
||||
{{status.tasks.llmindex_status}}
|
||||
@if (status.tasks.llmindex_status === 'OK') {
|
||||
@if (isStale(status.tasks.llmindex_last_modified)) {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
}
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
|
||||
[class.text-danger]="status.tasks.llmindex_status === SystemStatusItemStatus.ERROR"
|
||||
[class.text-warning]="status.tasks.llmindex_status === SystemStatusItemStatus.WARNING"
|
||||
[class.text-muted]="status.tasks.llmindex_status === SystemStatusItemStatus.DISABLED"></i-bs>
|
||||
}
|
||||
</button>
|
||||
@if (currentUserIsSuperUser) {
|
||||
@if (isRunning(PaperlessTaskName.LLMIndexUpdate)) {
|
||||
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
|
||||
} @else {
|
||||
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.LLMIndexUpdate)">
|
||||
<i-bs name="play-fill"></i-bs>
|
||||
<ng-container i18n>Run Task</ng-container>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</dd>
|
||||
<ng-template #llmIndexStatus>
|
||||
@if (status.tasks.llmindex_status === 'OK') {
|
||||
<h6><ng-container i18n>Last Run</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.llmindex_last_modified | customDate:'medium'}}</span>
|
||||
} @else {
|
||||
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.llmindex_error}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,6 +67,9 @@ const status: SystemStatus = {
|
||||
sanity_check_status: SystemStatusItemStatus.OK,
|
||||
sanity_check_last_run: new Date().toISOString(),
|
||||
sanity_check_error: null,
|
||||
llmindex_status: SystemStatusItemStatus.OK,
|
||||
llmindex_last_modified: new Date().toISOString(),
|
||||
llmindex_error: null,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbModalModule,
|
||||
@ -12,9 +12,11 @@ import {
|
||||
SystemStatus,
|
||||
SystemStatusItemStatus,
|
||||
} from 'src/app/data/system-status'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@ -35,6 +37,14 @@ import { environment } from 'src/environments/environment'
|
||||
],
|
||||
})
|
||||
export class SystemStatusDialogComponent implements OnInit {
|
||||
activeModal = inject(NgbActiveModal)
|
||||
private clipboard = inject(Clipboard)
|
||||
private systemStatusService = inject(SystemStatusService)
|
||||
private tasksService = inject(TasksService)
|
||||
private toastService = inject(ToastService)
|
||||
private permissionsService = inject(PermissionsService)
|
||||
private settingsService = inject(SettingsService)
|
||||
|
||||
public SystemStatusItemStatus = SystemStatusItemStatus
|
||||
public PaperlessTaskName = PaperlessTaskName
|
||||
public status: SystemStatus
|
||||
@ -49,14 +59,9 @@ export class SystemStatusDialogComponent implements OnInit {
|
||||
return this.permissionsService.isSuperUser()
|
||||
}
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private clipboard: Clipboard,
|
||||
private systemStatusService: SystemStatusService,
|
||||
private tasksService: TasksService,
|
||||
private toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
) {}
|
||||
get aiEnabled(): boolean {
|
||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.versionMismatch =
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Component, inject, Input } from '@angular/core'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import {
|
||||
PermissionAction,
|
||||
@ -13,14 +13,12 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
styleUrls: ['./tag.component.scss'],
|
||||
})
|
||||
export class TagComponent {
|
||||
private permissionsService = inject(PermissionsService)
|
||||
private tagService = inject(TagService)
|
||||
|
||||
private _tag: Tag
|
||||
private _tagID: number
|
||||
|
||||
constructor(
|
||||
private permissionsService: PermissionsService,
|
||||
private tagService: TagService
|
||||
) {}
|
||||
|
||||
@Input()
|
||||
public set tag(tag: Tag) {
|
||||
this._tag = tag
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { DecimalPipe } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
||||
import {
|
||||
NgbProgressbarModule,
|
||||
NgbToastModule,
|
||||
@ -21,6 +21,8 @@ import { Toast } from 'src/app/services/toast.service'
|
||||
styleUrl: './toast.component.scss',
|
||||
})
|
||||
export class ToastComponent {
|
||||
private clipboard = inject(Clipboard)
|
||||
|
||||
@Input() toast: Toast
|
||||
|
||||
@Input() autohide: boolean = true
|
||||
@ -31,8 +33,6 @@ export class ToastComponent {
|
||||
|
||||
public copied: boolean = false
|
||||
|
||||
constructor(private clipboard: Clipboard) {}
|
||||
|
||||
onShown(toast: Toast) {
|
||||
if (!this.autohide) return
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
NgbAccordionModule,
|
||||
NgbProgressbarModule,
|
||||
@ -20,7 +20,7 @@ import { ToastComponent } from '../toast/toast.component'
|
||||
],
|
||||
})
|
||||
export class ToastsComponent implements OnInit, OnDestroy {
|
||||
constructor(public toastService: ToastService) {}
|
||||
toastService = inject(ToastService)
|
||||
|
||||
private subscription: Subscription
|
||||
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
DragDropModule,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
@ -42,13 +42,13 @@ import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-widget.
|
||||
],
|
||||
})
|
||||
export class DashboardComponent extends ComponentWithPermissions {
|
||||
settingsService = inject(SettingsService)
|
||||
savedViewService = inject(SavedViewService)
|
||||
private tourService = inject(TourService)
|
||||
private toastService = inject(ToastService)
|
||||
|
||||
public dashboardViews: SavedView[] = []
|
||||
constructor(
|
||||
public settingsService: SettingsService,
|
||||
public savedViewService: SavedViewService,
|
||||
private tourService: TourService,
|
||||
private toastService: ToastService
|
||||
) {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.savedViewService.listAll().subscribe(() => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AsyncPipe, NgClass, NgStyle } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
@ -84,26 +85,22 @@ export class SavedViewWidgetComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private documentService = inject(DocumentService)
|
||||
private router = inject(Router)
|
||||
private list = inject(DocumentListViewService)
|
||||
private websocketStatusService = inject(WebsocketStatusService)
|
||||
openDocumentsService = inject(OpenDocumentsService)
|
||||
documentListViewService = inject(DocumentListViewService)
|
||||
permissionsService = inject(PermissionsService)
|
||||
private settingsService = inject(SettingsService)
|
||||
private customFieldService = inject(CustomFieldsService)
|
||||
|
||||
public DisplayMode = DisplayMode
|
||||
public DisplayField = DisplayField
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
private customFields: CustomField[] = []
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private router: Router,
|
||||
private list: DocumentListViewService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public documentListViewService: DocumentListViewService,
|
||||
public permissionsService: PermissionsService,
|
||||
private settingsService: SettingsService,
|
||||
private customFieldService: CustomFieldsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@Input()
|
||||
savedView: SavedView
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DecimalPipe } from '@angular/common'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import * as mimeTypeNames from 'mime-names'
|
||||
@ -51,15 +51,11 @@ export class StatisticsWidgetComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
loading: boolean = false
|
||||
private http = inject(HttpClient)
|
||||
private websocketConnectionService = inject(WebsocketStatusService)
|
||||
private documentListViewService = inject(DocumentListViewService)
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private websocketConnectionService: WebsocketStatusService,
|
||||
private documentListViewService: DocumentListViewService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
loading: boolean = false
|
||||
|
||||
statistics: Statistics = {}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, QueryList, ViewChildren } from '@angular/core'
|
||||
import { Component, QueryList, ViewChildren, inject } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbAlert,
|
||||
@ -37,15 +37,11 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
],
|
||||
})
|
||||
export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
@ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
|
||||
private websocketStatusService = inject(WebsocketStatusService)
|
||||
private uploadDocumentsService = inject(UploadDocumentsService)
|
||||
settingsService = inject(SettingsService)
|
||||
|
||||
constructor(
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
public settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
|
||||
|
||||
getStatus() {
|
||||
return this.websocketStatusService.getConsumerStatus()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Output, inject } from '@angular/core'
|
||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
@ -9,7 +9,7 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
imports: [NgbAlertModule],
|
||||
})
|
||||
export class WelcomeWidgetComponent {
|
||||
constructor(public readonly tourService: TourService) {}
|
||||
readonly tourService = inject(TourService)
|
||||
|
||||
@Output()
|
||||
dismiss: EventEmitter<boolean> = new EventEmitter()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { FILTER_ASN } from '../../data/filter-rule-type'
|
||||
import { DocumentService } from '../../services/rest/document.service'
|
||||
@ -9,12 +9,11 @@ import { DocumentService } from '../../services/rest/document.service'
|
||||
styleUrls: ['./document-asn.component.scss'],
|
||||
})
|
||||
export class DocumentAsnComponent implements OnInit {
|
||||
private documentsService = inject(DocumentService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private router = inject(Router)
|
||||
|
||||
asn: string
|
||||
constructor(
|
||||
private documentsService: DocumentService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.paramMap.subscribe((paramMap) => {
|
||||
|
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