Compare commits

..

132 Commits

Author SHA1 Message Date
shamoon
da2ac19193 Update ai_classifier.py 2025-07-15 14:42:56 -07:00
shamoon
3583470856 Merge branch 'dev' into feature-ai 2025-07-15 14:36:03 -07:00
shamoon
5bfbe856a6 Fix tests for change to structured output 2025-07-15 14:34:54 -07:00
shamoon
20bae4bd41 Move to structured output 2025-07-15 14:27:29 -07:00
shamoon
fa496dfc8d Fix: also fix frontend date format in other places
See #10369
2025-07-11 00:46:20 -07:00
shamoon
924471b59c Fix: fix date format for 'today' in DateComponent (#10369) 2025-07-11 00:43:52 -07:00
shamoon
b94912a392 Merge branch 'dev' into feature-ai 2025-07-08 14:20:07 -07:00
shamoon
50e6a4bd61 Merge branch 'dev' into feature-ai 2025-07-08 14:19:26 -07:00
GitHub Actions
feb320cae9 Auto translate strings 2025-07-08 21:14:57 +00:00
shamoon
9178af5fb2 Feature: add Vietnamese translation (#10352) 2025-07-08 14:13:20 -07:00
dependabot[bot]
850444c2fc docker(deps): Bump astral-sh/uv (#10343)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.7.9-python3.12-bookworm-slim to 0.7.19-python3.12-bookworm-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.7.9...0.7.19)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.7.19-python3.12-bookworm-slim
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 12:26:24 -07:00
GitHub Actions
90baba2cec Auto translate strings 2025-07-08 16:31:30 +00:00
dependabot[bot]
9889c59d3d Chore(deps): Bump the small-changes group across 1 directory with 7 updates (#10347)
* Chore(deps): Bump the small-changes group across 1 directory with 7 updates

Bumps the small-changes group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [concurrent-log-handler](https://github.com/Preston-Landers/concurrent-log-handler) | `0.9.26` | `0.9.28` |
| [dateparser](https://github.com/scrapinghub/dateparser) | `1.2.1` | `1.2.2` |
| [imap-tools](https://github.com/ikvk/imap_tools) | `1.10.0` | `1.11.0` |
| [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF) | `16.10.2` | `16.10.4` |
| [pathvalidate](https://github.com/thombashi/pathvalidate) | `3.2.3` | `3.3.1` |
| [python-dotenv](https://github.com/theskumar/python-dotenv) | `1.1.0` | `1.1.1` |
| [scikit-learn](https://github.com/scikit-learn/scikit-learn) | `1.6.1` | `1.7.0` |



Updates `concurrent-log-handler` from 0.9.26 to 0.9.28
- [Release notes](https://github.com/Preston-Landers/concurrent-log-handler/releases)
- [Changelog](https://github.com/Preston-Landers/concurrent-log-handler/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Preston-Landers/concurrent-log-handler/compare/0.9.26...0.9.28)

Updates `dateparser` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/scrapinghub/dateparser/releases)
- [Changelog](https://github.com/scrapinghub/dateparser/blob/master/HISTORY.rst)
- [Commits](https://github.com/scrapinghub/dateparser/compare/v1.2.1...v1.2.2)

Updates `imap-tools` from 1.10.0 to 1.11.0
- [Release notes](https://github.com/ikvk/imap_tools/releases)
- [Changelog](https://github.com/ikvk/imap_tools/blob/master/docs/release_notes.rst)
- [Commits](https://github.com/ikvk/imap_tools/compare/v1.10.0...v1.11.0)

Updates `ocrmypdf` from 16.10.2 to 16.10.4
- [Release notes](https://github.com/ocrmypdf/OCRmyPDF/releases)
- [Changelog](https://github.com/ocrmypdf/OCRmyPDF/blob/main/docs/release_notes.md)
- [Commits](https://github.com/ocrmypdf/OCRmyPDF/compare/v16.10.2...v16.10.4)

Updates `pathvalidate` from 3.2.3 to 3.3.1
- [Release notes](https://github.com/thombashi/pathvalidate/releases)
- [Changelog](https://github.com/thombashi/pathvalidate/blob/master/CHANGELOG.md)
- [Commits](https://github.com/thombashi/pathvalidate/compare/v3.2.3...v3.3.1)

Updates `python-dotenv` from 1.1.0 to 1.1.1
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1)

Updates `scikit-learn` from 1.6.1 to 1.7.0
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.6.1...1.7.0)

---
updated-dependencies:
- dependency-name: concurrent-log-handler
  dependency-version: 0.9.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: dateparser
  dependency-version: 1.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: imap-tools
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: ocrmypdf
  dependency-version: 16.10.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: pathvalidate
  dependency-version: 3.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: python-dotenv
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: scikit-learn
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
...

Signed-off-by: dependabot[bot] <support@github.com>

* Removes the setup_logging_queues call which is no longer needed

* Renames the MailboxTLS

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
2025-07-08 09:29:47 -07:00
Trenton H
3d2a3ede71 Chore: Updates dependency groups (#10339) 2025-07-07 17:37:58 -07:00
shamoon
bc019fab96 Fix: default to empty permissions for group creation (#10337) 2025-07-07 07:21:27 -07:00
shamoon
1cd21d0f38 Fix check scheduled workflows docstring 2025-07-03 00:11:12 -07:00
shamoon
87e5d82c46 Refactor to use Angular inject() for service injection, remove log line 2025-07-02 11:18:08 -07:00
shamoon
476844f32a Merge migrations again 2025-07-02 11:05:00 -07:00
shamoon
01285c96d4 Fix merge conflict 2025-07-02 11:05:00 -07:00
shamoon
3e6ba34c5e Merge migrations 2025-07-02 11:04:59 -07:00
shamoon
d9cbd3652a Add fallback parsing for invalid ai responses 2025-07-02 11:04:59 -07:00
shamoon
90bd878cf2 Truncate similar docs content 2025-07-02 11:04:58 -07:00
shamoon
62e04ab2fe Fix paperless_ai logging 2025-07-02 11:04:58 -07:00
shamoon
dbdc67da7a token limiting 2025-07-02 11:04:58 -07:00
shamoon
11a4e0d5ba Update AI docs 2025-07-02 11:04:57 -07:00
shamoon
c4b431f5a6 Cover app config changes 2025-07-02 11:04:57 -07:00
shamoon
d31f4669a2 Mock auto-trigger llm index 2025-07-02 11:04:56 -07:00
shamoon
483f1e9438 Fix / cleanup ai indexing test 2025-07-02 11:04:56 -07:00
shamoon
d7a358d39d Doh, add tests in new module 2025-07-02 11:04:56 -07:00
shamoon
b94a60d607 Coverage for llmindex tasks 2025-07-02 11:04:55 -07:00
shamoon
e6d8cd6547 Cover llmindex in system status 2025-07-02 11:04:55 -07:00
shamoon
e2fc7f596d Add llmindex to systemstatus 2025-07-02 11:04:54 -07:00
shamoon
20e7f01cec Auto-trigger llmindex rebuild when enabled 2025-07-02 11:04:04 -07:00
shamoon
96daa5eb18 Use PaperlessTask for llmindex 2025-07-02 11:04:04 -07:00
shamoon
84e17535fc Create llmindex if doesnt exist on update run 2025-07-02 11:04:03 -07:00
shamoon
77db0c399c Move ai to its own module 2025-07-02 11:04:03 -07:00
shamoon
e51c7a27bb Better respect perms for ai suggestions 2025-07-02 11:04:03 -07:00
shamoon
a3455c8373 Refactor load_or_build_index 2025-07-02 11:04:02 -07:00
shamoon
cce9dfd5b8 Update chat view decorators 2025-07-02 11:04:02 -07:00
shamoon
3a9257f10a Cover matching 2025-07-02 11:04:01 -07:00
shamoon
3b921da6c3 Cover partial indexing 2025-07-02 11:04:01 -07:00
shamoon
ad8519482c Refactor and consolidate rag / embedding and tests 2025-07-02 11:04:01 -07:00
shamoon
fe205b31c2 indexing cleanup and tests 2025-07-02 11:04:00 -07:00
shamoon
13ab148c7e Use partial reindex for bulk updates 2025-07-02 11:04:00 -07:00
shamoon
559caf72c2 Unify prompts, cover 2025-07-02 11:03:59 -07:00
shamoon
2481a66544 Incremental llm index update, add scheduled llm index task 2025-07-02 11:03:59 -07:00
shamoon
f6a3882199 Some cleanup, typing 2025-07-02 11:03:59 -07:00
shamoon
8d48d398eb Handle doc updates, refactor 2025-07-02 11:03:58 -07:00
shamoon
b3b9a8fb5b Chat coverage 2025-07-02 11:03:58 -07:00
shamoon
4cdc629e3d Tests for rest of RAG 2025-07-02 11:03:57 -07:00
shamoon
5195a97e4c Chat component and service coverage 2025-07-02 11:03:57 -07:00
shamoon
96fa522394 Real doc ID updating 2025-07-02 11:03:57 -07:00
shamoon
dd1da9f072 Sweet chat animation, cursor 2025-07-02 11:03:56 -07:00
shamoon
d99f2d6160 Only show chat if enabled 2025-07-02 11:03:56 -07:00
shamoon
ebd46f08e5 Fix partial length in chat 2025-07-02 11:03:55 -07:00
shamoon
6f0c6f39b1 Fix gzip breaks streaming and flush stream 2025-07-02 11:03:55 -07:00
shamoon
0690fd36c5 Fix openai api key, config settings saving 2025-07-02 11:03:55 -07:00
shamoon
0052f21cea Try rewriting with httpclient 2025-07-02 11:03:54 -07:00
shamoon
c809a65571 Extremely basic chat component 2025-07-02 11:03:07 -07:00
shamoon
bb3336f7bc Just use the built-in ollama LLM class of course 2025-07-02 11:01:58 -07:00
shamoon
a9ed46de11 Fix naming 2025-07-02 11:01:58 -07:00
shamoon
1ccaf66869 Trim nodes 2025-07-02 11:01:57 -07:00
shamoon
e864a51497 Backend streaming chat 2025-07-02 11:01:57 -07:00
shamoon
4a28be233e Fixup some tests 2025-07-02 11:01:56 -07:00
shamoon
9183bfc0a4 Just some docs
[ci skip]
2025-07-02 11:01:56 -07:00
shamoon
5f26139a5f Unify, respect perms
[ci skip]
2025-07-02 11:01:56 -07:00
shamoon
ccfc7d98b1 Individual doc chat
[ci skip]
2025-07-02 11:01:55 -07:00
shamoon
d1bd2af49c Super basic doc chat
[ci skip]
2025-07-02 11:01:55 -07:00
shamoon
e2eec6dc71 Better encapsulate backends, use llama_index OpenAI 2025-07-02 11:01:54 -07:00
shamoon
42e3684211 Add backend settings to frontend config
[ci skip]
2025-07-02 11:01:54 -07:00
shamoon
df8f07555f Tweak ollama timeout, prompt
[ci skip]
2025-07-02 11:01:54 -07:00
shamoon
3660336bcf Fix ollama, fix RAG
[ci skip]
2025-07-02 11:01:53 -07:00
shamoon
aeceaf60a2 RAG into suggestions 2025-07-02 11:01:53 -07:00
shamoon
959ebdbb85 llamaindex vector index, llmindex mangement command 2025-07-02 11:01:52 -07:00
shamoon
eb1c49090b Docs 2025-07-02 11:01:52 -07:00
shamoon
9f8b8a9f20 Use password and select config fields 2025-07-02 11:01:52 -07:00
shamoon
f5fc04cfe2 Use a frontend config 2025-07-02 11:01:51 -07:00
shamoon
3186550fd7 Pass AI enabled to frontend 2025-07-02 11:01:51 -07:00
shamoon
74aaf18630 Basic handling of non-AI response 2025-07-02 11:01:50 -07:00
shamoon
e6a147079d Cleaner auto-remove 2025-07-02 11:01:50 -07:00
shamoon
105b823fd9 Automatically remove suggestions after add 2025-07-02 11:01:50 -07:00
shamoon
be20c48588 Test views, caching 2025-07-02 11:01:49 -07:00
shamoon
377dcc39f5 Invalidate llm suggestion cache on doc save 2025-07-02 11:01:49 -07:00
shamoon
767118fa8a Fix 2025-07-02 11:01:48 -07:00
shamoon
339612f4ec Backend tests 2025-07-02 11:01:48 -07:00
shamoon
e7592c6269 Correct object retrieval 2025-07-02 11:01:48 -07:00
shamoon
ffc0b936f3 Refactor 2025-07-02 11:01:47 -07:00
shamoon
1a6540e8ed Move module 2025-07-02 11:01:47 -07:00
shamoon
abbf9060d0 Hook up the add buttons 2025-07-02 11:01:46 -07:00
shamoon
11a3dfe890 Refine the suggestions dropdown ui a bit 2025-07-02 11:01:46 -07:00
shamoon
faa5d3e5b9 Suggestions dropdown 2025-07-02 11:01:46 -07:00
shamoon
8d1a8c2c42 Messing with a suggest button 2025-07-02 11:01:45 -07:00
shamoon
01dc3cc17c Rename config 2025-07-02 11:01:45 -07:00
shamoon
cfbd5af820 Title suggestion ui 2025-07-02 11:01:44 -07:00
shamoon
e8090fd030 Just start the frontend
[ci skip]
2025-07-02 11:01:44 -07:00
shamoon
05896d5b70 wow llama3 is bad 2025-07-02 11:00:59 -07:00
shamoon
65b8a74166 Changeup logging 2025-07-02 11:00:58 -07:00
shamoon
56b1c7adeb Some logging, error handling 2025-07-02 11:00:58 -07:00
shamoon
55cb9cedc7 Basic start 2025-07-02 11:00:54 -07:00
Antoine Mérino
f940ed0b7b Fix: Fallback to Redis URL if no DB read cache Redis URL provided (#10315)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-07-02 10:59:24 -07:00
dependabot[bot]
3180ccf4cb Chore(deps): Bump stefanzweifel/git-auto-commit-action (#10302)
Bumps the actions group with 1 update: [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action).


Updates `stefanzweifel/git-auto-commit-action` from 5 to 6
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 14:26:07 +00:00
dependabot[bot]
43abb0541b Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates (#10311)
Bumps the frontend-eslint-dependencies group with 4 updates in the /src-ui directory: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser), [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) and [eslint](https://github.com/eslint/eslint).


Updates `@typescript-eslint/eslint-plugin` from 8.35.0 to 8.35.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.35.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.35.0 to 8.35.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.35.1/packages/parser)

Updates `@typescript-eslint/utils` from 8.35.0 to 8.35.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.35.1/packages/utils)

Updates `eslint` from 9.28.0 to 9.30.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.28.0...v9.30.1)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.35.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.35.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.35.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: eslint
  dependency-version: 9.30.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-eslint-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 22:14:01 +00:00
dependabot[bot]
a3a405354f Chore(deps-dev): Bump @types/node from 22.15.29 to 24.0.10 in /src-ui (#10306)
---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.0.10
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 22:03:30 +00:00
dependabot[bot]
09e98d600e Chore(deps): Bump bootstrap from 5.3.6 to 5.3.7 in /src-ui (#10308)
---
updated-dependencies:
- dependency-name: bootstrap
  dependency-version: 5.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 21:52:26 +00:00
dependabot[bot]
01a39b9bb4 Chore(deps-dev): Bump webpack from 5.98.0 to 5.99.9 in /src-ui (#10309)
---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.99.9
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 21:39:13 +00:00
dependabot[bot]
3b0b40f071 Chore(deps-dev): Bump @playwright/test from 1.51.1 to 1.53.2 in /src-ui (#10307)
---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.53.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 21:19:26 +00:00
GitHub Actions
6dce83865f Auto translate strings 2025-07-01 20:47:25 +00:00
dependabot[bot]
18252a19d7 Chore(deps): Bump the frontend-angular-dependencies group (#10303)
Bumps the frontend-angular-dependencies group in /src-ui with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `20.0.5` | `20.0.6` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `20.0.5` | `20.0.6` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `20.0.5` | `20.0.6` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `20.0.5` | `20.0.6` |
| [@angular/localize](https://github.com/angular/angular) | `20.0.5` | `20.0.6` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `20.0.5` | `20.0.6` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `20.0.5` | `20.0.6` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `20.0.5` | `20.0.6` |
| [@ng-bootstrap/ng-bootstrap](https://github.com/ng-bootstrap/ng-bootstrap) | `19.0.0` | `19.0.1` |
| [@ng-select/ng-select](https://github.com/ng-select/ng-select) | `15.1.2` | `15.1.3` |
| [ngx-cookie-service](https://github.com/stevermeister/ngx-cookie-service) | `19.1.2` | `20.0.1` |
| [ngx-device-detector](https://github.com/AhsanAyaz/ngx-device-detector) | `9.0.0` | `10.0.2` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `20.0.5` | `20.0.6` |


Updates `@angular/common` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/common)

Updates `@angular/compiler` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/compiler)

Updates `@angular/core` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/core)

Updates `@angular/forms` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/forms)

Updates `@angular/localize` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/compare/20.0.5...20.0.6)

Updates `@angular/platform-browser` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/platform-browser)

Updates `@angular/platform-browser-dynamic` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/platform-browser-dynamic)

Updates `@angular/router` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/router)

Updates `@ng-bootstrap/ng-bootstrap` from 19.0.0 to 19.0.1
- [Release notes](https://github.com/ng-bootstrap/ng-bootstrap/releases)
- [Changelog](https://github.com/ng-bootstrap/ng-bootstrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ng-bootstrap/ng-bootstrap/compare/19.0.0...19.0.1)

Updates `@ng-select/ng-select` from 15.1.2 to 15.1.3
- [Release notes](https://github.com/ng-select/ng-select/releases)
- [Changelog](https://github.com/ng-select/ng-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ng-select/ng-select/compare/v15.1.2...v15.1.3)

Updates `ngx-cookie-service` from 19.1.2 to 20.0.1
- [Release notes](https://github.com/stevermeister/ngx-cookie-service/releases)
- [Changelog](https://github.com/stevermeister/ngx-cookie-service/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stevermeister/ngx-cookie-service/compare/v19.1.2...v20.0.1)

Updates `ngx-device-detector` from 9.0.0 to 10.0.2
- [Release notes](https://github.com/AhsanAyaz/ngx-device-detector/releases)
- [Changelog](https://github.com/AhsanAyaz/ngx-device-detector/blob/master/steps-to-release.md)
- [Commits](https://github.com/AhsanAyaz/ngx-device-detector/compare/v9.0.0...v10.0.2)

Updates `@angular/compiler-cli` from 20.0.5 to 20.0.6
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.0.6/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/common"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-version: 20.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-bootstrap/ng-bootstrap"
  dependency-version: 19.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-select/ng-select"
  dependency-version: 15.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: ngx-cookie-service
  dependency-version: 20.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-angular-dependencies
- dependency-name: ngx-device-detector
  dependency-version: 10.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 20.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 13:44:44 -07:00
Antoine Mérino
733a9674d6 Fix: prevent duplicate cachalot app in Django settings (#10300)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-07-01 09:11:58 -07:00
GitHub Actions
f3b6e15321 Auto translate strings 2025-07-01 05:59:21 +00:00
Antoine Mérino
6591d5da63 Performance: Add support for configuring date parser languages (#10181)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-07-01 05:57:38 +00:00
GitHub Actions
c974dc9400 Auto translate strings 2025-07-01 05:41:44 +00:00
Antoine Mérino
1671d49d44 Enhancement: Add a database caching for improved performance (#9784)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-06-30 22:36:24 -07:00
GitHub Actions
6b248ef140 Auto translate strings 2025-06-29 04:40:53 +00:00
shamoon
735681d294 Fix: correct api created coercion with timezone (#10287) 2025-06-28 21:39:14 -07:00
shamoon
a9085c65c5 Chore: fix some test warnings / errors 2025-06-27 15:03:10 -07:00
shamoon
e312425b1c Fix: reset search query for preview on reset filter (#10279) 2025-06-27 14:36:38 -07:00
GitHub Actions
13fe064f6e Auto translate strings 2025-06-27 21:08:27 +00:00
shamoon
958f98d7e5 Chore: update to Angular 20 (#10273) 2025-06-27 14:06:40 -07:00
shamoon
dfad3c4d8e Chore: clarify file deletion logging 2025-06-27 13:34:44 -07:00
shamoon
37267f3f04 Chore: add webpack directly 2025-06-22 20:03:48 -07:00
shamoon
b34538d991 Update support.yml 2025-06-22 08:03:41 -07:00
shamoon
fc97bd1315 Chore: create support discussion template 2025-06-22 08:02:26 -07:00
shamoon
dbf3721ec2 Chore: reject absurd max age values (#10243) 2025-06-22 07:39:36 -07:00
shamoon
59afbe09b1 Chore: remove PAPERLESS_DEBUG references to avoid confusion 2025-06-20 20:46:11 -07:00
shamoon
bfeaa1b119 Chore: update settings to pathlib 2025-06-20 10:50:44 -07:00
github-actions[bot]
e1c3124698 Changelog v2.17.1 - GHA (#10229) 2025-06-19 12:40:37 -07:00
shamoon
f2e22e103b Bump version to 2.17.1 2025-06-19 12:07:49 -07:00
github-actions[bot]
dda94f013e New Crowdin translations by GitHub Action (#10228)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-06-19 12:06:52 -07:00
GitHub Actions
caf00e7ead Auto translate strings 2025-06-19 18:52:01 +00:00
shamoon
f214440d2e Fix: correct PAPERLESS_EMPTY_TRASH_DIR to Path (#10227) 2025-06-19 11:50:23 -07:00
github-actions[bot]
240c9ac511 Changelog v2.17.0 - GHA (#10225) 2025-06-19 10:43:13 -07:00
300 changed files with 11375 additions and 5425 deletions

View File

@@ -20,7 +20,6 @@
#
# This file is intended only to be used through VSCOde devcontainers. See README.md
# in the folder .devcontainer.
services:
broker:
image: docker.io/library/redis:7

55
.github/DISCUSSION_TEMPLATE/support.yml vendored Normal file
View 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

View File

@@ -1,6 +1,5 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
# Required for uv support for now
enable-beta-ecosystems: true

View File

@@ -4,7 +4,6 @@
# Requires a PAT with the correct scope set in the secrets.
#
# This workflow will not trigger runs on forked repos.
name: Cleanup Image Tags
on:
delete:

View File

@@ -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"

View File

@@ -1,7 +1,6 @@
# This file configures pre-commit hooks.
# See https://pre-commit.com/ for general information
# See https://pre-commit.com/hooks.html for a listing of possible hooks
repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -29,7 +28,7 @@ repos:
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/codespell-project/codespell
rev: v2.4.0
rev: v2.4.1
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
@@ -38,7 +37,7 @@ repos:
- json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.3.3'
rev: 'v3.6.2'
hooks:
- id: prettier
types_or:
@@ -50,17 +49,17 @@ repos:
- 'prettier-plugin-organize-imports@4.1.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
rev: v0.12.2
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.5.1"
rev: "v2.6.0"
hooks:
- id: pyproject-fmt
# Dockerfile hooks
- repo: https://github.com/AleksaC/hadolint-py
rev: v2.12.0.3
rev: v2.12.1b3
hooks:
- id: hadolint
# Shell script hooks
@@ -77,7 +76,7 @@ repos:
hooks:
- id: shellcheck
- repo: https://github.com/google/yamlfmt
rev: v0.14.0
rev: v0.17.2
hooks:
- id: yamlfmt
exclude: "^src-ui/pnpm-lock.yaml"

View File

@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.7.9-python3.12-bookworm-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.7.19-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6

View File

@@ -2,7 +2,6 @@
# and Tika containers for a more end to end test of the Tika related functionality
# Can be used locally or by the CI to start the necessary containers with the
# correct networking for the tests
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.20

View File

@@ -25,11 +25,9 @@
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -24,7 +24,6 @@
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -25,7 +25,6 @@
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -28,7 +28,6 @@
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -24,7 +24,6 @@
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -28,7 +28,6 @@
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -21,7 +21,6 @@
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8

View File

@@ -11,6 +11,7 @@ for command in decrypt_documents \
mail_fetcher \
document_create_classifier \
document_index \
document_llmindex \
document_renamer \
document_retagger \
document_thumbnails \

View 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

View File

@@ -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

View File

@@ -1,5 +1,64 @@
# Changelog
## paperless-ngx 2.17.1
### Bug Fixes
- Fix: correct PAPERLESS_EMPTY_TRASH_DIR to Path [@shamoon](https://github.com/shamoon) ([#10227](https://github.com/paperless-ngx/paperless-ngx/pull/10227))
### All App Changes
- Fix: correct PAPERLESS_EMPTY_TRASH_DIR to Path [@shamoon](https://github.com/shamoon) ([#10227](https://github.com/paperless-ngx/paperless-ngx/pull/10227))
## paperless-ngx 2.17.0
### Breaking Changes
- Fix: restore expected pre-2.16 scheduled workflow offset behavior [@shamoon](https://github.com/shamoon) ([#10218](https://github.com/paperless-ngx/paperless-ngx/pull/10218))
### Features / Enhancements
- QoL: log version at startup, show backend vs frontend mismatch in system status [@shamoon](https://github.com/shamoon) ([#10214](https://github.com/paperless-ngx/paperless-ngx/pull/10214))
- Feature: add Persian translation [@shamoon](https://github.com/shamoon) ([#10183](https://github.com/paperless-ngx/paperless-ngx/pull/10183))
- Enhancement: support import of zipped export [@kaerbr](https://github.com/kaerbr) ([#10073](https://github.com/paperless-ngx/paperless-ngx/pull/10073))
### Bug Fixes
- Fix: more api fixes [@shamoon](https://github.com/shamoon) ([#10204](https://github.com/paperless-ngx/paperless-ngx/pull/10204))
- Fix: restore expected pre-2.16 scheduled workflow offset behavior [@shamoon](https://github.com/shamoon) ([#10218](https://github.com/paperless-ngx/paperless-ngx/pull/10218))
- Fix: fix some API crashes [@shamoon](https://github.com/shamoon) ([#10196](https://github.com/paperless-ngx/paperless-ngx/pull/10196))
- Fix: remove duplicate base path in websocket urls [@robertmx](https://github.com/robertmx) ([#10194](https://github.com/paperless-ngx/paperless-ngx/pull/10194))
- Fix: use hard delete for custom fields with workflow removal [@shamoon](https://github.com/shamoon) ([#10191](https://github.com/paperless-ngx/paperless-ngx/pull/10191))
- Fix: fix mail account test api schema [@shamoon](https://github.com/shamoon) ([#10164](https://github.com/paperless-ngx/paperless-ngx/pull/10164))
- Fix: correct api schema for mail_account process [@shamoon](https://github.com/shamoon) ([#10157](https://github.com/paperless-ngx/paperless-ngx/pull/10157))
- Fix: correct api schema for next_asn [@shamoon](https://github.com/shamoon) ([#10151](https://github.com/paperless-ngx/paperless-ngx/pull/10151))
- Fix: fix email and notes endpoints api spec [@shamoon](https://github.com/shamoon) ([#10148](https://github.com/paperless-ngx/paperless-ngx/pull/10148))
### Dependencies
- Chore: bump angular/common to 19.12.14 [@shamoon](https://github.com/shamoon) ([#10212](https://github.com/paperless-ngx/paperless-ngx/pull/10212))
### All App Changes
<details>
<summary>14 changes</summary>
- QoL: log version at startup, show backend vs frontend mismatch in system status [@shamoon](https://github.com/shamoon) ([#10214](https://github.com/paperless-ngx/paperless-ngx/pull/10214))
- Fix: more api fixes [@shamoon](https://github.com/shamoon) ([#10204](https://github.com/paperless-ngx/paperless-ngx/pull/10204))
- Fix: restore expected pre-2.16 scheduled workflow offset behavior [@shamoon](https://github.com/shamoon) ([#10218](https://github.com/paperless-ngx/paperless-ngx/pull/10218))
- Chore: switch from os.path to pathlib.Path [@gothicVI](https://github.com/gothicVI) ([#9933](https://github.com/paperless-ngx/paperless-ngx/pull/9933))
- Chore: bump angular/common to 19.12.14 [@shamoon](https://github.com/shamoon) ([#10212](https://github.com/paperless-ngx/paperless-ngx/pull/10212))
- Fix: fix some API crashes [@shamoon](https://github.com/shamoon) ([#10196](https://github.com/paperless-ngx/paperless-ngx/pull/10196))
- Fix: remove duplicate base path in websocket urls [@robertmx](https://github.com/robertmx) ([#10194](https://github.com/paperless-ngx/paperless-ngx/pull/10194))
- Fix: use hard delete for custom fields with workflow removal [@shamoon](https://github.com/shamoon) ([#10191](https://github.com/paperless-ngx/paperless-ngx/pull/10191))
- Feature: add Persian translation [@shamoon](https://github.com/shamoon) ([#10183](https://github.com/paperless-ngx/paperless-ngx/pull/10183))
- Enhancement: support import of zipped export [@kaerbr](https://github.com/kaerbr) ([#10073](https://github.com/paperless-ngx/paperless-ngx/pull/10073))
- Fix: fix mail account test api schema [@shamoon](https://github.com/shamoon) ([#10164](https://github.com/paperless-ngx/paperless-ngx/pull/10164))
- Fix: correct api schema for mail_account process [@shamoon](https://github.com/shamoon) ([#10157](https://github.com/paperless-ngx/paperless-ngx/pull/10157))
- Fix: correct api schema for next_asn [@shamoon](https://github.com/shamoon) ([#10151](https://github.com/paperless-ngx/paperless-ngx/pull/10151))
- Fix: fix email and notes endpoints api spec [@shamoon](https://github.com/shamoon) ([#10148](https://github.com/paperless-ngx/paperless-ngx/pull/10148))
</details>
## paperless-ngx 2.16.3
### Features / Enhancements
@@ -5948,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

View File

@@ -159,6 +159,41 @@ Available options are `postgresql` and `mariadb`.
Defaults to unset, which uses Djangos 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.

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "2.17.0"
version = "2.17.1"
description = "A community-supported supercharged version of paperless: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.10"
@@ -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,17 +40,25 @@ 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",
"httpx-oauth~=0.16",
"imap-tools~=1.10.0",
"imap-tools~=1.11.0",
"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",
"pathvalidate~=3.2.3",
"openai>=1.76",
"pathvalidate~=3.3.1",
"pdf2image~=1.17.0",
"python-dateutil~=2.9.0",
"python-dotenv~=1.1.0",
@@ -59,7 +68,8 @@ dependencies = [
"pyzbar~=0.1.9",
"rapidfuzz~=3.13.0",
"redis[hiredis]~=5.2.1",
"scikit-learn~=1.6.1",
"scikit-learn~=1.7.0",
"sentence-transformers>=4.1",
"setproctitle~=1.3.4",
"tika-client~=0.9.0",
"tqdm~=4.67.1",
@@ -73,12 +83,12 @@ optional-dependencies.mariadb = [
"mysqlclient~=2.2.7",
]
optional-dependencies.postgres = [
"psycopg[c]==3.2.5",
"psycopg[c]==3.2.9",
# Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.2.5",
"psycopg-c==3.2.9",
]
optional-dependencies.webserver = [
"granian[uvloop]~=2.3.2",
"granian[uvloop]~=2.4.1",
]
[dependency-groups]
@@ -112,7 +122,7 @@ testing = [
lint = [
"pre-commit~=4.1.0",
"pre-commit-uv~=4.1.3",
"ruff~=0.9.9",
"ruff~=0.12.2",
]
typing = [
@@ -172,6 +182,7 @@ lint.extend-select = [
]
lint.ignore = [
"DJ001",
"PLC0415",
"RUF012",
"SIM105",
]
@@ -221,9 +232,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 +248,7 @@ testpaths = [
"src/paperless_mail/tests/",
"src/paperless_tesseract/tests/",
"src/paperless_tika/tests",
"src/paperless_ai/tests",
]
addopts = [
"--pythonwarnings=all",
@@ -303,8 +312,8 @@ environments = [
[tool.uv.sources]
# Markers are chosen to select these almost exclusively when building the Docker image
psycopg-c = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
]
zxing-cpp = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },

View File

@@ -48,6 +48,7 @@
"sv-SE": "src/locale/messages.sv_SE.xlf",
"tr-TR": "src/locale/messages.tr_TR.xlf",
"uk-UA": "src/locale/messages.uk_UA.xlf",
"vi-VN": "src/locale/messages.vi_VN.xlf",
"zh-CN": "src/locale/messages.zh_CN.xlf",
"zh-TW": "src/locale/messages.zh_TW.xlf"
}
@@ -60,10 +61,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 +89,15 @@
"file-saver",
"utif"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
"namedChunks": true,
"stylePreprocessorOptions": {
"includePaths": [
"."
]
}
},
"configurations": {
"production": {
@@ -107,8 +113,6 @@
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
@@ -188,6 +192,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

View File

@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "2.17.0",
"version": "2.17.1",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeUk from '@angular/common/locales/uk'
import localeVi from '@angular/common/locales/vi'
import localeZh from '@angular/common/locales/zh'
import localeZhHant from '@angular/common/locales/zh-Hant'
@@ -75,6 +76,7 @@ registerLocaleData(localeSr)
registerLocaleData(localeSv)
registerLocaleData(localeTr)
registerLocaleData(localeUk)
registerLocaleData(localeVi)
registerLocaleData(localeZh)
registerLocaleData(localeZhHant)

View File

@@ -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()

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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[] = []

View File

@@ -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))

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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 })

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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

View File

@@ -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>
}

View File

@@ -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

View 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>

View 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;
}
}

View 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()
})
})

View 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()
}
}
}

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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) => {

View File

@@ -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">&nbsp;<ng-container i18n>Custom Fields</ng-container></div>
<div class="d-none d-lg-inline">&nbsp;<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)">

View File

@@ -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()
}

View File

@@ -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
@@ -243,9 +246,9 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
customFields: CustomField[] = []
public readonly today: string = new Date().toISOString().split('T')[0]
public readonly today: string = new Date().toLocaleDateString('en-CA')
constructor(protected customFieldsService: CustomFieldsService) {
constructor() {
super()
this.selectionModel = new CustomFieldQueriesModel()
this.getFields()

View File

@@ -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()
}
@@ -162,7 +165,7 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
@Input()
placement: string = 'bottom-start'
public readonly today: string = new Date().toISOString().split('T')[0]
public readonly today: string = new Date().toLocaleDateString('en-CA')
get isActive(): boolean {
return (

View File

@@ -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>
}

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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> {

View File

@@ -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[]

View File

@@ -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() {
@@ -46,7 +43,7 @@ export class GroupEditDialogComponent extends EditDialogComponent<Group> {
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(''),
permissions: new FormControl(null),
permissions: new FormControl([]),
})
}
}

View File

@@ -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() {

View File

@@ -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))

View File

@@ -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>
}

View File

@@ -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()
}

View File

@@ -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>
}

View File

@@ -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() {

View File

@@ -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))

View File

@@ -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) => {

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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[]
@@ -62,7 +59,7 @@ export class DateComponent
@Output()
filterDocuments = new EventEmitter<NgbDateStruct[]>()
public readonly today: string = new Date().toISOString().split('T')[0]
public readonly today: string = new Date().toLocaleDateString('en-CA')
getSuggestions() {
return this.suggestions == null

View File

@@ -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()
}

View File

@@ -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')
})
})

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,17 +1,24 @@
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<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) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<i-bs name="eye"></i-bs>
</button>
<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) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<i-bs name="eye"></i-bs>
</button>
}
</div>
<div class="invalid-feedback">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div>
<div class="invalid-feedback">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div>

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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>&nbsp;
<a (click)="applySuggestion(s)" [routerLink]="[]">{{getSuggestion()}}</a>&nbsp;
</small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>

View File

@@ -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')
})
})

View File

@@ -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)
}
}

View File

@@ -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(' ')
}

View File

@@ -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 = ''

View File

@@ -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))
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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()
}

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
.suggestions-dropdown {
min-width: 250px;
}

Some files were not shown because too many files have changed in this diff Show More