Compare commits

..

27 Commits
logo-v3 ... dev

Author SHA1 Message Date
shamoon
1c2d5483c2 Chore: set fetch depth for bundle analysis (#12257) 2026-03-05 23:54:05 -08:00
shamoon
815e598218 Chore: update ESLint to v10 (#12256) 2026-03-05 22:59:47 -08:00
dependabot[bot]
a5a267fe49 Bump django-allauth from 65.14.0 to 65.14.1 (#12253)
Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.14.0 to 65.14.1.
- [Commits](https://github.com/sponsors/pennersr/commits)

---
updated-dependencies:
- dependency-name: django-allauth
  dependency-version: 65.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 14:32:04 -08:00
shamoon
24a2cfd957 Change: use explicit doc creation instead of clone for versions (#12226) 2026-03-04 15:57:44 -08:00
GitHub Actions
7cf2ef6398 Auto translate strings 2026-03-04 23:29:54 +00:00
shamoon
df03207eef Fix: correct doc version filename handling (#12223) 2026-03-04 23:28:07 +00:00
dependabot[bot]
fa998ecd49 Bump django from 5.2.11 to 5.2.12 (#12249)
Bumps [django](https://github.com/django/django) from 5.2.11 to 5.2.12.
- [Commits](https://github.com/django/django/compare/5.2.11...5.2.12)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.12
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 15:16:25 -08:00
Trenton H
1e21bcd26e Breaking: Drop support for Python 3.10 (#12234) 2026-03-04 15:03:33 -08:00
Trenton H
a9cb89c633 Enhancement: Improve exporter memory efficiency (#12236)
Phase 1 -- Eliminate JSON round-trip in document exporter

Replace json.loads(serializers.serialize("json", qs)) with
serializers.serialize("python", qs) to skip the intermediate
JSON string allocation and parse step. Use DjangoJSONEncoder
in check_and_write_json() to handle native Python types
(datetime, Decimal, UUID) the Python serializer returns.

Phase 2 -- Batched QuerySet serialization in document exporter

Add serialize_queryset_batched() helper that uses QuerySet.iterator()
and itertools.islice to stream records in configurable chunks, bounding
peak memory during serialization to batch_size * avg_record_size rather
than loading the entire QuerySet at once.
2026-03-04 14:54:20 -08:00
GitHub Actions
a37e24c1ad Auto translate strings 2026-03-04 22:17:32 +00:00
shamoon
85a18e5911 Enhancement: saved view sharing (#12142) 2026-03-04 14:15:43 -08:00
GitHub Actions
ae182c459b Auto translate strings 2026-03-04 21:34:02 +00:00
shamoon
d51a118aac Merge branch 'main' into dev 2026-03-04 13:31:20 -08:00
github-actions[bot]
d6a316b1df Changelog v2.20.10 - GHA (#12247)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-04 11:25:44 -08:00
shamoon
8f311c4b6b Bump version to 2.20.10 2026-03-04 10:38:14 -08:00
shamoon
f25322600d Merge branch 'release/v2.20.x' 2026-03-04 10:09:01 -08:00
shamoon
615f27e6fb Fix: support string coercion in filepath jinja templates (#12244) 2026-03-04 08:32:34 -08:00
Andreas Schneider
190fc70288 Fix: use maxsplit=1 in Redis URL parsing to handle URLs with multiple colons (#12239) 2026-03-04 01:06:51 -08:00
shamoon
5b809122b5 Fix: apply ordering after annotating tag document count (#12238) 2026-03-04 00:33:13 -08:00
GitHub Actions
c623234769 Auto translate strings 2026-03-04 00:29:21 +00:00
shamoon
299dac21ee Enhancement: “live” document updates (#12141) 2026-03-04 00:27:07 +00:00
Trenton H
5498503d60 Chore: Improve user migration path (#12232)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-03-03 15:51:48 -08:00
shamoon
8b8307571a Fix: enforce path limit for db filename fields (#12235) 2026-03-03 13:19:56 -08:00
GitHub Actions
16b58c2de5 Auto translate strings 2026-03-03 19:25:03 +00:00
shamoon
c724fbb5d9 Clarify bulk edit wording with versions 2026-03-03 11:22:22 -08:00
dependabot[bot]
9c0f112e94 docker(deps): Bump astral-sh/uv (#12191)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.10.5-python3.12-trixie-slim to 0.10.7-python3.12-trixie-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.10.5...0.10.7)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.10.7-python3.12-trixie-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>
2026-03-03 07:56:35 -08:00
Trenton H
43406f44f2 Feature: Improve the retagger output using rich (#12194) 2026-03-03 07:14:59 -08:00
131 changed files with 6975 additions and 2627 deletions

View File

@@ -14,10 +14,6 @@ component_management:
# https://docs.codecov.com/docs/carryforward-flags # https://docs.codecov.com/docs/carryforward-flags
flags: flags:
# Backend Python versions # Backend Python versions
backend-python-3.10:
paths:
- src/**
carryforward: true
backend-python-3.11: backend-python-3.11:
paths: paths:
- src/** - src/**
@@ -26,6 +22,14 @@ flags:
paths: paths:
- src/** - src/**
carryforward: true carryforward: true
backend-python-3.13:
paths:
- src/**
carryforward: true
backend-python-3.14:
paths:
- src/**
carryforward: true
# Frontend (shards merge into single flag) # Frontend (shards merge into single flag)
frontend-node-24.x: frontend-node-24.x:
paths: paths:
@@ -41,9 +45,10 @@ coverage:
project: project:
backend: backend:
flags: flags:
- backend-python-3.10
- backend-python-3.11 - backend-python-3.11
- backend-python-3.12 - backend-python-3.12
- backend-python-3.13
- backend-python-3.14
paths: paths:
- src/** - src/**
# https://docs.codecov.com/docs/commit-status#threshold # https://docs.codecov.com/docs/commit-status#threshold
@@ -59,9 +64,10 @@ coverage:
patch: patch:
backend: backend:
flags: flags:
- backend-python-3.10
- backend-python-3.11 - backend-python-3.11
- backend-python-3.12 - backend-python-3.12
- backend-python-3.13
- backend-python-3.14
paths: paths:
- src/** - src/**
target: 100% target: 100%

View File

@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
python-version: ['3.10', '3.11', '3.12'] python-version: ['3.11', '3.12', '3.13', '3.14']
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -164,6 +164,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6.0.2 uses: actions/checkout@v6.0.2
with:
fetch-depth: 2
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4.2.0 uses: pnpm/action-setup@v4.2.0
with: with:

View File

@@ -341,6 +341,9 @@ src/documents/migrations/0001_initial.py:0: error: Skipping analyzing "multisele
src/documents/migrations/0001_initial.py:0: error: Skipping analyzing "multiselectfield.db.fields": module is installed, but missing library stubs or py.typed marker [import-untyped] src/documents/migrations/0001_initial.py:0: error: Skipping analyzing "multiselectfield.db.fields": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0012_savedview_visibility_to_ui_settings.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0012_savedview_visibility_to_ui_settings.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0012_savedview_visibility_to_ui_settings.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/models.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type] src/documents/models.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.correspondent'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.correspondent'. [django-manager-missing]
src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.document_type'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.document_type'. [django-manager-missing]
@@ -440,9 +443,6 @@ src/documents/permissions.py:0: error: Item "list[str]" of "Any | list[str] | Qu
src/documents/permissions.py:0: error: Item "list[str]" of "Any | list[str] | QuerySet[User, User]" has no attribute "exists" [union-attr] src/documents/permissions.py:0: error: Item "list[str]" of "Any | list[str] | QuerySet[User, User]" has no attribute "exists" [union-attr]
src/documents/permissions.py:0: error: Missing type parameters for generic type "QuerySet" [type-arg] src/documents/permissions.py:0: error: Missing type parameters for generic type "QuerySet" [type-arg]
src/documents/permissions.py:0: error: Missing type parameters for generic type "dict" [type-arg] src/documents/permissions.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/plugins/helpers.py:0: error: "Collection[str]" has no attribute "update" [attr-defined]
src/documents/plugins/helpers.py:0: error: Argument 1 to "send" of "BaseStatusManager" has incompatible type "dict[str, Collection[str]]"; expected "dict[str, str | int | None]" [arg-type]
src/documents/plugins/helpers.py:0: error: Argument 1 to "send" of "BaseStatusManager" has incompatible type "dict[str, Collection[str]]"; expected "dict[str, str | int | None]" [arg-type]
src/documents/plugins/helpers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/plugins/helpers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/plugins/helpers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/plugins/helpers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/plugins/helpers.py:0: error: Skipping analyzing "channels_redis.pubsub": module is installed, but missing library stubs or py.typed marker [import-untyped] src/documents/plugins/helpers.py:0: error: Skipping analyzing "channels_redis.pubsub": module is installed, but missing library stubs or py.typed marker [import-untyped]
@@ -550,6 +550,7 @@ src/documents/serialisers.py:0: error: Function is missing a type annotation [n
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -640,6 +641,7 @@ src/documents/serialisers.py:0: error: Missing type parameters for generic type
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg] src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg] src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg] src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg] src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg] src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/serialisers.py:0: error: Need type annotation for "document" [var-annotated] src/documents/serialisers.py:0: error: Need type annotation for "document" [var-annotated]
@@ -667,7 +669,6 @@ src/documents/signals/handlers.py:0: error: Argument 3 to "validate_move" has in
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type] src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type] src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type] src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -1175,6 +1176,14 @@ src/documents/tests/test_management_exporter.py:0: error: Skipping analyzing "al
src/documents/tests/test_management_fuzzy.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/tests/test_management_fuzzy.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/tests/test_management_retagger.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_management_superuser.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/tests/test_management_superuser.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Cannot determine type of "apps" [has-type]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Cannot determine type of "apps" [has-type]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment] src/documents/tests/test_migration_share_link_bundle.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
@@ -1534,7 +1543,6 @@ src/documents/views.py:0: error: "get_serializer_context" undefined in superclas
src/documents/views.py:0: error: "object" not callable [operator] src/documents/views.py:0: error: "object" not callable [operator]
src/documents/views.py:0: error: "type[Model]" has no attribute "objects" [attr-defined] src/documents/views.py:0: error: "type[Model]" has no attribute "objects" [attr-defined]
src/documents/views.py:0: error: Argument "path" to "EmailAttachment" has incompatible type "Path | None"; expected "Path" [arg-type] src/documents/views.py:0: error: Argument "path" to "EmailAttachment" has incompatible type "Path | None"; expected "Path" [arg-type]
src/documents/views.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_correspondents" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type] src/documents/views.py:0: error: Argument 2 to "match_correspondents" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_document_types" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type] src/documents/views.py:0: error: Argument 2 to "match_document_types" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_storage_paths" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type] src/documents/views.py:0: error: Argument 2 to "match_storage_paths" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
@@ -1552,7 +1560,6 @@ src/documents/views.py:0: error: Function is missing a return type annotation [
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
@@ -1609,7 +1616,8 @@ src/documents/views.py:0: error: Function is missing a type annotation [no-unty
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Incompatible type for lookup 'owner': (got "User | AnonymousUser", expected "User | int | None") [misc] src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "Any | None", variable has type "dict[Any, Any]") [assignment] src/documents/views.py:0: error: Incompatible types in assignment (expression has type "Any | None", variable has type "dict[Any, Any]") [assignment]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Any]") [assignment] src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Any]") [assignment]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Document]") [assignment] src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Document]") [assignment]
@@ -1675,11 +1683,11 @@ src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[CustomField]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[CustomField]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Group]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[Group]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[MailAccount]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[MailAccount]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[MailRule]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[MailRule]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[SavedView]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[User]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[User]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Workflow]" is not indexable [index] src/documents/views.py:0: error: Value of type "Iterable[Workflow]" is not indexable [index]
src/documents/views.py:0: error: Value of type "dict[str, _PingReply] | None" is not indexable [index] src/documents/views.py:0: error: Value of type "dict[str, _PingReply] | None" is not indexable [index]
@@ -1928,6 +1936,7 @@ src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLaye
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr] src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr] src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr] src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item] src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item]
src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item] src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item]
src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item] src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item]

View File

@@ -13,7 +13,9 @@ If you want to implement something big:
## Python ## Python
Paperless supports python 3.10 - 3.12 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/). Paperless-ngx currently supports Python 3.11, 3.12, 3.13, and 3.14. As a policy, we aim to support at least the three most recent Python versions, and drop support for versions as they reach end-of-life. Older versions may be supported if dependencies permit, but this is not guaranteed.
We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
## Branches ## Branches

View File

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

View File

@@ -7,9 +7,9 @@
<p align="center"> <p align="center">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_white.png" width="50%"> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/resources/logo/web/png/White%20logo%20-%20no%20background.png" width="50%">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_black.png" width="50%"> <source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
<img src="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_black.png" width="50%"> <img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
</picture> </picture>
</p> </p>

View File

@@ -262,6 +262,10 @@ your files differently, you can do that by adjusting the
or using [storage paths (see below)](#storage-paths). Paperless adds the or using [storage paths (see below)](#storage-paths). Paperless adds the
correct file extension e.g. `.pdf`, `.jpg` automatically. correct file extension e.g. `.pdf`, `.jpg` automatically.
When a document has file versions, each version uses the same naming rules and
storage path resolution as any other document file, with an added version suffix
such as `_v1`, `_v2`, etc.
This variable allows you to configure the filename (folders are allowed) This variable allows you to configure the filename (folders are allowed)
using placeholders. For example, configuring this to using placeholders. For example, configuring this to
@@ -353,6 +357,8 @@ If paperless detects that two documents share the same filename,
paperless will automatically append `_01`, `_02`, etc to the filename. paperless will automatically append `_01`, `_02`, etc to the filename.
This happens if all the placeholders in a filename evaluate to the same This happens if all the placeholders in a filename evaluate to the same
value. value.
For versioned files, this counter is appended after the version suffix
(for example `statement_v2_01.pdf`).
If there are any errors in the placeholders included in `PAPERLESS_FILENAME_FORMAT`, If there are any errors in the placeholders included in `PAPERLESS_FILENAME_FORMAT`,
paperless will fall back to using the default naming scheme instead. paperless will fall back to using the default naming scheme instead.

View File

@@ -466,3 +466,8 @@ Initial API version.
- The document `created` field is now a date, not a datetime. The - The document `created` field is now a date, not a datetime. The
`created_date` field is considered deprecated and will be removed in a `created_date` field is considered deprecated and will be removed in a
future version. future version.
#### Version 10
- The `show_on_dashboard` and `show_in_sidebar` fields of saved views have been
removed. Relevant settings are now stored in the UISettings model.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 B

After

Width:  |  Height:  |  Size: 768 B

12
docs/assets/logo.svg Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M299,891.7c-4.2-19.8-12.5-59.6-13.6-59.6c-176.7-105.7-155.8-288.7-97.3-393.4
c12.5,131.8,245.8,222.8,109.8,383.9c-1.1,2,6.2,27.2,12.5,50.2c27.2-46,68-101.4,65.8-106.7C208.9,358.2,731.9,326.9,840.6,73.7
c49.1,244.8-25.1,623.5-445.5,719.7c-2,1.1-76.3,131.8-79.5,132.9c0-2-31.4-1.1-27.2-11.5C290.7,908.4,294.8,900.1,299,891.7
L299,891.7z M293.8,793.4c53.3-61.8-9.4-167.4-47.1-201.9C310.5,701.3,306.3,765.1,293.8,793.4L293.8,793.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,19 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860"> <!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/> <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
<g id="text" style="fill: #000;"> viewBox="0 0 2962.2 860.2" style="enable-background:new 0 0 2962.2 860.2;" xml:space="preserve">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/> <style type="text/css">
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/> .st0{fill:#17541F;stroke:#000000;stroke-miterlimit:10;}
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/> </style>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/> <path d="M1055.6,639.7v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/> c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/> h85.3v249.6L1055.6,639.7L1055.6,639.7z M1059.1,514.9c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/> c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/> c16.8,0,30.3-5.9,40.6-17.7C1054,546.9,1059.1,532.3,1059.1,514.9z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/> <path d="M1417.8,398.2c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/> c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V390.2h85.3v20.6
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/> c18-20,43.1-30.1,75.4-30.1C1379.2,380.7,1399.5,386.6,1417.8,398.2z M1389.5,514.9c0-17.4-5.1-31.9-15.3-43.8
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/> c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/> c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7S1389.5,532.3,1389.5,514.9z"/>
</g> <path d="M1713.6,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2s37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8c3.6,11.4,10.5,20.7,20.9,28.1
c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1695.8,570.1,1704.9,563.7,1713.6,555.3z M1596.9,486.2h92.9
c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11S1599,473.9,1596.9,486.2z"/>
<path d="M1908.8,418.4c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V390.2h85.3V418.4L1908.8,418.4z"/>
<path d="M2113,258.2v381.5h-85.3V258.2H2113z"/>
<path d="M2360.8,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2343.1,570.1,2352.1,563.7,2360.8,555.3z
M2244.1,486.2h92.9c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11C2251.7,464.1,2246.2,473.9,2244.1,486.2z"/>
<path d="M2565.9,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2590.5,448.7,2577.6,446.3,2565.9,446.3z"/>
<path d="M2817.3,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2841.8,448.7,2828.9,446.3,2817.3,446.3z"/>
<g>
<path d="M2508,724h60.2v17.3H2508V724z"/>
<path d="M2629.2,694.4c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6v-52.6
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2620.2,699.4,2624.4,696.4,2629.2,694.4z"/>
<path d="M2790.3,833.2c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2s-6.6-11.9-7.1-19.6h19.6
c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7c-3.6,3.4-8,6.2-13.3,8.2
c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6s2-17.3,6-24.7
s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2803.2,815.9,2798.9,826.4,2790.3,833.2z M2782.2,755.7c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2776.1,764.1,2779.6,760.4,2782.2,755.7z"/>
<path d="M2843.5,788.4h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2843.5,788.4z
"/>
</g>
<path d="M835.8,319.2c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2H647v-135h72.7c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7c11.5-18.9,17.3-39.5,17.3-61.9
C853.1,358.9,847.4,338.1,835.8,319.2z M747,416.6c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C761.1,396.6,756.4,407.7,747,416.6z"/>
<path class="st0" d="M164.7,698.7c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4C82.8,431.4,277,507.1,163.8,641.2
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C89.7,254.7,525,228.6,615.5,17.9c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C157.8,712.6,161.2,705.7,164.7,698.7L164.7,698.7z M160.4,616.9
c44.4-51.4-7.8-139.3-39.2-168C174.3,540.2,170.8,593.3,160.4,616.9L160.4,616.9z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #eee;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,19 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860"> <!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/> <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
<g id="text" style="fill: #fff;"> viewBox="0 0 2962.2 860.2" style="enable-background:new 0 0 2962.2 860.2;" xml:space="preserve">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/> <style type="text/css">
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/> .st0{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/> .st1{fill:#17541F;stroke:#000000;stroke-miterlimit:10;}
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/> </style>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/> <path class="st0" d="M1055.6,639.7v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/> c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/> h85.3v249.6L1055.6,639.7L1055.6,639.7z M1059.1,514.9c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/> c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/> c16.8,0,30.3-5.9,40.6-17.7C1054,546.9,1059.1,532.3,1059.1,514.9z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/> <path class="st0" d="M1417.8,398.2c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/> c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V390.2h85.3v20.6
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/> c18-20,43.1-30.1,75.4-30.1C1379.2,380.7,1399.5,386.6,1417.8,398.2z M1389.5,514.9c0-17.4-5.1-31.9-15.3-43.8
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/> c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
</g> c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7S1389.5,532.3,1389.5,514.9z"/>
<path class="st0" d="M1713.6,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2s37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8c3.6,11.4,10.5,20.7,20.9,28.1
c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1695.8,570.1,1704.9,563.7,1713.6,555.3z M1596.9,486.2h92.9
c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11S1599,473.9,1596.9,486.2z"/>
<path class="st0" d="M1908.8,418.4c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7
c-8.4-2.1-15.7-3.1-22-3.1c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V390.2h85.3V418.4L1908.8,418.4z"/>
<path class="st0" d="M2113,258.2v381.5h-85.3V258.2H2113z"/>
<path class="st0" d="M2360.8,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2343.1,570.1,2352.1,563.7,2360.8,555.3z
M2244.1,486.2h92.9c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11C2251.7,464.1,2246.2,473.9,2244.1,486.2z"/>
<path class="st0" d="M2565.9,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2590.5,448.7,2577.6,446.3,2565.9,446.3z"/>
<path class="st0" d="M2817.3,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2841.8,448.7,2828.9,446.3,2817.3,446.3z"/>
<g>
<path class="st0" d="M2508,724h60.2v17.3H2508V724z"/>
<path class="st0" d="M2629.2,694.4c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6v-52.6
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2620.2,699.4,2624.4,696.4,2629.2,694.4z"/>
<path class="st0" d="M2790.3,833.2c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2s-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2803.2,815.9,2798.9,826.4,2790.3,833.2z M2782.2,755.7c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2776.1,764.1,2779.6,760.4,2782.2,755.7z"/>
<path class="st0" d="M2843.5,788.4h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35
L2843.5,788.4z"/>
</g>
<path class="st0" d="M835.8,319.2c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2H647v-135h72.7c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7c11.5-18.9,17.3-39.5,17.3-61.9
C853.1,358.9,847.4,338.1,835.8,319.2z M747,416.6c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C761.1,396.6,756.4,407.7,747,416.6z"/>
<path class="st1" d="M164.7,698.7c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4C82.8,431.4,277,507.1,163.8,641.2
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C89.7,254.7,525,228.6,615.5,17.9c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C157.8,712.6,161.2,705.7,164.7,698.7L164.7,698.7z M160.4,616.9
c44.4-51.4-7.8-139.3-39.2-168C174.3,540.2,170.8,593.3,160.4,616.9L160.4,616.9z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
<defs>
<style>
.st0 {
fill: #005616;
}
</style>
</defs>
<path class="st0" d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 644 B

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
<defs>
<style>
.st0 {
fill: #fff;
}
</style>
</defs>
<path class="st0" d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 641 B

View File

@@ -1,5 +1,23 @@
# Changelog # Changelog
## paperless-ngx 2.20.10
### Bug Fixes
- Fix: support string coercion in filepath jinja templates [@shamoon](https://github.com/shamoon) ([#12244](https://github.com/paperless-ngx/paperless-ngx/pull/12244))
- Fix: apply ordering after annotating tag document count [@shamoon](https://github.com/shamoon) ([#12238](https://github.com/paperless-ngx/paperless-ngx/pull/12238))
- Fix: enforce path limit for db filename fields [@shamoon](https://github.com/shamoon) ([#12235](https://github.com/paperless-ngx/paperless-ngx/pull/12235))
### All App Changes
<details>
<summary>3 changes</summary>
- Fix: support string coercion in filepath jinja templates [@shamoon](https://github.com/shamoon) ([#12244](https://github.com/paperless-ngx/paperless-ngx/pull/12244))
- Fix: apply ordering after annotating tag document count [@shamoon](https://github.com/shamoon) ([#12238](https://github.com/paperless-ngx/paperless-ngx/pull/12238))
- Fix: enforce path limit for db filename fields [@shamoon](https://github.com/shamoon) ([#12235](https://github.com/paperless-ngx/paperless-ngx/pull/12235))
</details>
## paperless-ngx 2.20.9 ## paperless-ngx 2.20.9
### Security ### Security

View File

@@ -4,7 +4,7 @@ title: Home
<div class="grid-left" markdown> <div class="grid-left" markdown>
![image](assets/logo_full_black.svg#only-light){.index-logo} ![image](assets/logo_full_black.svg#only-light){.index-logo}
![image](assets/logo_full_eee.svg#only-dark){.index-logo} ![image](assets/logo_full_white.svg#only-dark){.index-logo}
**Paperless-ngx** is a _community-supported_ open-source document management system that transforms your **Paperless-ngx** is a _community-supported_ open-source document management system that transforms your
physical documents into a searchable online archive so you can keep, well, _less paper_. physical documents into a searchable online archive so you can keep, well, _less paper_.

View File

@@ -172,7 +172,7 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
#### Prerequisites #### Prerequisites
- Paperless runs on Linux only, Windows is not supported. - Paperless runs on Linux only, Windows is not supported.
- Python 3 is required with versions 3.10 - 3.12 currently supported. Newer versions may work, but some dependencies may not be fully compatible. - Python 3.11, 3.12, 3.13, or 3.14 is required. As a policy, Paperless-ngx aims to support at least the three most recent Python versions and drops support for versions as they reach end-of-life. Newer versions may work, but some dependencies may not be fully compatible.
#### Installation #### Installation

View File

@@ -95,6 +95,7 @@ Think of versions as **file history** for a document.
- Versions track the underlying file and extracted text content (OCR/text). - Versions track the underlying file and extracted text content (OCR/text).
- Metadata such as tags, correspondent, document type, storage path and custom fields stay on the "root" document. - Metadata such as tags, correspondent, document type, storage path and custom fields stay on the "root" document.
- Version files follow normal filename formatting (including storage paths) and add a `_vN` suffix (for example `_v1`, `_v2`).
- By default, search and document content use the latest version. - By default, search and document content use the latest version.
- In document detail, selecting a version switches the preview, file metadata and content (and download etc buttons) to that version. - In document detail, selecting a version switches the preview, file metadata and content (and download etc buttons) to that version.
- Deleting a non-root version keeps metadata and falls back to the latest remaining version. - Deleting a non-root version keeps metadata and falls back to the latest remaining version.

View File

@@ -1,12 +1,11 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "2.20.9" version = "2.20.10"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.11"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
@@ -111,6 +110,7 @@ docs = [
testing = [ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"faker~=40.5.1",
"imagehash", "imagehash",
"pytest~=9.0.0", "pytest~=9.0.0",
"pytest-cov~=7.0.0", "pytest-cov~=7.0.0",
@@ -176,7 +176,7 @@ torch = [
] ]
[tool.ruff] [tool.ruff]
target-version = "py310" target-version = "py311"
line-length = 88 line-length = 88
src = [ src = [
"src", "src",
@@ -309,6 +309,7 @@ markers = [
[tool.pytest_env] [tool.pytest_env]
PAPERLESS_DISABLE_DBHANDLER = "true" PAPERLESS_DISABLE_DBHANDLER = "true"
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache" PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
PAPERLESS_CHANNELS_BACKEND = "channels.layers.InMemoryChannelLayer"
[tool.coverage.report] [tool.coverage.report]
exclude_also = [ exclude_also = [

16
resources/logo.txt Normal file
View File

@@ -0,0 +1,16 @@
9w
{@@N
Q@@@@H
G@@@@@@@\
SilN@@@@@@@
*Q *@@@@@@@@S /= = = = = = = = = = = = = = = = = =\
*@ B@@@@@@@@N || ||
N R$ A@@@@@@@@@@ || PAPERLESS-NGX ||
x@@ $U B@@@@@@@@@R || ||
N@@N^ @ N@@@@@@@@@* \= = = = = = = = = = = = = = = = = =/
|@@@u @ E@@@@@@@@l
Q@@@ \ Px@@@@@@P
1@@S` @@@o'
z$ ;
v
/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,329 @@
%PDF-1.6
%âãÏÓ
3 0 obj
<</Metadata 7 0 R/OCProperties<</D<</ON[8 0 R 22 0 R]/Order 23 0 R/RBGroups[]>>/OCGs[8 0 R 22 0 R]>>/Pages 4 0 R/Type/Catalog>>
endobj
7 0 obj
<</Length 8109/Subtype/XML/Type/Metadata>>stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.1-c000 79.a8731b9, 2021/09/09-00:37:38 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/">
<xmp:CreateDate>2018-12-29T21:47:38Z</xmp:CreateDate>
<xmp:CreatorTool>Chromium</xmp:CreatorTool>
<xmp:ModifyDate>2022-02-26T20:11:14-08:00</xmp:ModifyDate>
<xmp:MetadataDate>2022-02-26T20:11:14-08:00</xmp:MetadataDate>
<xmp:Thumbnails>
<rdf:Alt>
<rdf:li rdf:parseType="Resource">
<xmpGImg:width>256</xmpGImg:width>
<xmpGImg:height>76</xmpGImg:height>
<xmpGImg:format>JPEG</xmpGImg:format>
<xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgATAEAAwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYqlnmXzJ&#xA;pHlvR7jV9VmENpAP9k7n7MaD9pm7DBKQAssZSAFl5x+RvnjUfN2r+br+8+BWmtJLaCtVijZZUVB8&#xA;liFT3O+VYp8RLVhnxEvWsub3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FWG+f/zU8seTLdlvJfrWqMKwaZCw9U1Gxc7iNfdvoByueQRa55BF8t+evzB8&#xA;w+c9T+t6pLxgjJFpYx1EMKn+Ud2PdjufltmJOZlzcOczI7vVP+cVYSbjzJNX4VSzSnuxmP8Axrl2&#xA;n6t2m6voLMlynYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FVk88FvBJPPIsMESl5ZZCFRVUVLMx2AA74q8A/Mv/nIqV2l0ryYeEYqkusuPiPY/V0Ybf67fQB1z&#xA;GyZugcXJn6B4TcXFxczyXFxK808rF5ZZGLuzHclmNSScxnGU8VfTH/OMOlNB5R1LUXFDfXnBPdII&#xA;wAf+CkYZl4Bs5enGz2TL3IdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVYj5X/MGw13zf5i8vRFRJosiJDv8UqgcJ2/55zfD92QjOyQ1xnZIZdk2x8v/np+a1zr2qTe&#xA;XNJmMeh2MhjuHQ0+tTIaMWI6xow+EdD9rwpiZclmhycPNks0OTyPKGh2Kr4YZZ5khhQySysEjjUV&#xA;ZmY0AA8ScVfbnkXy2nlryjpeiinqWkAE5HQzPV5SPnIxzYQjQp2MI0KT3JMnYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXw9oPm/WdE8zx+Y7OWl+szTSg14SCQ1kj&#xA;cd1etD/XNeJEG3XRkQbfUt9+Ytlqv5U6n5q0ZykqWcw9Ov7y3uuPHi3TdGYH3G/fMwzuNhzDkuNh&#xA;8f5guC7FXYq9q/5x5/LaXUNTTzdqUVNPsWP6NRx/e3A29QA/sxdj/N8jmRhhe7kYMdmy+ksynLdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir4a836BP5f8AM2pa&#xA;NOpVrOd40r+1HWsbivZkIYZr5CjTrpRo0y3ytqd6dCuLK3J9DzFYXmn3cP8ANe6dCLi3kUdjJG6R&#xA;e5qcnE7e9nE7e953lTU7FXrv5W/kPquuzQ6r5kiew0RSHS1eqXFyOwpsY4z3Y7n9nryF+PCTuW/H&#xA;hJ3PJ9NWlpbWltFa2sSwW0CiOGGMBURFFAqgdAMywHMAVcVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVdirsVdirsVdirsVdirsVdirsVdiryr86/yjfzbbprOjKq69aJwaI0UXMQ3Ccugdf2SevQ9&#xA;qU5cfFuObRlxcW45vL/ym0y40i+vNc8xwvZaP5Raa5uopk4yPezxCGKDi1DypuPeleuU4xW56NOI&#xA;VueiJ8n/APOP2q+Z7WLXbq6j0XStQLT2tmqNNOsLMSgoxRQCv2TyO29MMcJO6Y4Cd3snk78mvI3l&#xA;Z0uLa0N7qKUIvrykrqw7otAifNVr75fHEA5EcUYs4yxsdirsVdirsVdirsVdirsVdirsVdirsVdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVeRedfy/wBa8z/mjBbyQyxeUJYra81aQlfSnntP&#xA;URUWhrUpIqUNNqnsMolAmXk0TgTLyeuIiIioihUUBVVRQADYAAZe3t4q7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq//2Q==</xmpGImg:image>
</rdf:li>
</rdf:Alt>
</xmp:Thumbnails>
<pdf:Producer>Skia/PDF m64</pdf:Producer>
<xmpTPg:NPages>1</xmpTPg:NPages>
<xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
<xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
<xmpTPg:MaxPageSize rdf:parseType="Resource">
<stDim:w>2409.000000</stDim:w>
<stDim:h>909.000000</stDim:h>
<stDim:unit>Pixels</stDim:unit>
</xmpTPg:MaxPageSize>
<xmpTPg:PlateNames>
<rdf:Seq>
<rdf:li>Cyan</rdf:li>
<rdf:li>Magenta</rdf:li>
<rdf:li>Yellow</rdf:li>
<rdf:li>Black</rdf:li>
</rdf:Seq>
</xmpTPg:PlateNames>
<xmpTPg:SwatchGroups>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<xmpG:groupName>Default Swatch Group</xmpG:groupName>
<xmpG:groupType>0</xmpG:groupType>
</rdf:li>
</rdf:Seq>
</xmpTPg:SwatchGroups>
<xmpMM:InstanceID>uuid:e5f59418-0be8-dd42-a564-bc1f41615750</xmpMM:InstanceID>
<xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
<xmpMM:DocumentID>uuid:c2483dfa-3a53-3149-80a7-6822614a9dee</xmpMM:DocumentID>
<dc:format>application/pdf</dc:format>
<illustrator:CreatorSubTool>Adobe Illustrator</illustrator:CreatorSubTool>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj
4 0 obj
<</Count 1/Kids[5 0 R]/Type/Pages>>
endobj
5 0 obj
<</ArtBox[152.941 154.947 2262.04 755.845]/BleedBox[0.0 0.0 2409.0 909.0]/Contents 24 0 R/CropBox[0.0 0.0 2409.0 909.0]/LastModified(D:20220226201114-08'00')/MediaBox[0 0 2409 909]/Parent 4 0 R/PieceInfo<</Illustrator 25 0 R>>/Resources<</ExtGState<</GS0 26 0 R>>/Properties<</MC0 22 0 R>>>>/Thumb 27 0 R/TrimBox[0.0 0.0 2409.0 909.0]/Type/Page>>
endobj
24 0 obj
<</Filter/FlateDecode/Length 3540>>stream
H‰ì—ÍŽ¹„ïýõU"™ü½Z|2 Ã?ÀÀ»{Øõûþ"ɪîI¼;ö° i²ø“ÌŒˆÌüð·<C3B0>Û‡¿~ ÛŸþüq»ý| ÛcK™ÿvýúå_·nÿ¾ÅMÿ~ùñöá/ÿÛ<>ÿ¹ýÌ0ð/n}ԣƴÙÈG¨£l/Ÿnú¢ÿc:j­ÛnGëeKv¤·=Æ#uÛ,<1E>?/·=v¾o9%—mOý¨ŒòÑjÛvf»ÕsÈâŽ1Òõ½¦£Ç¡Í¹¥moáH9?œÝ& ¿{ض÷xX<78>—ak´rýÞÏokç~š½ŽÞïg¯»÷Óòbx€¿olßïÆëm¡ÝWÌ—ï§õË3WLÏíËüpYÜæŸ8«»butŒûtM´ÂÄØ^ñn9d+²®6;†ž3ÚÑ
Æ—vd³k,—q©õûŠÞ<C5A0>Ò5¶£h>†8ßÕË8JäÐqN«G Yf#Ëá5ÏD“søÇÑkòM<»š<š<>¡=xÅru¯X~œGh)k†¹¾Æ<C2BE>ºûsÝÂ%Íß";
þ’Ñ=\ŽXÚ†<C39A>Ùâý-]&t‡¯<>eã±Yn[ÎÀµ·Ë[k¨Mwžß9¾Qw6öÍ`‹ÖªÇßÇ|m-2çÏÆŠÕˆMÁ»‡wNütûáö÷öÅh툱~“<þ<>ìzæ_á„<5A> Ôô<> ,©_ ¬ãÅžh¸ä<C2B8><C3A4>èÊ<19>ü“ê<ÐOÛ.ú­sŸé§ïôvío ¦Ÿ^öH?=üòÌ3ÿ›¯ò/á<Ÿn \Ü’€Ú‚^áL¹i!“P­F£`<60>Â]°?Ç|Θ¯­‰wœv|'˜']^|_ÌtƒïÌm\8îÔÝã<C39D>ù<»‡'mÈD%–ûŠ*q™vßÕGÑ_34aèöëä}Ó­_˜»¨pnülb‰ î.H æiÚÉÄeû<65>©…r¢öÿ]aDò<44>¶Ç ¾¼%XêÚHŒrdE…ÕÁ*g§ÌÏ×[­„hl_<>
`F,<2C>B|Rä.¹\ȶ%Òf†tX<>ÿú,<2C>tÑ-É<>ÕÇ­ <1E>øk'² X;
u<EFBFBD>à]Ê{#ÔpsD™¬ë‰{!“<>Á"Nr
õ
T;ïˆÁt
ekìiÒdq>Ú8¼}Ž_n+‡<>ø²øwPX´†&üà˜0ÖHÖ‰…%îÊäQ TVÝ¢“¾“qûð@^©¸àå4¡%¸¥Ì¡é]Ië=]3~å^a £ÁÝèçgÄJÆB5<42>ÎÖ0;°¯<C2AF>\É‚).<2E>qf;ÆckÈŽŸév5^©ø±s<C2B1>ˆIâàzŽk÷<6B>³ÑpQÇ8DZ%ËŠÁWx[ -6Ljz||š‚=¦ŠG^ÇúØ<C3BA>-J…†,%yÐò´kÌß<C38C><Jüš4™ªÂÃ@ËrÐÖü¿óþŠy`·«ÞžÐXåyðnÜ.OÅîâpå Ž5œX<C593>íš(诈q2ØÀSÚÊ&wÉ¥1{¼ZÂQ㈭ž~ßϘZ”ÛU¢e…_B~1µ4¤ã( ·á`ÐÎýØŽátóÒ“7FÈcªkA Xû3ޝçÈ#«d³Æà3@Ièr}<7D>V
F “Ôa½9xIéÉëØ¡ZŠaGx°¤9”v`R#þýšÀR³Šz„d79LKžùTôt¥+\7†Ò•
™¢ì VApﮡàcÄ/Ma}I*9ÂãÏ=\EH¬¼³Éæ ]¶k'©»/z‡ãCÂÀèòm%
peJ—”Tpž¢ö´<)iùº ¤ý
JjßIIíû)i{£¤í;)©ýO%Ío•´þ_Jê”äØìB*KHão éP]$™É¨Sù<>¨Tˆ)ÔUgRBÜÎ.“I¿_µj¢À—"¿$d'|í>Zò¥Êüœa½÷T1ÝËGpe>]­¬j.Ì“Œf¥¼ãÓüÏ2þkb]QÁ¾[¡[쎥1Èdî^®-…jTá.É Žà⤂}  qV®Ñå<ú¾œÛ9\Ô“©lë¨Ò9dÕ`7Ëׄô¡)) #b5O‰¨B¬i^Ì+šËtW­³ÔЄÜ(E*^5|„Àj¾<6A>å}^ÖâÃS¥í¢Ê¿z³dòQi*«Eõ><>hDƒ/:~Œj.mf>1µN[x»/T_ œéÏðD”]¡ÏþU.Ò'zÕT« <09>ªÓ•ú<E280A2>D,­WtaŸv¶kBš­žo+¥ç5ì$tóôyNdÏÌî—t5¡ë,ìO®DÖ¼óÌ8äæ­LîzÒÌgÖšš²§Áî4xy ìêw¡fÞKé“VÄÙyrsqè}øÆ®/8ªw*^"H[JùY+ÁmÈœc¸g³=[3@TË!#aâÍC[¬B˜½œ÷‰KER™?ý¾F¸<46>:fr<66><63>ÂOÎ*ϼté±ÍÖ¶™·¶
©‡ƒd2sôpƒý«7óÚ(†I÷<49>½Þ8«9¶)) ¦5Ï„8_³ÔBC¢0;Œb,˜"ir}ØfŽ~´IÙˆã7%­ý!i_”´ü,iù­¤åß«¤…º$­M
_#Q“ ~­¨¥w5%ûwÓ´þžšÖÞ_ÓâïUÓâ¯Ô´þŽÖ…SÒ<53>«'I£³Ä e5-I•vOŽß£0ý¦çJA
%/HÞ“J˜NÙ£fÞÝÐáuN^;¢4•«¯ŽÙOY<‡ÞD9~ð dU¸·žüPþ[Xç™ÙUN<55>I¹†IÎŸÏ éü¨m•°o'L"RÏ Ž ·¨Nѧ~N<4E>¡;nÛÃÅÁôWXBÓ¶Æ™÷Ѫvo±‹ÊøÀ’ïÿôú„ÉYŒŸ <08>XÖî#ìÕI²&x™š"EZò>ŸÁ£A˜Y[Þ I59¸$ÕiŸº0uo¸RÖðǾþ(õI:DÐ{Le<4C><dÇõ¼<1F>м!xÝA=Î<16>àDg5.¯I®Hy?×ðA.Ò ¸(fŒ©å³‚ªæÚàrJ£·²dCÅL
L¿²¢†qJgbI<>cWœÝ%d Éd³zªì:ü;ö¶8¶sæÒ¸ÃbS¢—ܰèg‡êÈ?Ý‘],g'&Sò˜©>Î ]Fw°$ݵLßWÊÖK³ûÒgÚ¤ÈX`ÓÁ«ID"ÃEJ'õÓquô[Ê+¶<>PÛIΙäB³’%ìkoýä¢ÉÀê<C380>ã¿8Ø<ch¦Q}ñ
ÍGáARýÀÛ<C380>Á WªEì†ÊT3ˆÓhó”c2uõL_dd÷<64>½ÍqQ‰A,Zð¤©|žìšyõ¸ôu[Ô-ØÕ”ˆ¹ÍñÃRóŽ”ÏÐèíêlcñØ%2ìÊžå…mó±E÷ºÐôV­H3Êžé,g~R­<52>†#¼Z<C2BC>Ù>OîÅ!:;ÈP„n
I æ&øzszV7¡ „»¢3¦”ÈXŽªÅY>F=:ûê˜ecô ¬ÔV^Òilc=Þ$<24>^É$¯Kl•RjþdI€ÇäNÉôòL‰JY˜š¤TYÀÅÝqL<71>U¥Žaæzê¢HŽ$ô­ú(D/TÛ¯± î¿d—=rÛ0F{ŸÂÐ  YgRú*â"irÿ"ï-@HŠ{R$±ûí÷ã„6§<04>©á¯g3[ý­~¢»i<<3C>o¯ÀƒÎfû5Ë2<C38B>66¢M‘³)t+
b÷ó¸ÚbJRT8kÝÊÛÚ®éÝb£Ø¦©«vAaÀùuôÞiVË&1Vµé9cD<63>åJÞ {µ<>5pÁÇk‡Uâ:/~¬fŠàh½¸k6y]gÓó¯™eM†~«=<Ç0>ñcí̵{4 UoÞüýßqz?5[ø§.í ²užIà&)<0F>ýTù¹ êTçR¹8µ/oð{í4†aý?ÁÏå8FŒRÏ<52>ù>0%Ý'ó<>2pÿÞJ{€3J/nV†˜æšr7P?H™öA«ßÝNœº»Pþ¹ª‡Ïîm´€Ž<E282AC>"Fàr<72>§ÇOØdš«e»겆Aš…¾Öf=*V;J<}ç]˘ä8x3ÊXá¼E®œÔÀÊ:G8ûhÐñ`×¼©&gT<ÕŸÖV7öÙ…}pËÛµH-Z3?þy~ØÖÆ51¨EjWeœ÷œ<C3B7>ìY63Ó»YSn£Ö³ä·i3ï¼?Bþ›:[@Å9L7¡u ´ÖÝ
̵^CA†I|T“0Êçk'Çœ&ã*Tü)þß_ϳËs<C38B>æÒójoNÙ℃Íá
ú.¸(ç¤ê<C2A4>åy(Å@™7mÚ̘ʶPuÚÚªÚ­œ”P°¶<05>åJçUÒÙÃgéˆ^À’}æÉt9jËjHÉ5t]î!:>H|H>@‰êDë8p€s:¿â‰ÜlSÀ¬Ì
 ÷¨/^6“p=ëR [<1E>B°Ú ëIžf<C5BE>ŽÝ-Љµ+‡®ªÆ\q²9EÂÊ$NÅ“ƒ:=á
¸Çy§Ñ±œÝVö<EFBFBD>$8 (fóÉ~†/ç{²'<RdN87¸.O9ƒÛºû¶ÅWÜvås˜ÙvÁÖüS ²^ÚxŸÔùóëÇ'ÿþ 0B¸|
endstream
endobj
27 0 obj
<</BitsPerComponent 8/ColorSpace 28 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 39/Length 181/Width 105>>stream
8;Z]!\Ij?G$j8@t/k5dUU3<am*`9`037'CAm[R>[,!)9)1.3+''h?`o>gmB[ET8ck
@lE>-DRC'5>'BJ23HlQilI&Ga&7If\2VcDkpR`P&Ag+(rGsf,$]V4,Oi!oYPO?6G/
Ye+**Y]%)+Lu]7C/1+obTM<jR!k7bhp#"6_FO&2i!:ndgO8~>
endstream
endobj
28 0 obj
[/Indexed/DeviceRGB 255 29 0 R]
endobj
29 0 obj
<</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
endstream
endobj
22 0 obj
<</Intent 30 0 R/Name(Layer 1)/Type/OCG/Usage 31 0 R>>
endobj
30 0 obj
[/View/Design]
endobj
31 0 obj
<</CreatorInfo<</Creator(Adobe Illustrator 26.0)/Subtype/Artwork>>>>
endobj
26 0 obj
<</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>>
endobj
25 0 obj
<</LastModified(D:20220226201114-08'00')/Private 32 0 R>>
endobj
32 0 obj
<</AIMetaData 33 0 R/AIPDFPrivateData1 34 0 R/ContainerVersion 12/CreatorVersion 26/RoundtripStreamType 2/RoundtripVersion 26>>
endobj
33 0 obj
<</Length 1444>>stream
%!PS-Adobe-3.0
%%Creator: Adobe Illustrator(R) 24.0
%%AI8_CreatorVersion: 26.0.3
%%For: (Michael Shamoon) ()
%%Title: (White logo - no background.pdf)
%%CreationDate: 2/26/22 8:11 PM
%%Canvassize: 16383
%%BoundingBox: 152 154 2263 756
%%HiResBoundingBox: 152.941359391029 154.946950299891 2262.04187549133 755.845102922764
%%DocumentProcessColors: Cyan Magenta Yellow Black
%AI5_FileFormat 14.0
%AI12_BuildNumber: 778
%AI3_ColorUsage: Color
%AI7_ImageSettings: 0
%%RGBProcessColor: 0 0 0 ([Registration])
%AI3_Cropmarks: 0 0 2409 909
%AI3_TemplateBox: 1203.5 454.5 1203.5 454.5
%AI3_TileBox: 826.5 166.5 1560.5 742.5
%AI3_DocumentPreview: None
%AI5_ArtSize: 14400 14400
%AI5_RulerUnits: 6
%AI24_LargeCanvasScale: 1
%AI9_ColorModel: 1
%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
%AI5_TargetResolution: 800
%AI5_NumLayers: 1
%AI17_Begin_Content_if_version_gt:24 4
%AI10_OpenToVie: -2651 3020 0.25059563884769 0 7787.44597860344 8164.54751330906 2548 1389 18 0 0 6 45 0 0 0 1 1 0 1 1 0 1
%AI17_Alternate_Content
%AI9_OpenToView: -2651 3020 0.25059563884769 2548 1389 18 0 0 6 45 0 0 0 1 1 0 1 1 0 1
%AI17_End_Versioned_Content
%AI5_OpenViewLayers: 7
%AI17_Begin_Content_if_version_gt:24 4
%AI17_Alternate_Content
%AI17_End_Versioned_Content
%%PageOrigin:704 -46
%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142
%AI9_Flatten: 1
%AI12_CMSettings: 00.MS
%%EndComments
endstream
endobj
34 0 obj
<</Length 33953>>stream
%AI24_ZStandard_Data(µ/ýX<ž¿EZ-<2D>D¤™æ£Ù¥¶[ã*DÞJï§ënYÉwzOê°@Yþ=
)œàr<C3A0>²
}€h>@>@$F"ôãr<C3A3>.)™<>
Kˆ<C38B>j…Ãq
E`ð è‡YXZª>Tü!ò¹T Õ¸A×À”Ÿ<0E>Qìt€h4îRØdBFs2Ê<32>š<EFBFBD>´µdˆ:-¢Hæd”
â€àd”ÙÅ<18>ŠKÑ™‡c4(Vš¦X‰b'S6 
“y8p+cÁa3á.±€_&ÔAõ!êl&\âeÑñáÀºPÀêpM´ ,0O´ˆbǵ@U»Œˆ:‰•Pm<>2ŸƒMK¢€Dv´X$+Q@ƒI±H"H¦”( Š
£°:­Ïš€@ìHE;ٕΤ¡€G*ŠˆŒÃf4À±ŒÒÙŒv¤À*<01>X—N«³Ù„É,¤€¸¤¤Èô¡ð0!i™],
 ±A"˜ˆ"+2‰b'ƒÒpâ`"KDÂŽ
H*2‰@´A—ÖÀ à>)¹É® DË¢d¦€x!Ù)“`Ñp°/&‘¥³ ±TuY”•f µHÊñhiùÀRAl²ËDÔÙp@#j@!âUÂ5€…¥eD‰AK†¨³É®—¡(„t6ê°‰ø‰0Àˆ:­<C2AD>ÎFÃ@s"Rél"² %Z%„¨\2xT2 )‰<>]«ÑÆAe<41>±z„ <0B>
c“ñ’‘Àép° m…“]Bl<42>F*‹„,¢ÓÙhˆX$š] ¢Ø!e'£ÌðÅÎAFgb© ²kJÉ“e>¤•AFg£±±9HP<48>}è0eVˆ
ê%ÔÙì«XVo (|6Ù•¡©i†ìZHX Ð0aàTÁHDà"j!mZÑ* ::T¡ÜÐ+¤·-»F^ž ÁÛha°qzò²d D + =L```Š
¢Vv<56><76>,NÁã˜B !­ÂȣİÉ.…Œ†ƒØ24 +bÀƒ ”†‡ÀB@J;ÛöÒÀɃ´à@­<˜ ³É.æhN®8¼³ÙPp >Ÿƒø8" Ž<C2A0><C5BD>:BŸƒç!SL§a£NüQ:%hćà“]Á­j= Xx(¾¤œ»Iˆ“+Íã¨ìCàÀ”<>ÆFBc<ˆ88 ¬xD'Ü'%³Ë<C2B3>¥Ñ±9Cf¡¡!"G¾ )XŒGv"» 4 O„ˆ„Æ ‚%UÊ@:½™3ht6YI'ÄÒ’]ûÁ2Ze´
|IvÒ°<19>]J6`úHqL x, hJ`8m<¢ËA
ËCÀÈ`G(¥  ”ŠuHm*J"ë)!­Àì:Ù@X8² ‘ñ YXZv˜(|ïµÙgc€!x ®d×Kv2Ê•<C38A>ÕYP•xŒü™<C3BC>´ì8ÑTÙ5NcÁb¡€³0*ͳm™B!m[F@vQ|
øä¢W +®@¡o`İi}$GsÓñ`mÙ•€ ¡!«!@ÁlZ
*Ö¶5˜²ÃdB
\
˜¼lÙ•ÀÑœô, œdÅÈ(Ó9XR…ˆf5Œbò&/Ë<C38B>)#H0Ù5 p'ZNéÙpd[JAÂ$³aÄhY¸P@±HŽ„LR »$X0HHH4JL":
‰Ñ™|¼<>‰ÕGƒ€‡<E282AC>V)"€r¢±Â¡:øÈhD\h#Š<01> G*È®d<0E>86.¶ .ÙI,T2±<Á#‰¨ÈÕ«´ÑTá+R€Hˆe$b¤¡/F<>ÅqVv…|F;ð¤hàã$A”¤<E2809D>þX¤X2Ç⸠I(³ c[6
^%4p1”!!-Ò 4+ž]Ò9`%T¬€tÉg°6 ¬MƒÂydq\v]t(”pTL Ù@ù ÙH² %úŽÁƒƒŒNÁɦjS´"¨áhNNlidó`1l[vYtÅÄA…Š@”'ŒlaÈ.
ˆŽPŽ„tLa€O€C†rd£%SàN^¶ìZ©
XX<Y쀬-»V<C2BB>´ñP>-ídca
=-»VÞFCJ
L‰°°AV†£9iÒ<>l ,†mcU œ„¼¬ÕÙe€u6/°‚] <20>l+o£á¥xNž@½m¬Š
'V%m Ù?Œô䃢-<02> ΀ÍSÒ­E,<¤•H7 Tüa†¢a“]ï´ Yu "P<Z `;(w6+Pˆ<50>É£ÀqU ’…ì
eóÀñ=X´\„pJ,`”ÞÈcñbqñqض<C398>f2ÊÎ †CŸ&"RrB"ãaBÂ…å<E280A6>AQ´¬€0­çUœ<>Œ‡÷¼ @Œô‚]IE,X$]¬Žòè(Áð`FD)Ÿ-$5a<35>&wAC@c|2NFÙ2á<32><C3A1>BiãJpx x޽8<C2BD> °²qe >òŒ<$r‰@AùKh™)€±á
©Y2Eàqp2NNüÐâ!Ò%Œdp"-=¼Ä-Ä<02>ª€Y<E282AC>]šÆSïAPÒæ„@%Aòá0hÑ@8¨ + P´È Y Ââ<lh"D'qä,Žcygã0q2J<32>SJÅ3á™Ë‚Ê&#Y)œˆ ø`¸ðd×XŒŠ‡<15><>ÅËzH+Âce2;!™"5lŒXD1Ñêp`
Ðk±á$·V6
'9(?ñÎ&»0ͤ*i ‡[À®V´U CêTaJ°VJ;™HèÓ
<EFBFBD>22ÃiEúDè§”S+¥™$L)ÕGâÓ"¡¨¶’'UR'Õ©EÚh¥T¡ÆByáxyj±4ОP¬T)%‘Á²©ÐÚ<-ª-ô°l$MkUJeÑZj[)©R*HhÒ&z¤
M©N
õRO
…:R(-/+Z´i)Mtú¨P<š
©E"hÒ&ÚÕP§ŽçKʨX­H[Çê´•XÈ¥ J±¤^0Ö)E…i¬ EJ<45>R‹„­”^XXé„Z­HXëE5N˜V"¡V˜ª…™4-…½¨ØTX K<>H**©SëdÒ°”
KP+ìE5€Æby`.ÊH0 ÓP§,EÂTR'H,,¥­ca˜¶Ò´<C382>„i,m¥#9À°”
3i,„Jh*¤ (£UÂíô<C3AD>¢
h±H!ˆM…ÊÓÖ±qªØH¦©Åae¬Jx´µy>ÊbÙH*ÚF‡I”¡*áùd<EU@ÓP%Hc½h(”ª”:m(ͤj1­VÕb¡Z¬Õ
Õbm«•Ém+Õß­Â{Qß'Bƒm°2´”ZJºÒR¹­•:¹Ò¾˜X+•[ ´R¥R$Ò©•"¹U´Öi«[ ”zIq¬Í´­ Z±6ŠªÀê„*¹UZZ'É™¶’6ÕÊê„Jq¦­•ÒN,%
Åm¨ÒŠÊ¦r¦­”R9±4“CQ@QµR§MK<4D>P)šÉ±´¨¬ËÖ©…¢*pJ ÅÚT)©“¦BZ¥IÓP( ű´¨¤T­¥m«U‰¤¢¥kEZµ¨JŠr¬Õ¦™H+,ÇZÀm«
…”Ò@<40>Z¹"¡6M´"€ÓP¤jUb9ÓVêèªVІê4RËÉm«Õú«ZT)Û
er­T
J@C9ÖŠQ^X®•JÙ\JŠÊm«Õ¦ pÚ@Ø(˜IK<49>R4Pf#¥N(º±[À±V,jP)-<2D>åX«MÅ­ ZqÛjUÚKʪµi¤T%·
ÀŠ”2<EFBFBD>FP­ph)<E280BA>ÒpÊÀ°\ OéÂT)) Õ"¡PV'JcmšÚA¡H(”ª¤’"mš
iåPTK•:aª_jGr`¨Jx°U Ï
µ:¡>Ê"<22>PìÔJyÆ‘<01>q82"0Õo±H3i¦MUÒ68h¬Æby4
©µ©Vˆà´J'PcP<63>T´
€†Zµ¨JØê„*¡H(˜
[j+uÚV&€jµÂÒ,°(áÑÖ˪õÁX+Dµ¼œLš<05> k;…¾þ¯Tñµ©Åqb<71>>R´ŠÝÛ­Çרs×ýR•4Žh¦”
¥}ž2Ä০»å†*©Ó
ë”"}hœZ$”¶®€åJ•<4A>i*¤©m-ê%$áaÉŒR*œ6ÕÈ7E«à@¡´w¤hºoS´ŠÕ…Ò>ÚÒX))¤–;톘»<CB9C>
K)Ru©¤NÐN°SZ+.¡o´R´ŠÖ)åÑJÑ*Fè%Ò
#]%åÑJ¤PkEå´á­˜ŠzCl**db:m¢¿Ì¢ÚZ,mSÙ´´<E280B9>©Z/LÂÃÂ"µ¨V¬
¥*µ;­LBL&Òê1 3<>6l€RJÛH+VŠ´µ°(áQÁZ/**Ųp{<7B>tB°S+EzÚ
€ÖÂ´Ô ÕzQa%JJujP/-'¶i+Ò

ÓZ$”ŠuÂrÃÛT(U‰„Eg÷'€“„§uB<75>PH­h˜&À¦±´-S•ðxHÚæY©¶RX­­¢EÕj©P2õ©gª©»Ò2<C392>ðÃÝ·®0B—ùZ+*'me€´Òö¢âÐJ'” …¥¥T)“ÆRR¡ô„¢ql$X`m$X`-CªÅz)™°Rª%¤ ¤¡V-$Ö‚ ,­õrÚZ-RJÛL¨ÓJ‰<4A>ÕÒ84
é´‘PRd"µ´–Õ­ø‚" <09>ÒLª”fR¥P-0d4“ê¢Â„ÃÀ`ÒZ%
¥q°$2Ixxp0
J¢h)Ú“L/ªF¦Vh…«€!JLQ˜Ùðdd0eÙH<03>P+Ö ”ÁZ) Îó?a|3b©µ<C2A9>.2˜´µ84“ªtJyp0<EFBFBD><EFBFBD>¤BimžŒIv%`šM©N­K•ÁL²kCÕ<43>Ò6Z©“*EÒ68X@Ú6Ï)¥:µJ§Œc “ÞÞëTq•2®K (Z«¥¡H¦T¢<E28093>8ØÅÀ¹L ˜bzm€`y™°$!€Y`©
`‰M`y
‚ååÁÓÇêõ½5£K,><3E>зß<C39F>
»ŸëoG<EFBFBD>ÿ\bþbÇþ׺û]^X^$½0°ÔP«çJÀ´ÒÓ<>´P/¤
D s-`)¨Rœ‹¥¡hq´6OÌLÅ2W1Í\šÌeJE8LÃY)½¨:0ŽfÓL¶i*¤ÉSPp­H+˜)õ¢Zµ¨>ZëôÂ<C3B4>mš)uJYi2@ZZŠ„Êh¥N¤Š<C2A4>”"¡4N,SJCA`+,”SÅJ©EǦj<C2A6>L˜§VI[±6­tZY6•
´6Okó¬´H)ŒTJ¥ê@©TØ
ëeƒÀ6­Õ"•´k
U:µ6RF ë„R¡´ À±R¡4R-*Ee0
¦m$TH³:\Ó¨¤N%ÒÖ<C392>e0M«¤@ÁÀLšNªUÒ>2—*%%±zQm¢ÔIå•a/.•Ô©•:m)Õ…Éꥥq¨¨Pª-#Ë.V+¥µ´¨>¦TD].Óâc²)­Õ"¡BÛ
]ßǸ17ª&tÛ7<C39B>6É·Öù ©“ÊiÃÐTHÙ@;}¸I31¥" µH©Mk¡<&¯Þ؃ŠEÈŠÁöÅ^ÆdVÿv×O!ÖÍùàGïvÿ±Äâû†<C3BB>ŸGO_—Xn2¦þV[ÐNªŽ”¶Â0I•ðdWc½ -LRN&­”ÖJ©>&©“ŠaòÊÚ´H-/°õ´<C2B1>¦²:”„
µ6<EFBFBD>‰£4“Ê"Jx²K±
E*qh*©Â¤W“][`µN)<29>MC½ldvs´6<C2B4>R]` ÐN<68>¹ËçO1wo!ê䮽1wœ«µH¨UÖ3b1{Y,©e¥Ú4$)Õ餢mÈ$¶!ÚȲ+±JêÔjiš
©EÂä%<Ù…-h*$–‡ Kx² þc †ÒòraB<61>^ LV%<еH-*¤¡N!—ðdK‰„À ´ŒÔJi*$ ,Eàƒ-°6Jõa±´jËh¥´
”eW¿ß<EFBFBD>ê
<EFBFBD>ûcÌÛj§Û·i„1nz|]»bqS#GgíèXÚÆi”e×óÇøíb튭„JêdJ½¨ ²ìbÈO±M'$@ŠôɲË"S]€Ø.ç-g±­E¥T'MŠiåL©Õ¦ÂR"¡`ª¦€©TT(Lk<4C>V
Õ:udÙ¥É`š”JÕBZáÀPi¥t²È` *”e»to±…”êÄÚ´(áɲ+]/¦ÙÈlªik±^P( [ØT-’¶}²ìÂÒ±†¢2y820YÙ•±¡4"2* ’…QDJVd ¤<ž“Š&#%dìiPYp4^ÖÃÒá”a<>l8³À)ÉdDØ øD€€~ (Zf—·´à‡ED(¤<>Ð'äã$¤Ñ$ÞÙ´Š…+‰w6™!<21>Mxg“v$Þ1(øH†‡MLˆÈ8PûD¼ '³«€t²,R²'+""%ïl^ CÄöK±áÞÙ¨èïl4$(<28>Mà ðΆóÑí€Ò;€¶;¸$„“Ù¥ÞÙd†I$âTðÂáQ<C3A1>"$"­ £å
B#ÍBdŒ¨"¥”áÉ.‡‚Ø`xpÐl‡w6
­°™Œ’=4JªÞÙ@ ‡r4¼³Ñf„E”4¼³qå<>ÂHó¡`¤ÎÔ*"Í®„ ”Œ „HU>+…ö% Ù¥PÚTlH­¨¡X9HÐä„|#±+‰¼gôÀ9'
\êÄ$eQÒÀɃgÙ(
æ<EFBFBD><# (Q'U!ÂPÂ"2XbˆÁÄâ< ˜>/»@6D$ŸˆD ‰€^>ÙëŒ "LšÝ8h8y6T²÷Ð9܆*P6º-¨,8Ùo”ôeÃâäIÈtlx41a€é=-*%Œñ¶Á<19><4E>È[<17>Š*°<h<¨´ÓÊ  2$<n
Võp¡"ºàh\<$ Ëê!»T¤¤K˃{HyOL<4F>¨4m ©L)†+ã`¯¼:ð`<+AQUÃJÅé§B”(,U‰8©z‰Ð¯XV[œ`8ôæAä*Dª¹È®K,àT ˜qT”Â;…ME0•Dhf¡ãB@ 6ÓMJ8'ä" ¸<C3AC>sRa”ÐàœÜP-'<27>…¥eÆ„¤%)b€§{Ta…E²FÊ"CpÂ10m0x˜3.=ïl|#šÈg™0„æƒñ˜Yœ„,>ž¡£A}´<»º@‹“.
[2)©ÅÉ̃Š4KZ%<25><EFBFBD>{-,™ÅÉš0¦dQØÄ¢%,I€„…“
,mb
'U+;[ÂI%€ÂI<C382>DŠ&Ç…Ô¤”f·CAB8iuh¡ìÁ¨'4ŸT<8©ádÛœŒ’@Ag³éðΆY8,8L ƒƒà‘ÁÉF'7ŒŠe5Di8q ´EepÒdÀ
Ov=dpòãáÀº!âäKÉjF@ !ÁÀ”NBTò•4”(†G…¡€9…OgÓ<67>] úpNkÁ#¥+2 —ƒ<14>V@K—É‚NvTTtÅ¡Ê01 mº"%FÞÁ±HÀT¬\0 &“L#È`bf!%]:­Õ‹ J+…²­¸h("((+#¬% x ÙBDR!šKÈJ<08>ˆ2bÒÙ¬j$¤b1Ê<C38A><E28093>-#¤£ÊxaÉ XÉè¸|
XZ†HɆQbÑÙ”Œ”„d¤>20(ÑJvµ´Šhd$„dÌÈÈ‚`5ó²/B ¶Ž
â ¢ð…ÒË–]"*[âJL"<1F>. ðFa@Ãा, Ƴ*Ã;ɈB<71>b ˆ˜ØÚõIGN D\&·L“. <09>Mr\g$B7£„ÎF¡3¡{ÐÙhš“#ƒÑ‹†€ˆ\y
+b€!`y°V\ˆ‡ƒ€Í¦ºèŒãZà4lÛGÛlÖ…ÄÉ<C384>„ήìZølMv1H 0ô¶aLBÙURÀ£Ã3°á°ðÙ4
±<C2B1>æôɸ<>H(OÅbò<{@â„f;(Ð( ²+¡À‰‡´¼Æ‹Â¦¢-%“õ°D2ž àppD Øxéð‡–ìÊ®ìú`áäÁ &“°mÌÄ"cb<63>¦Ã Gh0Yò€Y¸ Á„œ+m0,<2C>
¦â€£€áˆQÀ,sÚ „+m0ÙõàJL¹Ð`^‹„"¤íJëXx>X¬;„P6ª
•lptEB<>”Gñ°<C3B1>d(2sàò±Ê®ìÊ®ì:€xÈ®UÈÆÁÇà Ç%õÀqBŠÈ®ìÊ.6™À”
C*`46V. ˜Ì.J<>ò\ P%!ÕTKå
WtÕ€²Q‰ Gçmxt<78>)è‹ b?
ŸìÚh˜ <20>ñÛ–]Ù•±à°m <0F>Î(»v@vµxFíZÙ•]$­<>è`¡%ÂÀB;<Rë@ E?% Íãá<C3A3> ÎvXˆR‰H³ ³4ˆD PÇD•±Þ<>FR,ŸÏŠcðÒÙ D<>L†"»NZÄÂÃFÅ<02>I¨eBÔ1è08lbvm°L0<(IË$4@€s <1D>]+
Z]ÙuÁ:<3A> 
±N- Çãò §ŒQÄHɧÓ]& ¸D,<|HTYv‰öckÐQyaL<>|²Ë´QC'ˆ¡ÖÆyÀÈ ½€
JPPŽ<EFBFBD>B²E0¬:R.NÞÈ3`…ôJUgÂ7 Ð…FÉó<C389>«ÏEÄ€ˆMñ¡"<n ¼Œˆ³Kãå`E%0édjeÁa±TW,#šÅ `É‘Sf9&!NF ˜xt2
fåm4¬¼<C2AC>†ÍÊ€I$Â<1D>H#—_Á¼pxX¶¨1d×6à£Ã³"rÚD°'ÃÑ qĤtBŠ@}x<>`e<>ÒØ4dhiÂlÙFª<17>ƒ<EFBFBD>‡Í tú<¸„F"T²iÈpÀ(<28>6eá”TCÊg;Ùp “•“ÒfWsŒDèÊV8..ä àa óÂá™x$@™ žì:q…H20 R ‘†
t@am°4R@
êðdW… Á(Á$´ñN"0-“<>DÅǃöÁJ÷ hÊâ`X:L*@Ló mæÂ•6šŒÈ3a2IÀ„µÁ<dD6˜Žf7@ #šÅ¨Ú€'N #<11>÷ŒN¢€ƒÉf׿
…‘/<2F>@ AÃ;Þpivé…w6Ri(5€(± '5'=#Õ \xgƒ¡¢! Ÿ „(»”Â;
Ϭè˜<,'ŸÑÇw6
õHPŠMC‡DóNn2JSfÕ¡¸øpà‰Þ,$œ\ äÄIpŸ”$Á<ì<>*¤³ÙŒ6.w6­€v0j™xgã2-Nf:CË-Ä;ÒŠ<>Óá IÀŒ^6˜Ž&#r¡¡@0iÉÁ<C389>
qvq¼† <0B>„çá=.
@:©XpX ½ 8¡(çäY-NFR„Av2àŒ†¨³qïl„ÂÓ :XY
5jyˆe•'³k31¡ÎãAPGp2J<0E>€Š5ÐÅÎA<C38E>ƒ”Ê<C38A>¤ ò:<78>Í(äáÀ(.â<>
´ØˆP«8xˆ@,ÊX>$Íx AU4@RçÁÂITv2JWAᤄ€
1eV<EFBFBD>@,GÃÁ*| <0C>44¡ '£t(œÌ®g`JRBÊ wxg³a*N %M ˆ<>Ëê)O‡gEñ+ŸÏg¡Áã¥g#„“$€(ŒÐ[ù¤hJ4ÈFƒ~t#ÂI<C382>+Ðl<C390>Ku80H'Y&É*
ds#ƒ“®"ƒ“/
œLpn>±E%ÁÉ(×CÄÉìjµÄÞ¸ACÄIN…ˆGÃÁ(„D%†àd”šQveÅCÉ u*6áAÀBI„",,d2¦Ì.Lw8©Òé±úpà(<#<23><>ˆ
Ç9÷Iyg“0 )#ïlXXZf䜌“Q&˜€@lH[áxHÃÁRИ„FZ
H*!1Ê„^²ЦdÀ
«BÕâ<C395>­,ºÒRÀtP'
¦Ä€£€É`<60><>¥³ÁL <0C>2<18> /!REZˆ<5A>°<œ4¤¼ÇåKëa­@,™]--ÒÒá!Å£áðPRЦä†1@RÉiÃ(H„X¡ÍH„0¨x…ˆmt°âÏ vƒÇÉA<C389>;yÙ@©Ì”-.<2E>ŒÅ&n>J"šìbX)ÈlCq1sñRK<E280BA>Ç
G+J;¨àH±dÕGÁÂ+ àl„<˜
ˆÍJ©ÁÓªð„D@-VCO*TCÃ@³ÙµzH±à€hØ0Ë„?`H¤
G°^¸  –”*Ã;“V<E2809C>YGGȃIY¼`€@
 `ÄÈJ(‚Ì' d,+"ŽŒ Å 2@2ŒŠÉÇCJ¼aŒL'…ƒÖn[k£9™<39>&/ÛGJ_6ÕkãDlÛ¦±8Éì:á“lñ†I¦€””$Æ
Ij<D2ó¡0 _J>ÕÊ<C395>LYayäÇI¢##[p䊆ÂH•”Š4¹<34>¨H…¨!ÊŠ“*”
ŠP.(ˆHN<48>ˆˆT±œ:6Ù²Á `]: O<>‡Y`<z€1ØŒd˜<64>”ѱáÕ3! Åy8mt:N8*<!ëA°øð<07>£°àdB>­T³k´y,<ŒÄò°±% OJ&ºx'UîÄ"[5t^V^V<D8¼÷IQpŸ”ä|4¸ÌB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;}
</style>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st0" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;}
</style>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st0" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#17541F;}
</style>
<rect class="st0" width="3212.8" height="1212.8"/>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st1" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#17541F;}
</style>
<path class="st0" d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path class="st0" d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path class="st0" d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path class="st0" d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7
c-8.4-2.1-15.7-3.1-22-3.1c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path class="st0" d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path class="st0" d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path class="st0" d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path class="st0" d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path class="st0" d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path class="st0" d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path class="st0" d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2
c-4.2-5.4-6.6-11.9-7.1-19.6h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9
v-24.7c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3
c-4-7.3-6-15.5-6-24.6s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8
h19.4v107.8C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16
c-2.6-4.8-6.1-8.5-10.5-11.1c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4
c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1
C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path class="st0" d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35
L2968.8,996.6z"/>
</g>
<path class="st0" d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st1" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="900"
height="900"
id="svg3923"
sodipodi:docname="square.svg"
inkscape:export-filename="/tmp/test.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata3929">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3927" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2096"
id="namedview3925"
showgrid="false"
inkscape:zoom="1.1360927"
inkscape:cx="635.07139"
inkscape:cy="606.383"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g3921" />
<g
transform="matrix(10.638298,0,0,10.638298,106.38298,-206.38301)"
id="g3921">
<defs
id="SvgjsDefs1018" />
<g
id="SvgjsG1019"
featureKey="root"
style="fill:#ffffff" />
<g
id="SvgjsG1020"
featureKey="symbol1"
transform="matrix(0.10341565,0,0,0.10341565,-11.43874,18.048418)"
inkscape:export-filename="/tmp/test.png"
inkscape:export-xdpi="116.02285"
inkscape:export-ydpi="116.02285"
style="fill:#17541f">
<defs
id="defs3911" />
<g
id="g3915">
<path
d="M 231,798 C 227,779 219,741 218,741 49,640 69,465 125,365 c 12,126 235,213 105,367 -1,2 6,26 12,48 26,-44 65,-97 63,-102 C 145,288 645,258 749,16 c 47,234 -24,596 -426,688 -2,1 -73,126 -76,127 0,-2 -30,-1 -26,-11 2,-6 6,-14 10,-22 z M 330,625 C 267,476 452,312 544,271 356,439 324,564 330,625 Z m -104,79 c 51,-59 -9,-160 -45,-193 61,105 57,166 45,193 z"
style="fill:#17541f"
id="path3913"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,51 +0,0 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*",
"/src/app/components/common/pdf-viewer/**"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "pngx",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "pngx",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

View File

@@ -58,7 +58,7 @@
"content": { "content": {
"size": -1, "size": -1,
"mimeType": "application/json", "mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}" "text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
}, },
"headersSize": -1, "headersSize": -1,
"bodySize": -1, "bodySize": -1,

View File

@@ -58,7 +58,7 @@
"content": { "content": {
"size": -1, "size": -1,
"mimeType": "application/json", "mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}" "text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
}, },
"headersSize": -1, "headersSize": -1,
"bodySize": -1, "bodySize": -1,

View File

@@ -58,7 +58,7 @@
"content": { "content": {
"size": -1, "size": -1,
"mimeType": "application/json", "mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}" "text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
}, },
"headersSize": -1, "headersSize": -1,
"bodySize": -1, "bodySize": -1,

View File

@@ -58,7 +58,7 @@
"content": { "content": {
"size": -1, "size": -1,
"mimeType": "application/json", "mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}" "text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
}, },
"headersSize": -1, "headersSize": -1,
"bodySize": -1, "bodySize": -1,

58
src-ui/eslint.config.js Normal file
View File

@@ -0,0 +1,58 @@
const angularEslintPlugin = require('@angular-eslint/eslint-plugin')
const angularTemplatePlugin = require('@angular-eslint/eslint-plugin-template')
const angularTemplateParser = require('@angular-eslint/template-parser')
const tsParser = require('@typescript-eslint/parser')
module.exports = [
{
ignores: ['projects/**/*', 'src/app/components/common/pdf-viewer/**'],
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: ['tsconfig.json'],
createDefaultProgram: true,
ecmaVersion: 2020,
sourceType: 'module',
},
},
plugins: {
'@angular-eslint': angularEslintPlugin,
'@angular-eslint/template': angularTemplatePlugin,
},
processor: '@angular-eslint/template/extract-inline-html',
rules: {
...angularEslintPlugin.configs.recommended.rules,
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'pngx',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'pngx',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
languageOptions: {
parser: angularTemplateParser,
},
plugins: {
'@angular-eslint/template': angularTemplatePlugin,
},
rules: {
...angularTemplatePlugin.configs.recommended.rules,
},
},
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "2.20.9", "version": "2.20.10",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"ng": "ng", "ng": "ng",
@@ -44,11 +44,11 @@
"@angular-builders/jest": "^21.0.3", "@angular-builders/jest": "^21.0.3",
"@angular-devkit/core": "^21.2.0", "@angular-devkit/core": "^21.2.0",
"@angular-devkit/schematics": "^21.2.0", "@angular-devkit/schematics": "^21.2.0",
"@angular-eslint/builder": "21.2.0", "@angular-eslint/builder": "21.3.0",
"@angular-eslint/eslint-plugin": "21.2.0", "@angular-eslint/eslint-plugin": "21.3.0",
"@angular-eslint/eslint-plugin-template": "21.2.0", "@angular-eslint/eslint-plugin-template": "21.3.0",
"@angular-eslint/schematics": "21.2.0", "@angular-eslint/schematics": "21.3.0",
"@angular-eslint/template-parser": "21.2.0", "@angular-eslint/template-parser": "21.3.0",
"@angular/build": "^21.2.0", "@angular/build": "^21.2.0",
"@angular/cli": "~21.2.0", "@angular/cli": "~21.2.0",
"@angular/compiler-cli": "~21.2.0", "@angular/compiler-cli": "~21.2.0",
@@ -59,7 +59,7 @@
"@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0", "@typescript-eslint/parser": "^8.54.0",
"@typescript-eslint/utils": "^8.54.0", "@typescript-eslint/utils": "^8.54.0",
"eslint": "^9.39.2", "eslint": "^10.0.2",
"jest": "30.2.0", "jest": "30.2.0",
"jest-environment-jsdom": "^30.2.0", "jest-environment-jsdom": "^30.2.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",

315
src-ui/pnpm-lock.yaml generated
View File

@@ -103,20 +103,20 @@ importers:
specifier: ^21.2.0 specifier: ^21.2.0
version: 21.2.0(chokidar@5.0.0) version: 21.2.0(chokidar@5.0.0)
'@angular-eslint/builder': '@angular-eslint/builder':
specifier: 21.2.0 specifier: 21.3.0
version: 21.2.0(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 21.3.0(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular-eslint/eslint-plugin': '@angular-eslint/eslint-plugin':
specifier: 21.2.0 specifier: 21.3.0
version: 21.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular-eslint/eslint-plugin-template': '@angular-eslint/eslint-plugin-template':
specifier: 21.2.0 specifier: 21.3.0
version: 21.2.0(@angular-eslint/template-parser@21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular-eslint/schematics': '@angular-eslint/schematics':
specifier: 21.2.0 specifier: 21.3.0
version: 21.2.0(@angular-eslint/template-parser@21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular-eslint/template-parser': '@angular-eslint/template-parser':
specifier: 21.2.0 specifier: 21.3.0
version: 21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular/build': '@angular/build':
specifier: ^21.2.0 specifier: ^21.2.0
version: 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0) version: 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)
@@ -140,16 +140,16 @@ importers:
version: 25.3.3 version: 25.3.3
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^8.54.0 specifier: ^8.54.0
version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.54.0 specifier: ^8.54.0
version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': '@typescript-eslint/utils':
specifier: ^8.54.0 specifier: ^8.54.0
version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
eslint: eslint:
specifier: ^9.39.2 specifier: ^10.0.2
version: 9.39.2(jiti@2.6.1) version: 10.0.2(jiti@2.6.1)
jest: jest:
specifier: 30.2.0 specifier: 30.2.0
version: 30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)) version: 30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3))
@@ -364,48 +364,48 @@ packages:
resolution: {integrity: sha512-3kn3FI5v7BQ7Zct6raek+WgvyDwOJ8wElbyC903GxMQCDBRGGcevhHvTAIHhknihEsrgplzPhTlWeMbk1JfdFg==} resolution: {integrity: sha512-3kn3FI5v7BQ7Zct6raek+WgvyDwOJ8wElbyC903GxMQCDBRGGcevhHvTAIHhknihEsrgplzPhTlWeMbk1JfdFg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
'@angular-eslint/builder@21.2.0': '@angular-eslint/builder@21.3.0':
resolution: {integrity: sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==} resolution: {integrity: sha512-26QUUouei52biUFAlJSrWNAU9tuF2miKwd8uHdxWwCF31xz+OxC5+NfudWvt1AFaYow7gWueX1QX3rNNtSPDrg==}
peerDependencies: peerDependencies:
'@angular/cli': '>= 21.0.0 < 22.0.0' '@angular/cli': '>= 21.0.0 < 22.0.0'
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '*' typescript: '*'
'@angular-eslint/bundled-angular-compiler@21.2.0': '@angular-eslint/bundled-angular-compiler@21.3.0':
resolution: {integrity: sha512-J0DWL+j6t9ItFIyIADvzHGqwDA1qfVJ9bx+oTmJ/Hlo7cUpIRoXpcTXpug0CEEABFH0RfDu6PDG2b0FoZ1+7bg==} resolution: {integrity: sha512-l521I24J9gJxyMbRkrM24Tc7W8J8BP+TDAmVs2nT8+lXbS3kg8QpWBRtd+hNUgq6o+vt+lKBkytnEfu8OiqeRg==}
'@angular-eslint/eslint-plugin-template@21.2.0': '@angular-eslint/eslint-plugin-template@21.3.0':
resolution: {integrity: sha512-lJ13Dj0DjR6YiceQR0sRbyWzSzOQ6uZPwK9CJUF3wuZjYAUvL1D61zaU9QrVLtf89NVOxv+dYZHDdu3IDeIqbA==} resolution: {integrity: sha512-lVixd/KypPWgA/5/pUOhJV9MTcaHjYZEqyOi+IiLk+h+maGxn6/s6Ot+20n+XGS85zAgOY+qUw6EEQ11hoojIQ==}
peerDependencies: peerDependencies:
'@angular-eslint/template-parser': 21.2.0 '@angular-eslint/template-parser': 21.3.0
'@typescript-eslint/types': ^7.11.0 || ^8.0.0 '@typescript-eslint/types': ^7.11.0 || ^8.0.0
'@typescript-eslint/utils': ^7.11.0 || ^8.0.0 '@typescript-eslint/utils': ^7.11.0 || ^8.0.0
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '*' typescript: '*'
'@angular-eslint/eslint-plugin@21.2.0': '@angular-eslint/eslint-plugin@21.3.0':
resolution: {integrity: sha512-X2Qn2viDsjm91CEMxNrxDH3qkKpp6un0C1F1BW2p/m9J4AUVfOcXwWz9UpHFSHTRQ+YlTJbiH1ZwwAPeKhFaxA==} resolution: {integrity: sha512-Whf/AUUBekOlfSJRS78m76YGrBQAZ3waXE7oOdlW5xEQvn8jBDN9EGuNnjg/syZzvzjK4ZpYC4g1XYXrc+fQIg==}
peerDependencies: peerDependencies:
'@typescript-eslint/utils': ^7.11.0 || ^8.0.0 '@typescript-eslint/utils': ^7.11.0 || ^8.0.0
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '*' typescript: '*'
'@angular-eslint/schematics@21.2.0': '@angular-eslint/schematics@21.3.0':
resolution: {integrity: sha512-WtT4fPKIUQ/hswy+l2GF/rKOdD+42L3fUzzcwRzNutQbe2tU9SimoSOAsay/ylWEuhIOQTs7ysPB8fUgFQoLpA==} resolution: {integrity: sha512-8deU/zVY9f8k8kAQQ9PL130ox2VlrZw3fMxgsPNAY5tjQ0xk0J2YVSszYHhcqdMGG1J01IsxIjvQaJ4pFfEmMw==}
peerDependencies: peerDependencies:
'@angular/cli': '>= 21.0.0 < 22.0.0' '@angular/cli': '>= 21.0.0 < 22.0.0'
'@angular-eslint/template-parser@21.2.0': '@angular-eslint/template-parser@21.3.0':
resolution: {integrity: sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==} resolution: {integrity: sha512-ysyou1zAY6M6rSZNdIcYKGd4nk6TCapamyFNB3ivmTlVZ0O35TS9o/rJ0aUttuHgDp+Ysgs3ql+LA746PXgCyQ==}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '*' typescript: '*'
'@angular-eslint/utils@21.2.0': '@angular-eslint/utils@21.3.0':
resolution: {integrity: sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==} resolution: {integrity: sha512-oNigH6w3l+owTMboj/uFG0tHOy43uH8BpQRtBOQL1/s2+5in/BJ2Fjobv3SyizxTgeJ1FhRefbkT8GmVjK7jAA==}
peerDependencies: peerDependencies:
'@typescript-eslint/utils': ^7.11.0 || ^8.0.0 '@typescript-eslint/utils': ^7.11.0 || ^8.0.0
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '*' typescript: '*'
'@angular/build@21.1.2': '@angular/build@21.1.2':
@@ -1579,33 +1579,25 @@ packages:
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.21.1': '@eslint/config-array@0.23.2':
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} resolution: {integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/config-helpers@0.4.2': '@eslint/config-helpers@0.5.2':
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/core@0.17.0': '@eslint/core@1.1.0':
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/eslintrc@3.3.3': '@eslint/object-schema@3.0.2':
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} resolution: {integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/js@9.39.2': '@eslint/plugin-kit@0.6.0':
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/object-schema@2.1.7':
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.4.1':
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fastify/busboy@2.1.1': '@fastify/busboy@2.1.1':
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
@@ -3324,8 +3316,8 @@ packages:
peerDependencies: peerDependencies:
ajv: ^8.8.2 ajv: ^8.8.2
ajv@6.12.6: ajv@6.14.0:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@8.17.1: ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
@@ -4024,12 +4016,8 @@ packages:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
eslint-scope@8.4.0: eslint-scope@9.1.1:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-scope@9.1.0:
resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint-visitor-keys@3.4.3: eslint-visitor-keys@3.4.3:
@@ -4040,9 +4028,13 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.39.2: eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.0.2:
resolution: {integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
jiti: '*' jiti: '*'
@@ -4050,9 +4042,9 @@ packages:
jiti: jiti:
optional: true optional: true
espree@10.4.0: espree@11.1.1:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
esprima@4.0.1: esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
@@ -4316,10 +4308,6 @@ packages:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
gopd@1.2.0: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -4974,9 +4962,6 @@ packages:
lodash.memoize@4.1.2: lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash@4.17.23: lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
@@ -5110,9 +5095,6 @@ packages:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@3.1.5: minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
@@ -7123,48 +7105,48 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- chokidar - chokidar
'@angular-eslint/builder@21.2.0(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@angular-eslint/builder@21.3.0(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) '@angular-devkit/architect': 0.2102.0(chokidar@5.0.0)
'@angular-devkit/core': 21.2.0(chokidar@5.0.0) '@angular-devkit/core': 21.2.0(chokidar@5.0.0)
'@angular/cli': 21.2.0(@types/node@25.3.3)(chokidar@5.0.0) '@angular/cli': 21.2.0(@types/node@25.3.3)(chokidar@5.0.0)
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- chokidar - chokidar
'@angular-eslint/bundled-angular-compiler@21.2.0': {} '@angular-eslint/bundled-angular-compiler@21.3.0': {}
'@angular-eslint/eslint-plugin-template@21.2.0(@angular-eslint/template-parser@21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@angular-eslint/eslint-plugin-template@21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@angular-eslint/bundled-angular-compiler': 21.2.0 '@angular-eslint/bundled-angular-compiler': 21.3.0
'@angular-eslint/template-parser': 21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@angular-eslint/template-parser': 21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular-eslint/utils': 21.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@angular-eslint/utils': 21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
aria-query: 5.3.2 aria-query: 5.3.2
axobject-query: 4.1.0 axobject-query: 4.1.0
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
'@angular-eslint/eslint-plugin@21.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@angular-eslint/eslint-plugin@21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@angular-eslint/bundled-angular-compiler': 21.2.0 '@angular-eslint/bundled-angular-compiler': 21.3.0
'@angular-eslint/utils': 21.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@angular-eslint/utils': 21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
ts-api-utils: 2.4.0(typescript@5.9.3) ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
'@angular-eslint/schematics@21.2.0(@angular-eslint/template-parser@21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@angular-eslint/schematics@21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@angular-devkit/core': 21.2.0(chokidar@5.0.0) '@angular-devkit/core': 21.2.0(chokidar@5.0.0)
'@angular-devkit/schematics': 21.2.0(chokidar@5.0.0) '@angular-devkit/schematics': 21.2.0(chokidar@5.0.0)
'@angular-eslint/eslint-plugin': 21.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@angular-eslint/eslint-plugin': 21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular-eslint/eslint-plugin-template': 21.2.0(@angular-eslint/template-parser@21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@angular-eslint/eslint-plugin-template': 21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@angular/cli': 21.2.0(@types/node@25.3.3)(chokidar@5.0.0) '@angular/cli': 21.2.0(@types/node@25.3.3)(chokidar@5.0.0)
ignore: 7.0.5 ignore: 7.0.5
semver: 7.7.3 semver: 7.7.4
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
transitivePeerDependencies: transitivePeerDependencies:
- '@angular-eslint/template-parser' - '@angular-eslint/template-parser'
@@ -7174,18 +7156,18 @@ snapshots:
- eslint - eslint
- typescript - typescript
'@angular-eslint/template-parser@21.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@angular-eslint/bundled-angular-compiler': 21.2.0 '@angular-eslint/bundled-angular-compiler': 21.3.0
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
eslint-scope: 9.1.0 eslint-scope: 9.1.1
typescript: 5.9.3 typescript: 5.9.3
'@angular-eslint/utils@21.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@angular-eslint/utils@21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@angular-eslint/bundled-angular-compiler': 21.2.0 '@angular-eslint/bundled-angular-compiler': 21.3.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
'@angular/build@21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': '@angular/build@21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)':
@@ -8422,50 +8404,34 @@ snapshots:
'@esbuild/win32-x64@0.27.3': '@esbuild/win32-x64@0.27.3':
optional: true optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': '@eslint-community/eslint-utils@4.9.1(eslint@10.0.2(jiti@2.6.1))':
dependencies: dependencies:
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.1': '@eslint/config-array@0.23.2':
dependencies: dependencies:
'@eslint/object-schema': 2.1.7 '@eslint/object-schema': 3.0.2
debug: 4.4.3 debug: 4.4.3
minimatch: 3.1.2 minimatch: 10.2.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@eslint/config-helpers@0.4.2': '@eslint/config-helpers@0.5.2':
dependencies: dependencies:
'@eslint/core': 0.17.0 '@eslint/core': 1.1.0
'@eslint/core@0.17.0': '@eslint/core@1.1.0':
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.3': '@eslint/object-schema@3.0.2': {}
'@eslint/plugin-kit@0.6.0':
dependencies: dependencies:
ajv: 6.12.6 '@eslint/core': 1.1.0
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
'@eslint/js@9.39.2': {}
'@eslint/object-schema@2.1.7': {}
'@eslint/plugin-kit@0.4.1':
dependencies:
'@eslint/core': 0.17.0
levn: 0.4.1 levn: 0.4.1
'@fastify/busboy@2.1.1': {} '@fastify/busboy@2.1.1': {}
@@ -9844,15 +9810,15 @@ snapshots:
dependencies: dependencies:
'@types/yargs-parser': 21.0.3 '@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/type-utils': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.54.0 '@typescript-eslint/visitor-keys': 8.54.0
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
ignore: 7.0.5 ignore: 7.0.5
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 2.4.0(typescript@5.9.3) ts-api-utils: 2.4.0(typescript@5.9.3)
@@ -9860,14 +9826,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/parser@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.54.0 '@typescript-eslint/visitor-keys': 8.54.0
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9890,13 +9856,13 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/type-utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
ts-api-utils: 2.4.0(typescript@5.9.3) ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -9919,13 +9885,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1) eslint: 10.0.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -10136,7 +10102,7 @@ snapshots:
ajv: 8.18.0 ajv: 8.18.0
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
ajv@6.12.6: ajv@6.14.0:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0 fast-json-stable-stringify: 2.1.0
@@ -10905,12 +10871,7 @@ snapshots:
esrecurse: 4.3.0 esrecurse: 4.3.0
estraverse: 4.3.0 estraverse: 4.3.0
eslint-scope@8.4.0: eslint-scope@9.1.1:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-scope@9.1.0:
dependencies: dependencies:
'@types/esrecurse': 4.3.1 '@types/esrecurse': 4.3.1
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -10921,28 +10882,27 @@ snapshots:
eslint-visitor-keys@4.2.1: {} eslint-visitor-keys@4.2.1: {}
eslint@9.39.2(jiti@2.6.1): eslint-visitor-keys@5.0.1: {}
eslint@10.0.2(jiti@2.6.1):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1 '@eslint/config-array': 0.23.2
'@eslint/config-helpers': 0.4.2 '@eslint/config-helpers': 0.5.2
'@eslint/core': 0.17.0 '@eslint/core': 1.1.0
'@eslint/eslintrc': 3.3.3 '@eslint/plugin-kit': 0.6.0
'@eslint/js': 9.39.2
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7 '@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3 '@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8 '@types/estree': 1.0.8
ajv: 6.12.6 ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6 cross-spawn: 7.0.6
debug: 4.4.3 debug: 4.4.3
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 8.4.0 eslint-scope: 9.1.1
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 5.0.1
espree: 10.4.0 espree: 11.1.1
esquery: 1.7.0 esquery: 1.7.0
esutils: 2.0.3 esutils: 2.0.3
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -10953,8 +10913,7 @@ snapshots:
imurmurhash: 0.1.4 imurmurhash: 0.1.4
is-glob: 4.0.3 is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1 json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2 minimatch: 10.2.4
minimatch: 3.1.2
natural-compare: 1.4.0 natural-compare: 1.4.0
optionator: 0.9.4 optionator: 0.9.4
optionalDependencies: optionalDependencies:
@@ -10962,11 +10921,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
espree@10.4.0: espree@11.1.1:
dependencies: dependencies:
acorn: 8.16.0 acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0) acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 5.0.1
esprima@4.0.1: {} esprima@4.0.1: {}
@@ -11284,8 +11243,6 @@ snapshots:
once: 1.4.0 once: 1.4.0
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
globals@14.0.0: {}
gopd@1.2.0: {} gopd@1.2.0: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
@@ -12177,8 +12134,6 @@ snapshots:
lodash.memoize@4.1.2: {} lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
lodash@4.17.23: {} lodash@4.17.23: {}
log-symbols@7.0.1: log-symbols@7.0.1:
@@ -12314,10 +12269,6 @@ snapshots:
dependencies: dependencies:
brace-expansion: 5.0.4 brace-expansion: 5.0.4
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
minimatch@3.1.5: minimatch@3.1.5:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12

View File

@@ -277,7 +277,7 @@
<div class="col"> <div class="col">
<select class="form-select" formControlName="pdfEditorDefaultEditMode"> <select class="form-select" formControlName="pdfEditorDefaultEditMode">
<option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option> <option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option>
<option [ngValue]="PdfEditorEditMode.Update" i18n>Update existing document</option> <option [ngValue]="PdfEditorEditMode.Update" i18n>Add document version</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -8,8 +8,10 @@
[ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2 col-xxxl-1' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }" [ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2 col-xxxl-1' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard" routerLink="/dashboard"
tourAnchor="tour.intro"> tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1.5em" height="1.5em" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" height="1.5em" fill="currentColor">
<path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/> <path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" />
</svg> </svg>
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled"> <div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
@if (customAppTitle?.length) { @if (customAppTitle?.length) {
@@ -97,12 +99,7 @@
</ul> </ul>
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
@if (savedViewService.loading) { @if (savedViewService.sidebarViews?.length > 0) {
<h6 class="sidebar-heading px-3 text-muted">
<span i18n>Saved views</span>
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
</h6>
} @else if (savedViewService.sidebarViews?.length > 0) {
<h6 class="sidebar-heading px-3 text-muted"> <h6 class="sidebar-heading px-3 text-muted">
<span i18n>Saved views</span> <span i18n>Saved views</span>
</h6> </h6>
@@ -132,6 +129,11 @@
</li> </li>
} }
</ul> </ul>
} @else if (savedViewService.loading) {
<h6 class="sidebar-heading px-3 text-muted">
<span i18n>Saved views</span>
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
</h6>
} }
</div> </div>

View File

@@ -1,22 +1,22 @@
@if (customLogo) { @if (customLogo) {
<img src="{{customLogo}}" [class]="getClasses()" [attr.style]="'height:'+height" /> <img src="{{customLogo}}" [class]="getClasses()" [attr.style]="'height:'+height" />
} @else { } @else {
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2670 860" [attr.style]="'height:'+height"> <svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
<path class="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/> <path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
<g class="text" style="fill: #000;"> <g class="text" style="fill:#000">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/> <path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/> <path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/> <path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/> <path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/> <rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/> <path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/> <path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/> <path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/> <rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/> <path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/> <path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/> <polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/> <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
</g> </g>
</svg> </svg>
} }

View File

@@ -84,7 +84,7 @@
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()"> <input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm"> <label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
<i-bs name="pencil"></i-bs> <i-bs name="pencil"></i-bs>
<span class="form-check-label ms-2" i18n>Update existing document</span> <span class="form-check-label ms-2" i18n>Add document version</span>
</label> </label>
</div> </div>
@if (editMode === PdfEditorEditMode.Create) { @if (editMode === PdfEditorEditMode.Create) {

View File

@@ -16,6 +16,12 @@
</div> </div>
</form> </form>
@if (note) {
<div class="small text-muted fst-italic mt-2">
{{ note }}
</div>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@if (!buttonsEnabled) { @if (!buttonsEnabled) {

View File

@@ -40,6 +40,9 @@ export class PermissionsDialogComponent {
@Input() @Input()
title = $localize`Set permissions` title = $localize`Set permissions`
@Input()
note: string = null
@Input() @Input()
set object(o: ObjectWithPermissions) { set object(o: ObjectWithPermissions) {
this.o = o this.o = o

View File

@@ -1,5 +1,5 @@
<pngx-page-header title="Dashboard" [subTitle]="subtitle" i18n-title tourAnchor="tour.dashboard"> <pngx-page-header title="Dashboard" [subTitle]="subtitle" i18n-title tourAnchor="tour.dashboard">
<pngx-logo extra_classes="d-none d-md-block mt-n2" height="3rem"></pngx-logo> <pngx-logo extra_classes="d-none d-md-block mt-n2" height="3.5rem"></pngx-logo>
</pngx-page-header> </pngx-page-header>
<div class="row"> <div class="row">

View File

@@ -65,6 +65,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
@@ -83,9 +84,9 @@ const doc: Document = {
storage_path: 31, storage_path: 31,
tags: [41, 42, 43], tags: [41, 42, 43],
content: 'text content', content: 'text content',
added: new Date('May 4, 2014 03:24:00'), added: new Date('May 4, 2014 03:24:00').toISOString(),
created: new Date('May 4, 2014 03:24:00'), created: new Date('May 4, 2014 03:24:00').toISOString(),
modified: new Date('May 4, 2014 03:24:00'), modified: new Date('May 4, 2014 03:24:00').toISOString(),
archive_serial_number: null, archive_serial_number: null,
original_file_name: 'file.pdf', original_file_name: 'file.pdf',
owner: null, owner: null,
@@ -327,6 +328,29 @@ describe('DocumentDetailComponent', () => {
expect(component.activeNavID).toEqual(component.DocumentDetailNavIDs.Notes) expect(component.activeNavID).toEqual(component.DocumentDetailNavIDs.Notes)
}) })
it('should switch from preview to details when pdf preview enters the DOM', fakeAsync(() => {
component.nav = {
activeId: component.DocumentDetailNavIDs.Preview,
select: jest.fn(),
} as any
;(component as any).pdfPreview = {
nativeElement: { offsetParent: {} },
}
tick()
expect(component.nav.select).toHaveBeenCalledWith(
component.DocumentDetailNavIDs.Details
)
}))
it('should forward title key up value to titleSubject', () => {
const subjectSpy = jest.spyOn(component.titleSubject, 'next')
component.titleKeyUp({ target: { value: 'Updated title' } })
expect(subjectSpy).toHaveBeenCalledWith('Updated title')
})
it('should change url on tab switch', () => { it('should change url on tab switch', () => {
initNormally() initNormally()
const navigateSpy = jest.spyOn(router, 'navigate') const navigateSpy = jest.spyOn(router, 'navigate')
@@ -524,7 +548,7 @@ describe('DocumentDetailComponent', () => {
jest.spyOn(documentService, 'get').mockReturnValue( jest.spyOn(documentService, 'get').mockReturnValue(
of({ of({
...doc, ...doc,
modified: new Date('2024-01-02T00:00:00Z'), modified: '2024-01-02T00:00:00Z',
duplicate_documents: updatedDuplicates, duplicate_documents: updatedDuplicates,
}) })
) )
@@ -1386,17 +1410,21 @@ describe('DocumentDetailComponent', () => {
expect(errorSpy).toHaveBeenCalled() expect(errorSpy).toHaveBeenCalled()
}) })
it('should warn when open document does not match doc retrieved from backend on init', () => { it('should show incoming update modal when open local draft is older than backend on init', () => {
let openModal: NgbModalRef let openModal: NgbModalRef
modalService.activeInstances.subscribe((modals) => (openModal = modals[0])) modalService.activeInstances.subscribe((modals) => (openModal = modals[0]))
const modalSpy = jest.spyOn(modalService, 'open') const modalSpy = jest.spyOn(modalService, 'open')
const openDoc = Object.assign({}, doc) const openDoc = Object.assign({}, doc, {
__changedFields: ['title'],
})
// simulate a document being modified elsewhere and db updated // simulate a document being modified elsewhere and db updated
doc.modified = new Date() const remoteDoc = Object.assign({}, doc, {
modified: new Date(new Date(doc.modified).getTime() + 1000).toISOString(),
})
jest jest
.spyOn(activatedRoute, 'paramMap', 'get') .spyOn(activatedRoute, 'paramMap', 'get')
.mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' }))) .mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' })))
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc)) jest.spyOn(documentService, 'get').mockReturnValueOnce(of(remoteDoc))
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc) jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue( jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({ of({
@@ -1406,11 +1434,185 @@ describe('DocumentDetailComponent', () => {
}) })
) )
fixture.detectChanges() // calls ngOnInit fixture.detectChanges() // calls ngOnInit
expect(modalSpy).toHaveBeenCalledWith(ConfirmDialogComponent) expect(modalSpy).toHaveBeenCalledWith(ConfirmDialogComponent, {
const closeSpy = jest.spyOn(openModal, 'close') backdrop: 'static',
})
const confirmDialog = openModal.componentInstance as ConfirmDialogComponent const confirmDialog = openModal.componentInstance as ConfirmDialogComponent
confirmDialog.confirmClicked.next(confirmDialog) expect(confirmDialog.messageBold).toContain('Document was updated at')
expect(closeSpy).toHaveBeenCalled() })
it('should react to websocket document updated notifications', () => {
initNormally()
const updateMessage = {
document_id: component.documentId,
modified: '2026-02-17T00:00:00Z',
owner_id: 1,
}
const handleSpy = jest
.spyOn(component as any, 'handleIncomingDocumentUpdated')
.mockImplementation(() => {})
const websocketStatusService = TestBed.inject(WebsocketStatusService)
websocketStatusService.handleDocumentUpdated(updateMessage)
expect(handleSpy).toHaveBeenCalledWith(updateMessage)
})
it('should queue incoming update while network is active and flush after', () => {
initNormally()
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const toastSpy = jest.spyOn(toastService, 'showInfo')
component.networkActive = true
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
modified: '2026-02-17T00:00:00Z',
})
expect(loadSpy).not.toHaveBeenCalled()
component.networkActive = false
;(component as any).flushPendingIncomingUpdate()
expect(loadSpy).toHaveBeenCalledWith(component.documentId, true)
expect(toastSpy).toHaveBeenCalledWith(
'Document reloaded with latest changes.'
)
})
it('should ignore queued incoming update matching local save modified', () => {
initNormally()
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const toastSpy = jest.spyOn(toastService, 'showInfo')
component.networkActive = true
;(component as any).lastLocalSaveModified = '2026-02-17T00:00:00+00:00'
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
modified: '2026-02-17T00:00:00+00:00',
})
component.networkActive = false
;(component as any).flushPendingIncomingUpdate()
expect(loadSpy).not.toHaveBeenCalled()
expect(toastSpy).not.toHaveBeenCalled()
})
it('should clear pdf source if preview URL is empty', () => {
component.pdfSource = { url: '/preview', password: 'secret' } as any
component.previewUrl = null
;(component as any).updatePdfSource()
expect(component.pdfSource).toEqual({ url: null, password: undefined })
})
it('should close incoming update modal if one is open', () => {
const modalRef = { close: jest.fn() } as unknown as NgbModalRef
;(component as any).incomingUpdateModal = modalRef
;(component as any).closeIncomingUpdateModal()
expect(modalRef.close).toHaveBeenCalled()
expect((component as any).incomingUpdateModal).toBeNull()
})
it('should reload remote version when incoming update modal is confirmed', async () => {
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modals) => (openModal = modals[0]))
const reloadSpy = jest
.spyOn(component as any, 'reloadRemoteVersion')
.mockImplementation(() => {})
;(component as any).showIncomingUpdateModal('2026-02-17T00:00:00Z')
const dialog = openModal.componentInstance as ConfirmDialogComponent
dialog.confirmClicked.next()
await openModal.result
expect(dialog.buttonsEnabled).toBe(false)
expect(reloadSpy).toHaveBeenCalled()
expect((component as any).incomingUpdateModal).toBeNull()
})
it('should overwrite open document state when loading remote version with force', () => {
const openDoc = Object.assign({}, doc, {
title: 'Locally edited title',
__changedFields: ['title'],
})
const remoteDoc = Object.assign({}, doc, {
title: 'Remote title',
modified: '2026-02-17T00:00:00Z',
})
jest.spyOn(documentService, 'get').mockReturnValue(of(remoteDoc))
jest.spyOn(documentService, 'getMetadata').mockReturnValue(
of({
has_archive_version: false,
original_mime_type: 'application/pdf',
})
)
jest.spyOn(documentService, 'getSuggestions').mockReturnValue(
of({
suggested_tags: [],
suggested_document_types: [],
suggested_correspondents: [],
})
)
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
const setDirtySpy = jest.spyOn(openDocumentsService, 'setDirty')
const saveSpy = jest.spyOn(openDocumentsService, 'save')
;(component as any).loadDocument(doc.id, true)
expect(openDoc.title).toEqual('Remote title')
expect(openDoc.__changedFields).toEqual([])
expect(setDirtySpy).toHaveBeenCalledWith(openDoc, false)
expect(saveSpy).toHaveBeenCalled()
})
it('should ignore incoming update for a different document id', () => {
initNormally()
const loadSpy = jest.spyOn(component as any, 'loadDocument')
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId + 1,
modified: '2026-02-17T00:00:00Z',
})
expect(loadSpy).not.toHaveBeenCalled()
})
it('should show incoming update modal when local document has unsaved edits', () => {
initNormally()
jest.spyOn(openDocumentsService, 'isDirty').mockReturnValue(true)
const modalSpy = jest
.spyOn(component as any, 'showIncomingUpdateModal')
.mockImplementation(() => {})
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
modified: '2026-02-17T00:00:00Z',
})
expect(modalSpy).toHaveBeenCalledWith('2026-02-17T00:00:00Z')
})
it('should reload current document and show toast when reloading remote version', () => {
component.documentId = doc.id
const closeModalSpy = jest
.spyOn(component as any, 'closeIncomingUpdateModal')
.mockImplementation(() => {})
const loadSpy = jest
.spyOn(component as any, 'loadDocument')
.mockImplementation(() => {})
const notifySpy = jest.spyOn(component.docChangeNotifier, 'next')
const toastSpy = jest.spyOn(toastService, 'showInfo')
;(component as any).reloadRemoteVersion()
expect(closeModalSpy).toHaveBeenCalled()
expect(notifySpy).toHaveBeenCalledWith(doc.id)
expect(loadSpy).toHaveBeenCalledWith(doc.id, true)
expect(toastSpy).toHaveBeenCalledWith('Document reloaded.')
}) })
it('should change preview element by render type', () => { it('should change preview element by render type', () => {
@@ -1721,6 +1923,14 @@ describe('DocumentDetailComponent', () => {
expect(component.createDisabled(DataType.Tag)).toBeFalsy() expect(component.createDisabled(DataType.Tag)).toBeFalsy()
}) })
it('should expose add permission via userCanAdd getter', () => {
currentUserCan = true
expect(component.userCanAdd).toBeTruthy()
currentUserCan = false
expect(component.userCanAdd).toBeFalsy()
})
it('should call tryRenderTiff when no archive and file is tiff', () => { it('should call tryRenderTiff when no archive and file is tiff', () => {
initNormally() initNormally()
const tiffRenderSpy = jest.spyOn( const tiffRenderSpy = jest.spyOn(

View File

@@ -13,6 +13,7 @@ import {
NgbDateStruct, NgbDateStruct,
NgbDropdownModule, NgbDropdownModule,
NgbModal, NgbModal,
NgbModalRef,
NgbNav, NgbNav,
NgbNavChangeEvent, NgbNavChangeEvent,
NgbNavModule, NgbNavModule,
@@ -80,6 +81,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
import { getFilenameFromContentDisposition } from 'src/app/utils/http' import { getFilenameFromContentDisposition } from 'src/app/utils/http'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif' import * as UTIF from 'utif'
@@ -143,6 +145,11 @@ enum ContentRenderType {
TIFF = 'tiff', TIFF = 'tiff',
} }
interface IncomingDocumentUpdate {
document_id: number
modified: string
}
@Component({ @Component({
selector: 'pngx-document-detail', selector: 'pngx-document-detail',
templateUrl: './document-detail.component.html', templateUrl: './document-detail.component.html',
@@ -208,6 +215,7 @@ export class DocumentDetailComponent
private componentRouterService = inject(ComponentRouterService) private componentRouterService = inject(ComponentRouterService)
private deviceDetectorService = inject(DeviceDetectorService) private deviceDetectorService = inject(DeviceDetectorService)
private savedViewService = inject(SavedViewService) private savedViewService = inject(SavedViewService)
private readonly websocketStatusService = inject(WebsocketStatusService)
@ViewChild('inputTitle') @ViewChild('inputTitle')
titleInput: TextComponent titleInput: TextComponent
@@ -267,6 +275,9 @@ export class DocumentDetailComponent
isDirty$: Observable<boolean> isDirty$: Observable<boolean>
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()
docChangeNotifier: Subject<any> = new Subject() docChangeNotifier: Subject<any> = new Subject()
private incomingUpdateModal: NgbModalRef
private pendingIncomingUpdate: IncomingDocumentUpdate
private lastLocalSaveModified: string | null = null
requiresPassword: boolean = false requiresPassword: boolean = false
password: string password: string
@@ -475,9 +486,12 @@ export class DocumentDetailComponent
) )
} }
private loadDocument(documentId: number): void { private loadDocument(documentId: number, forceRemote: boolean = false): void {
let redirectedToRoot = false let redirectedToRoot = false
this.closeIncomingUpdateModal()
this.pendingIncomingUpdate = null
this.selectedVersionId = documentId this.selectedVersionId = documentId
this.lastLocalSaveModified = null
this.previewUrl = this.documentsService.getPreviewUrl( this.previewUrl = this.documentsService.getPreviewUrl(
this.selectedVersionId this.selectedVersionId
) )
@@ -545,21 +559,25 @@ export class DocumentDetailComponent
openDocument.duplicate_documents = doc.duplicate_documents openDocument.duplicate_documents = doc.duplicate_documents
this.openDocumentService.save() this.openDocumentService.save()
} }
const useDoc = openDocument || doc let useDoc = openDocument || doc
if (openDocument) { if (openDocument && forceRemote) {
if ( Object.assign(openDocument, doc)
new Date(doc.modified) > new Date(openDocument.modified) && openDocument.__changedFields = []
!this.modalService.hasOpenModals() this.openDocumentService.setDirty(openDocument, false)
) { this.openDocumentService.save()
const modal = this.modalService.open(ConfirmDialogComponent) useDoc = openDocument
modal.componentInstance.title = $localize`Document changes detected` } else if (openDocument) {
modal.componentInstance.messageBold = $localize`The version of this document in your browser session appears older than the existing version.` if (new Date(doc.modified) > new Date(openDocument.modified)) {
modal.componentInstance.message = $localize`Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.` if (this.hasLocalEdits(openDocument)) {
modal.componentInstance.cancelBtnClass = 'visually-hidden' this.showIncomingUpdateModal(doc.modified)
modal.componentInstance.btnCaption = $localize`Ok` } else {
modal.componentInstance.confirmClicked.subscribe(() => // No local edits to preserve, so keep the tab in sync automatically.
modal.close() Object.assign(openDocument, doc)
) openDocument.__changedFields = []
this.openDocumentService.setDirty(openDocument, false)
this.openDocumentService.save()
useDoc = openDocument
}
} }
} else { } else {
this.openDocumentService this.openDocumentService
@@ -590,6 +608,98 @@ export class DocumentDetailComponent
}) })
} }
private hasLocalEdits(doc: Document): boolean {
return (
this.openDocumentService.isDirty(doc) || !!doc.__changedFields?.length
)
}
private showIncomingUpdateModal(modified: string): void {
if (this.incomingUpdateModal) return
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
this.incomingUpdateModal = modal
let formattedModified = null
const parsed = new Date(modified)
formattedModified = parsed.toLocaleString()
modal.componentInstance.title = $localize`Document was updated`
modal.componentInstance.messageBold = $localize`Document was updated at ${formattedModified}.`
modal.componentInstance.message = $localize`Reload to discard your local unsaved edits and load the latest remote version.`
modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Reload`
modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false
modal.close()
this.reloadRemoteVersion()
})
modal.result.finally(() => {
this.incomingUpdateModal = null
})
}
private closeIncomingUpdateModal() {
if (!this.incomingUpdateModal) return
this.incomingUpdateModal.close()
this.incomingUpdateModal = null
}
private flushPendingIncomingUpdate() {
if (!this.pendingIncomingUpdate || this.networkActive) return
const pendingUpdate = this.pendingIncomingUpdate
this.pendingIncomingUpdate = null
this.handleIncomingDocumentUpdated(pendingUpdate)
}
private handleIncomingDocumentUpdated(data: IncomingDocumentUpdate): void {
if (
!this.documentId ||
!this.document ||
data.document_id !== this.documentId
)
return
if (this.networkActive) {
this.pendingIncomingUpdate = data
return
}
// If modified timestamp of the incoming update is the same as the last local save,
// we assume this update is from our own save and dont notify
const incomingModified = data.modified
if (
incomingModified &&
this.lastLocalSaveModified &&
incomingModified === this.lastLocalSaveModified
) {
this.lastLocalSaveModified = null
return
}
this.lastLocalSaveModified = null
if (this.openDocumentService.isDirty(this.document)) {
this.showIncomingUpdateModal(data.modified)
} else {
this.docChangeNotifier.next(this.documentId)
this.loadDocument(this.documentId, true)
this.toastService.showInfo(
$localize`Document reloaded with latest changes.`
)
}
}
private reloadRemoteVersion() {
if (!this.documentId) return
this.closeIncomingUpdateModal()
this.docChangeNotifier.next(this.documentId)
this.loadDocument(this.documentId, true)
this.toastService.showInfo($localize`Document reloaded.`)
}
ngOnInit(): void { ngOnInit(): void {
this.setZoom( this.setZoom(
this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale
@@ -648,6 +758,11 @@ export class DocumentDetailComponent
this.getCustomFields() this.getCustomFields()
this.websocketStatusService
.onDocumentUpdated()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((data) => this.handleIncomingDocumentUpdated(data))
this.route.paramMap this.route.paramMap
.pipe( .pipe(
filter( filter(
@@ -1033,6 +1148,7 @@ export class DocumentDetailComponent
) )
.subscribe({ .subscribe({
next: (doc) => { next: (doc) => {
this.closeIncomingUpdateModal()
Object.assign(this.document, doc) Object.assign(this.document, doc)
doc['permissions_form'] = { doc['permissions_form'] = {
owner: doc.owner, owner: doc.owner,
@@ -1079,6 +1195,8 @@ export class DocumentDetailComponent
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: (docValues) => { next: (docValues) => {
this.closeIncomingUpdateModal()
this.lastLocalSaveModified = docValues.modified ?? null
// in case data changed while saving eg removing inbox_tags // in case data changed while saving eg removing inbox_tags
this.documentForm.patchValue(docValues) this.documentForm.patchValue(docValues)
const newValues = Object.assign({}, this.documentForm.value) const newValues = Object.assign({}, this.documentForm.value)
@@ -1093,16 +1211,19 @@ export class DocumentDetailComponent
this.networkActive = false this.networkActive = false
this.error = null this.error = null
if (close) { if (close) {
this.pendingIncomingUpdate = null
this.close(() => this.close(() =>
this.openDocumentService.refreshDocument(this.documentId) this.openDocumentService.refreshDocument(this.documentId)
) )
} else { } else {
this.openDocumentService.refreshDocument(this.documentId) this.openDocumentService.refreshDocument(this.documentId)
this.flushPendingIncomingUpdate()
} }
this.savedViewService.maybeRefreshDocumentCounts() this.savedViewService.maybeRefreshDocumentCounts()
}, },
error: (error) => { error: (error) => {
this.networkActive = false this.networkActive = false
this.lastLocalSaveModified = null
const canEdit = const canEdit =
this.permissionsService.currentUserHasObjectPermissions( this.permissionsService.currentUserHasObjectPermissions(
PermissionAction.Change, PermissionAction.Change,
@@ -1122,6 +1243,7 @@ export class DocumentDetailComponent
error error
) )
} }
this.flushPendingIncomingUpdate()
}, },
}) })
} }
@@ -1158,8 +1280,11 @@ export class DocumentDetailComponent
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: ({ updateResult, nextDocId, closeResult }) => { next: ({ updateResult, nextDocId, closeResult }) => {
this.closeIncomingUpdateModal()
this.error = null this.error = null
this.networkActive = false this.networkActive = false
this.pendingIncomingUpdate = null
this.lastLocalSaveModified = null
if (closeResult && updateResult && nextDocId) { if (closeResult && updateResult && nextDocId) {
this.router.navigate(['documents', nextDocId]) this.router.navigate(['documents', nextDocId])
this.titleInput?.focus() this.titleInput?.focus()
@@ -1167,8 +1292,10 @@ export class DocumentDetailComponent
}, },
error: (error) => { error: (error) => {
this.networkActive = false this.networkActive = false
this.lastLocalSaveModified = null
this.error = error.error this.error = error.error
this.toastService.showError($localize`Error saving document`, error) this.toastService.showError($localize`Error saving document`, error)
this.flushPendingIncomingUpdate()
}, },
}) })
} }
@@ -1254,7 +1381,7 @@ export class DocumentDetailComponent
.subscribe({ .subscribe({
next: () => { next: () => {
this.toastService.showInfo( this.toastService.showInfo(
$localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.` $localize`Reprocess operation for "${this.document.title}" will begin in the background.`
) )
if (modal) { if (modal) {
modal.close() modal.close()

View File

@@ -830,7 +830,7 @@ export class BulkEditorComponent
}) })
const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent
rotateDialog.title = $localize`Rotate confirm` rotateDialog.title = $localize`Rotate confirm`
rotateDialog.messageBold = $localize`This operation will permanently rotate the original version of ${this.list.selected.size} document(s).` rotateDialog.messageBold = $localize`This operation will add rotated versions of the ${this.list.selected.size} document(s).`
rotateDialog.btnClass = 'btn-danger' rotateDialog.btnClass = 'btn-danger'
rotateDialog.btnCaption = $localize`Proceed` rotateDialog.btnCaption = $localize`Proceed`
rotateDialog.documentID = Array.from(this.list.selected)[0] rotateDialog.documentID = Array.from(this.list.selected)[0]

View File

@@ -104,11 +104,9 @@
} }
} }
<div *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }"> @if (list.activeSavedViewId && activeSavedViewCanChange) {
@if (list.activeSavedViewId) { <button ngbDropdownItem (click)="saveViewConfig()" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfig()" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button> }
}
</div>
<button ngbDropdownItem (click)="saveViewConfigAs()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button> <button ngbDropdownItem (click)="saveViewConfigAs()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button>
<a ngbDropdownItem routerLink="/savedviews" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" i18n>All saved views</a> <a ngbDropdownItem routerLink="/savedviews" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" i18n>All saved views</a>
</div> </div>

View File

@@ -168,6 +168,10 @@ describe('DocumentListComponent', () => {
) )
}) })
it('should not allow changing a saved view when none is active', () => {
expect(component.activeSavedViewCanChange).toBeFalsy()
})
it('should determine if filtered, support reset', () => { it('should determine if filtered, support reset', () => {
fixture.detectChanges() fixture.detectChanges()
documentListService.setFilterRules([ documentListService.setFilterRules([
@@ -299,6 +303,19 @@ describe('DocumentListComponent', () => {
expect(setCountSpy).toHaveBeenCalledWith(expect.any(Object), 3) expect(setCountSpy).toHaveBeenCalledWith(expect.any(Object), 3)
}) })
it('should reset active saved view when loading unknown view config', () => {
component['activeSavedView'] = { id: 1 } as SavedView
const activateSpy = jest.spyOn(documentListService, 'activateSavedView')
const reloadSpy = jest.spyOn(documentListService, 'reload')
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(null))
component.loadViewConfig(10)
expect(component['activeSavedView']).toBeNull()
expect(activateSpy).not.toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
})
it('should support 3 different display modes', () => { it('should support 3 different display modes', () => {
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs) jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
fixture.detectChanges() fixture.detectChanges()
@@ -466,7 +483,7 @@ describe('DocumentListComponent', () => {
}) })
it('should handle error on view saving', () => { it('should handle error on view saving', () => {
component.list.activateSavedView({ const view: SavedView = {
id: 10, id: 10,
name: 'Saved View 10', name: 'Saved View 10',
sort_field: 'added', sort_field: 'added',
@@ -477,7 +494,16 @@ describe('DocumentListComponent', () => {
value: '20', value: '20',
}, },
], ],
}) }
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
const queryParams = { view: view.id.toString() }
jest
.spyOn(activatedRoute, 'queryParamMap', 'get')
.mockReturnValue(of(convertToParamMap(queryParams)))
activatedRoute.snapshot.queryParams = queryParams
router.routerState.snapshot.url = '/view/10/'
fixture.detectChanges()
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest jest
.spyOn(savedViewService, 'patch') .spyOn(savedViewService, 'patch')
@@ -489,6 +515,40 @@ describe('DocumentListComponent', () => {
) )
}) })
it('should not save a view without object change permissions', () => {
const view: SavedView = {
id: 10,
name: 'Saved View 10',
sort_field: 'added',
sort_reverse: true,
filter_rules: [
{
rule_type: FILTER_HAS_TAGS_ANY,
value: '20',
},
],
owner: 999,
user_can_change: false,
}
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
jest
.spyOn(permissionService, 'currentUserHasObjectPermissions')
.mockReturnValue(false)
const queryParams = { view: view.id.toString() }
jest
.spyOn(activatedRoute, 'queryParamMap', 'get')
.mockReturnValue(of(convertToParamMap(queryParams)))
activatedRoute.snapshot.queryParams = queryParams
router.routerState.snapshot.url = '/view/10/'
fixture.detectChanges()
const patchSpy = jest.spyOn(savedViewService, 'patch')
component.saveViewConfig()
expect(patchSpy).not.toHaveBeenCalled()
})
it('should support edited view saving as', () => { it('should support edited view saving as', () => {
const view: SavedView = { const view: SavedView = {
id: 10, id: 10,
@@ -520,19 +580,105 @@ describe('DocumentListComponent', () => {
const modalSpy = jest.spyOn(modalService, 'open') const modalSpy = jest.spyOn(modalService, 'open')
const toastSpy = jest.spyOn(toastService, 'showInfo') const toastSpy = jest.spyOn(toastService, 'showInfo')
const savedViewServiceCreate = jest.spyOn(savedViewService, 'create') const savedViewServiceCreate = jest.spyOn(savedViewService, 'create')
jest
.spyOn(savedViewService, 'dashboardViews', 'get')
.mockReturnValue([{ id: 77 } as SavedView])
jest
.spyOn(savedViewService, 'sidebarViews', 'get')
.mockReturnValue([{ id: 88 } as SavedView])
const updateVisibilitySpy = jest
.spyOn(settingsService, 'updateSavedViewsVisibility')
.mockReturnValue(of({ success: true }))
savedViewServiceCreate.mockReturnValueOnce(of(modifiedView)) savedViewServiceCreate.mockReturnValueOnce(of(modifiedView))
component.saveViewConfigAs() component.saveViewConfigAs()
const modalCloseSpy = jest.spyOn(openModal, 'close')
const permissions = {
owner: 5,
set_permissions: {
view: {
users: [4],
groups: [3],
},
change: {
users: [2],
groups: [1],
},
},
}
openModal.componentInstance.saveClicked.next({
name: 'Foo Bar',
showOnDashboard: true,
showInSideBar: true,
permissions_form: permissions,
})
expect(savedViewServiceCreate).toHaveBeenCalled()
expect(savedViewServiceCreate).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Foo Bar',
owner: permissions.owner,
set_permissions: permissions.set_permissions,
})
)
expect(updateVisibilitySpy).toHaveBeenCalledWith(
expect.arrayContaining([77, modifiedView.id]),
expect.arrayContaining([88, modifiedView.id])
)
expect(modalSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled()
})
it('should show error when visibility update fails after creating a view', () => {
const view: SavedView = {
id: 10,
name: 'Saved View 10',
sort_field: 'added',
sort_reverse: true,
filter_rules: [
{
rule_type: FILTER_HAS_TAGS_ANY,
value: '20',
},
],
}
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
const queryParams = { view: view.id.toString() }
jest
.spyOn(activatedRoute, 'queryParamMap', 'get')
.mockReturnValue(of(convertToParamMap(queryParams)))
activatedRoute.snapshot.queryParams = queryParams
router.routerState.snapshot.url = '/view/10/'
fixture.detectChanges()
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
jest
.spyOn(savedViewService, 'create')
.mockReturnValueOnce(of({ ...view, id: 42, name: 'Foo Bar' }))
jest.spyOn(savedViewService, 'dashboardViews', 'get').mockReturnValue([])
jest.spyOn(savedViewService, 'sidebarViews', 'get').mockReturnValue([])
jest
.spyOn(settingsService, 'updateSavedViewsVisibility')
.mockReturnValueOnce(
throwError(() => new Error('unable to save visibility settings'))
)
const toastErrorSpy = jest.spyOn(toastService, 'showError')
component.saveViewConfigAs()
const modalCloseSpy = jest.spyOn(openModal, 'close') const modalCloseSpy = jest.spyOn(openModal, 'close')
openModal.componentInstance.saveClicked.next({ openModal.componentInstance.saveClicked.next({
name: 'Foo Bar', name: 'Foo Bar',
show_on_dashboard: true, showOnDashboard: true,
show_in_sidebar: true, showInSideBar: false,
}) })
expect(savedViewServiceCreate).toHaveBeenCalled()
expect(modalSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled() expect(modalCloseSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalledWith(
'View "Foo Bar" created successfully, but could not update visibility settings.',
expect.any(Error)
)
}) })
it('should handle error on edited view saving as', () => { it('should handle error on edited view saving as', () => {
@@ -563,6 +709,10 @@ describe('DocumentListComponent', () => {
let openModal: NgbModalRef let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const updateVisibilitySpy = jest.spyOn(
settingsService,
'updateSavedViewsVisibility'
)
jest.spyOn(savedViewService, 'create').mockReturnValueOnce( jest.spyOn(savedViewService, 'create').mockReturnValueOnce(
throwError( throwError(
() => () =>
@@ -575,9 +725,10 @@ describe('DocumentListComponent', () => {
openModal.componentInstance.saveClicked.next({ openModal.componentInstance.saveClicked.next({
name: 'Foo Bar', name: 'Foo Bar',
show_on_dashboard: true, showOnDashboard: true,
show_in_sidebar: true, showInSideBar: true,
}) })
expect(updateVisibilitySpy).not.toHaveBeenCalled()
expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] }) expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] })
}) })

View File

@@ -47,7 +47,10 @@ import { UsernamePipe } from 'src/app/pipes/username.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { HotKeyService } from 'src/app/services/hot-key.service' import { HotKeyService } from 'src/app/services/hot-key.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { PermissionsService } from 'src/app/services/permissions.service' import {
PermissionAction,
PermissionsService,
} from 'src/app/services/permissions.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
@@ -148,12 +151,18 @@ export class DocumentListComponent
unmodifiedFilterRules: FilterRule[] = [] unmodifiedFilterRules: FilterRule[] = []
private unmodifiedSavedView: SavedView private unmodifiedSavedView: SavedView
private activeSavedView: SavedView | null = null
private unsubscribeNotifier: Subject<any> = new Subject() private unsubscribeNotifier: Subject<any> = new Subject()
get savedViewIsModified(): boolean { get savedViewIsModified(): boolean {
if (!this.list.activeSavedViewId || !this.unmodifiedSavedView) return false if (
else { !this.list.activeSavedViewId ||
!this.unmodifiedSavedView ||
!this.activeSavedViewCanChange
) {
return false
} else {
return ( return (
this.unmodifiedSavedView.sort_field !== this.list.sortField || this.unmodifiedSavedView.sort_field !== this.list.sortField ||
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse || this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
@@ -180,6 +189,16 @@ export class DocumentListComponent
} }
} }
get activeSavedViewCanChange(): boolean {
if (!this.activeSavedView) {
return false
}
return this.permissionService.currentUserHasObjectPermissions(
PermissionAction.Change,
this.activeSavedView
)
}
get isFiltered() { get isFiltered() {
return !!this.filterEditor?.rulesModified return !!this.filterEditor?.rulesModified
} }
@@ -264,11 +283,13 @@ export class DocumentListComponent
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ view }) => { .subscribe(({ view }) => {
if (!view) { if (!view) {
this.activeSavedView = null
this.router.navigate(['404'], { this.router.navigate(['404'], {
replaceUrl: true, replaceUrl: true,
}) })
return return
} }
this.activeSavedView = view
this.unmodifiedSavedView = view this.unmodifiedSavedView = view
this.list.activateSavedViewWithQueryParams( this.list.activateSavedViewWithQueryParams(
view, view,
@@ -292,6 +313,7 @@ export class DocumentListComponent
// loading a saved view on /documents // loading a saved view on /documents
this.loadViewConfig(parseInt(queryParams.get('view'))) this.loadViewConfig(parseInt(queryParams.get('view')))
} else { } else {
this.activeSavedView = null
this.list.activateSavedView(null) this.list.activateSavedView(null)
this.list.loadFromQueryParams(queryParams) this.list.loadFromQueryParams(queryParams)
this.unmodifiedFilterRules = [] this.unmodifiedFilterRules = []
@@ -374,7 +396,7 @@ export class DocumentListComponent
} }
saveViewConfig() { saveViewConfig() {
if (this.list.activeSavedViewId != null) { if (this.list.activeSavedViewId != null && this.activeSavedViewCanChange) {
let savedView: SavedView = { let savedView: SavedView = {
id: this.list.activeSavedViewId, id: this.list.activeSavedViewId,
filter_rules: this.list.filterRules, filter_rules: this.list.filterRules,
@@ -388,6 +410,7 @@ export class DocumentListComponent
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: (view) => { next: (view) => {
this.activeSavedView = view
this.unmodifiedSavedView = view this.unmodifiedSavedView = view
this.toastService.showInfo( this.toastService.showInfo(
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.` $localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
@@ -409,6 +432,11 @@ export class DocumentListComponent
.getCached(viewID) .getCached(viewID)
.pipe(first()) .pipe(first())
.subscribe((view) => { .subscribe((view) => {
if (!view) {
this.activeSavedView = null
return
}
this.activeSavedView = view
this.unmodifiedSavedView = view this.unmodifiedSavedView = view
this.list.activateSavedView(view) this.list.activateSavedView(view)
this.list.reload(() => { this.list.reload(() => {
@@ -426,24 +454,48 @@ export class DocumentListComponent
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
let savedView: SavedView = { let savedView: SavedView = {
name: formValue.name, name: formValue.name,
show_on_dashboard: formValue.showOnDashboard,
show_in_sidebar: formValue.showInSideBar,
filter_rules: this.list.filterRules, filter_rules: this.list.filterRules,
sort_reverse: this.list.sortReverse, sort_reverse: this.list.sortReverse,
sort_field: this.list.sortField, sort_field: this.list.sortField,
display_mode: this.list.displayMode, display_mode: this.list.displayMode,
display_fields: this.activeDisplayFields, display_fields: this.activeDisplayFields,
} }
const permissions = formValue.permissions_form
if (permissions) {
if (permissions.owner !== null && permissions.owner !== undefined) {
savedView.owner = permissions.owner
}
if (permissions.set_permissions) {
savedView['set_permissions'] = permissions.set_permissions
}
}
this.savedViewService this.savedViewService
.create(savedView) .create(savedView)
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: () => { next: (createdView) => {
modal.close() this.saveCreatedViewVisibility(
this.toastService.showInfo( createdView,
$localize`View "${savedView.name}" created successfully.` formValue.showOnDashboard,
formValue.showInSideBar
) )
.pipe(first())
.subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
$localize`View "${savedView.name}" created successfully.`
)
},
error: (error) => {
modal.close()
this.toastService.showError(
$localize`View "${savedView.name}" created successfully, but could not update visibility settings.`,
error
)
},
})
}, },
error: (httpError) => { error: (httpError) => {
let error = httpError.error let error = httpError.error
@@ -457,6 +509,28 @@ export class DocumentListComponent
}) })
} }
private saveCreatedViewVisibility(
createdView: SavedView,
showOnDashboard: boolean,
showInSideBar: boolean
) {
const dashboardViewIds = this.savedViewService.dashboardViews.map(
(v) => v.id
)
const sidebarViewIds = this.savedViewService.sidebarViews.map((v) => v.id)
if (showOnDashboard) {
dashboardViewIds.push(createdView.id)
}
if (showInSideBar) {
sidebarViewIds.push(createdView.id)
}
return this.settingsService.updateSavedViewsVisibility(
dashboardViewIds,
sidebarViewIds
)
}
openDocumentDetail(document: Document | number) { openDocumentDetail(document: Document | number) {
this.router.navigate([ this.router.navigate([
'documents', 'documents',

View File

@@ -8,6 +8,7 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check> <pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check> <pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
<pngx-permissions-form accordion="true" formControlName="permissions_form"></pngx-permissions-form>
@if (error?.filter_rules) { @if (error?.filter_rules) {
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6> <h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>

View File

@@ -7,7 +7,13 @@ import {
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { of } from 'rxjs'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { CheckComponent } from '../../common/input/check/check.component' import { CheckComponent } from '../../common/input/check/check.component'
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
import { TextComponent } from '../../common/input/text/text.component' import { TextComponent } from '../../common/input/text/text.component'
import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component' import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component'
@@ -18,7 +24,21 @@ describe('SaveViewConfigDialogComponent', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [NgbActiveModal], providers: [
NgbActiveModal,
{
provide: UserService,
useValue: {
listAll: () => of({ results: [] }),
},
},
{
provide: GroupService,
useValue: {
listAll: () => of({ results: [] }),
},
},
],
imports: [ imports: [
NgbModalModule, NgbModalModule,
FormsModule, FormsModule,
@@ -26,6 +46,9 @@ describe('SaveViewConfigDialogComponent', () => {
SaveViewConfigDialogComponent, SaveViewConfigDialogComponent,
TextComponent, TextComponent,
CheckComponent, CheckComponent,
PermissionsFormComponent,
PermissionsUserComponent,
PermissionsGroupComponent,
], ],
}).compileComponents() }).compileComponents()
@@ -81,6 +104,26 @@ describe('SaveViewConfigDialogComponent', () => {
}) })
}) })
it('should support permissions input', () => {
const permissions = {
owner: 10,
set_permissions: {
view: { users: [2], groups: [3] },
change: { users: [4], groups: [5] },
},
}
let result
component.saveClicked.subscribe((saveResult) => (result = saveResult))
component.saveViewConfigForm.get('permissions_form').patchValue(permissions)
component.save()
expect(result).toEqual({
name: '',
showInSideBar: false,
showOnDashboard: false,
permissions_form: permissions,
})
})
it('should support default name', () => { it('should support default name', () => {
const saveClickedSpy = jest.spyOn(component.saveClicked, 'emit') const saveClickedSpy = jest.spyOn(component.saveClicked, 'emit')
const modalCloseSpy = jest.spyOn(modal, 'close') const modalCloseSpy = jest.spyOn(modal, 'close')

View File

@@ -13,14 +13,22 @@ import {
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { User } from 'src/app/data/user'
import { CheckComponent } from '../../common/input/check/check.component' import { CheckComponent } from '../../common/input/check/check.component'
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
import { TextComponent } from '../../common/input/text/text.component' import { TextComponent } from '../../common/input/text/text.component'
@Component({ @Component({
selector: 'pngx-save-view-config-dialog', selector: 'pngx-save-view-config-dialog',
templateUrl: './save-view-config-dialog.component.html', templateUrl: './save-view-config-dialog.component.html',
styleUrls: ['./save-view-config-dialog.component.scss'], styleUrls: ['./save-view-config-dialog.component.scss'],
imports: [CheckComponent, TextComponent, FormsModule, ReactiveFormsModule], imports: [
CheckComponent,
TextComponent,
PermissionsFormComponent,
FormsModule,
ReactiveFormsModule,
],
}) })
export class SaveViewConfigDialogComponent implements OnInit { export class SaveViewConfigDialogComponent implements OnInit {
private modal = inject(NgbActiveModal) private modal = inject(NgbActiveModal)
@@ -36,6 +44,8 @@ export class SaveViewConfigDialogComponent implements OnInit {
closeEnabled = false closeEnabled = false
users: User[]
_defaultName = '' _defaultName = ''
get defaultName() { get defaultName() {
@@ -52,6 +62,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
name: new FormControl(''), name: new FormControl(''),
showInSideBar: new FormControl(false), showInSideBar: new FormControl(false),
showOnDashboard: new FormControl(false), showOnDashboard: new FormControl(false),
permissions_form: new FormControl(null),
}) })
ngOnInit(): void { ngOnInit(): void {
@@ -62,7 +73,16 @@ export class SaveViewConfigDialogComponent implements OnInit {
} }
save() { save() {
this.saveClicked.emit(this.saveViewConfigForm.value) const formValue = this.saveViewConfigForm.value
const saveViewConfig = {
name: formValue.name,
showInSideBar: formValue.showInSideBar,
showOnDashboard: formValue.showOnDashboard,
}
if (formValue.permissions_form) {
saveViewConfig['permissions_form'] = formValue.permissions_form
}
this.saveClicked.emit(saveViewConfig)
} }
cancel() { cancel() {

View File

@@ -25,15 +25,23 @@
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label> @if (canDeleteSavedView(view)) {
<pngx-confirm-button <label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
label="Delete" <button
i18n-label class="btn btn-sm btn-outline-secondary form-control mb-2"
(confirm)="deleteSavedView(view)" type="button"
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" (click)="editPermissions(view)"
buttonClasses="btn-sm btn-outline-danger form-control" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }"
iconName="trash"> i18n><i-bs class="me-1" name="person-fill-lock"></i-bs>Permissions</button>
</pngx-confirm-button> <pngx-confirm-button
label="Delete"
i18n-label
(confirm)="deleteSavedView(view)"
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
buttonClasses="btn-sm btn-outline-danger form-control"
iconName="trash">
</pngx-confirm-button>
}
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@@ -3,16 +3,16 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser' import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { Subject, of, throwError } from 'rxjs'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { CheckComponent } from '../../common/input/check/check.component' import { CheckComponent } from '../../common/input/check/check.component'
@@ -32,7 +32,9 @@ describe('SavedViewsComponent', () => {
let component: SavedViewsComponent let component: SavedViewsComponent
let fixture: ComponentFixture<SavedViewsComponent> let fixture: ComponentFixture<SavedViewsComponent>
let savedViewService: SavedViewService let savedViewService: SavedViewService
let settingsService: SettingsService
let toastService: ToastService let toastService: ToastService
let modalService: NgbModal
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -57,6 +59,8 @@ describe('SavedViewsComponent', () => {
provide: PermissionsService, provide: PermissionsService,
useValue: { useValue: {
currentUserCan: () => true, currentUserCan: () => true,
currentUserHasObjectPermissions: () => true,
currentUserOwnsObject: () => true,
}, },
}, },
{ {
@@ -77,11 +81,13 @@ describe('SavedViewsComponent', () => {
}).compileComponents() }).compileComponents()
savedViewService = TestBed.inject(SavedViewService) savedViewService = TestBed.inject(SavedViewService)
settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
modalService = TestBed.inject(NgbModal)
fixture = TestBed.createComponent(SavedViewsComponent) fixture = TestBed.createComponent(SavedViewsComponent)
component = fixture.componentInstance component = fixture.componentInstance
jest.spyOn(savedViewService, 'listAll').mockReturnValue( jest.spyOn(savedViewService, 'list').mockReturnValue(
of({ of({
all: savedViews.map((v) => v.id), all: savedViews.map((v) => v.id),
count: savedViews.length, count: savedViews.length,
@@ -94,14 +100,13 @@ describe('SavedViewsComponent', () => {
it('should support save saved views, show error', () => { it('should support save saved views, show error', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany') const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
const control = component.savedViewsForm
const toggle = fixture.debugElement.query( .get('savedViews')
By.css('.form-check.form-switch input') .get(savedViews[0].id.toString())
) .get('name')
toggle.nativeElement.checked = true control.setValue(`${savedViews[0].name}-changed`)
toggle.nativeElement.dispatchEvent(new Event('change')) control.markAsDirty()
// saved views error first // saved views error first
savedViewPatchSpy.mockReturnValueOnce( savedViewPatchSpy.mockReturnValueOnce(
@@ -110,12 +115,13 @@ describe('SavedViewsComponent', () => {
component.save() component.save()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled() expect(savedViewPatchSpy).toHaveBeenCalled()
toastSpy.mockClear()
toastErrorSpy.mockClear() toastErrorSpy.mockClear()
savedViewPatchSpy.mockClear() savedViewPatchSpy.mockClear()
// succeed saved views // succeed saved views
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[])) savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
control.setValue(savedViews[0].name)
control.markAsDirty()
component.save() component.save()
expect(toastErrorSpy).not.toHaveBeenCalled() expect(toastErrorSpy).not.toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled() expect(savedViewPatchSpy).toHaveBeenCalled()
@@ -127,26 +133,65 @@ describe('SavedViewsComponent', () => {
expect(patchSpy).not.toHaveBeenCalled() expect(patchSpy).not.toHaveBeenCalled()
const view = savedViews[0] const view = savedViews[0]
const toggle = fixture.debugElement.query( component.savedViewsForm
By.css('.form-check.form-switch input') .get('savedViews')
) .get(view.id.toString())
toggle.nativeElement.checked = true .get('name')
toggle.nativeElement.dispatchEvent(new Event('change')) .setValue('changed-view-name')
// register change component.savedViewsForm
component.savedViewsForm.get('savedViews').get(view.id.toString()).value[ .get('savedViews')
'show_on_dashboard' .get(view.id.toString())
] = !view.show_on_dashboard .get('name')
.markAsDirty()
fixture.detectChanges() fixture.detectChanges()
component.save() component.save()
expect(patchSpy).toHaveBeenCalledWith([ expect(patchSpy).toHaveBeenCalled()
{ const patchBody = patchSpy.mock.calls[0][0][0]
id: view.id, expect(patchBody).toMatchObject({
name: view.name, id: view.id,
show_in_sidebar: view.show_in_sidebar, name: 'changed-view-name',
show_on_dashboard: !view.show_on_dashboard, })
}, expect(patchBody.show_on_dashboard).toBeUndefined()
]) expect(patchBody.show_in_sidebar).toBeUndefined()
})
it('should persist visibility changes to user settings', () => {
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
const updateVisibilitySpy = jest
.spyOn(settingsService, 'updateSavedViewsVisibility')
.mockReturnValue(of({ success: true }))
const dashboardControl = component.savedViewsForm
.get('savedViews')
.get(savedViews[0].id.toString())
.get('show_on_dashboard')
dashboardControl.setValue(false)
dashboardControl.markAsDirty()
component.save()
expect(patchSpy).not.toHaveBeenCalled()
expect(updateVisibilitySpy).toHaveBeenCalledWith([], [savedViews[0].id])
})
it('should skip model updates for views that cannot be edited', () => {
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
const updateVisibilitySpy = jest.spyOn(
settingsService,
'updateSavedViewsVisibility'
)
const nameControl = component.savedViewsForm
.get('savedViews')
.get(savedViews[0].id.toString())
.get('name')
nameControl.disable()
component.save()
expect(patchSpy).not.toHaveBeenCalled()
expect(updateVisibilitySpy).not.toHaveBeenCalled()
}) })
it('should support delete saved view', () => { it('should support delete saved view', () => {
@@ -162,14 +207,55 @@ describe('SavedViewsComponent', () => {
it('should support reset', () => { it('should support reset', () => {
const view = savedViews[0] const view = savedViews[0]
component.savedViewsForm.get('savedViews').get(view.id.toString()).value[ component.savedViewsForm
'show_on_dashboard' .get('savedViews')
] = !view.show_on_dashboard .get(view.id.toString())
.get('show_on_dashboard')
.setValue(!view.show_on_dashboard)
component.reset() component.reset()
expect( expect(
component.savedViewsForm.get('savedViews').get(view.id.toString()).value[ component.savedViewsForm
'show_on_dashboard' .get('savedViews')
] .get(view.id.toString())
.get('show_on_dashboard').value
).toEqual(view.show_on_dashboard) ).toEqual(view.show_on_dashboard)
}) })
it('should support editing permissions', () => {
const confirmClicked = new Subject<any>()
const modalRef = {
componentInstance: {
confirmClicked,
buttonsEnabled: true,
},
close: jest.fn(),
} as any
jest.spyOn(modalService, 'open').mockReturnValue(modalRef)
const patchSpy = jest.spyOn(savedViewService, 'patch')
patchSpy.mockReturnValue(of(savedViews[0] as SavedView))
component.editPermissions(savedViews[0] as SavedView)
confirmClicked.next({
permissions: {
owner: 1,
set_permissions: {
view: { users: [2], groups: [] },
change: { users: [], groups: [3] },
},
},
merge: true,
})
expect(patchSpy).toHaveBeenCalledWith(
expect.objectContaining({
id: savedViews[0].id,
owner: 1,
set_permissions: {
view: { users: [2], groups: [] },
change: { users: [], groups: [3] },
},
})
)
expect(modalRef.close).toHaveBeenCalled()
})
}) })

View File

@@ -6,11 +6,18 @@ import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { dirtyCheck } from '@ngneat/dirty-check-forms' import { dirtyCheck } from '@ngneat/dirty-check-forms'
import { BehaviorSubject, Observable, takeUntil } from 'rxjs' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
import { DisplayMode } from 'src/app/data/document' import { DisplayMode } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import {
PermissionAction,
PermissionsService,
} from 'src/app/services/permissions.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
@@ -34,15 +41,18 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
AsyncPipe, AsyncPipe,
NgxBootstrapIconsModule,
], ],
}) })
export class SavedViewsComponent export class SavedViewsComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
private savedViewService = inject(SavedViewService) private readonly savedViewService = inject(SavedViewService)
private settings = inject(SettingsService) private readonly permissionsService = inject(PermissionsService)
private toastService = inject(ToastService) private readonly settings = inject(SettingsService)
private readonly toastService = inject(ToastService)
private readonly modalService = inject(NgbModal)
DisplayMode = DisplayMode DisplayMode = DisplayMode
@@ -65,11 +75,17 @@ export class SavedViewsComponent
} }
ngOnInit(): void { ngOnInit(): void {
this.reloadViews()
}
private reloadViews(): void {
this.loading = true this.loading = true
this.savedViewService.listAll().subscribe((r) => { this.savedViewService
this.savedViews = r.results .list(null, null, null, false, { full_perms: true })
this.initialize() .subscribe((r) => {
}) this.savedViews = r.results
this.initialize()
})
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@@ -95,16 +111,20 @@ export class SavedViewsComponent
display_mode: view.display_mode, display_mode: view.display_mode,
display_fields: view.display_fields, display_fields: view.display_fields,
} }
const canEdit = this.canEditSavedView(view)
this.savedViewsGroup.addControl( this.savedViewsGroup.addControl(
view.id.toString(), view.id.toString(),
new FormGroup({ new FormGroup({
id: new FormControl(null), id: new FormControl({ value: null, disabled: !canEdit }),
name: new FormControl(null), name: new FormControl({ value: null, disabled: !canEdit }),
show_on_dashboard: new FormControl(null), show_on_dashboard: new FormControl({
show_in_sidebar: new FormControl(null), value: null,
page_size: new FormControl(null), disabled: false,
display_mode: new FormControl(null), }),
display_fields: new FormControl([]), show_in_sidebar: new FormControl({ value: null, disabled: false }),
page_size: new FormControl({ value: null, disabled: !canEdit }),
display_mode: new FormControl({ value: null, disabled: !canEdit }),
display_fields: new FormControl({ value: [], disabled: !canEdit }),
}) })
) )
} }
@@ -133,10 +153,7 @@ export class SavedViewsComponent
$localize`Saved view "${savedView.name}" deleted.` $localize`Saved view "${savedView.name}" deleted.`
) )
this.savedViewService.clearCache() this.savedViewService.clearCache()
this.savedViewService.listAll().subscribe((r) => { this.reloadViews()
this.savedViews = r.results
this.initialize()
})
}) })
} }
@@ -145,26 +162,120 @@ export class SavedViewsComponent
} }
public save() { public save() {
// only patch views that have actually changed // Save only changed views, then save the visibility changes into user settings.
const groups = Object.values(this.savedViewsGroup.controls) as FormGroup[]
const visibilityChanged = groups.some(
(group) =>
group.get('show_on_dashboard')?.dirty ||
group.get('show_in_sidebar')?.dirty
)
const changed: SavedView[] = [] const changed: SavedView[] = []
Object.values(this.savedViewsGroup.controls) const dashboardVisibleIds: number[] = []
.filter((g: FormGroup) => !g.pristine) const sidebarVisibleIds: number[] = []
.forEach((group: FormGroup) => {
changed.push(group.value) groups.forEach((group) => {
}) const value = group.getRawValue()
if (value.show_on_dashboard) {
dashboardVisibleIds.push(value.id)
}
if (value.show_in_sidebar) {
sidebarVisibleIds.push(value.id)
}
// Would be fine to send, but no longer stored on the model
delete value.show_on_dashboard
delete value.show_in_sidebar
if (!group.get('name')?.enabled) {
// Quick check for user doesn't have permissions, then bail
return
}
const modelFieldsChanged =
group.get('name')?.dirty ||
group.get('page_size')?.dirty ||
group.get('display_mode')?.dirty ||
group.get('display_fields')?.dirty
if (!modelFieldsChanged) {
return
}
changed.push(value)
})
if (!changed.length && !visibilityChanged) {
return
}
let saveOperation = of([])
if (changed.length) { if (changed.length) {
this.savedViewService.patchMany(changed).subscribe({ saveOperation = saveOperation.pipe(
switchMap(() => this.savedViewService.patchMany(changed))
)
}
if (visibilityChanged) {
saveOperation = saveOperation.pipe(
switchMap(() =>
this.settings.updateSavedViewsVisibility(
dashboardVisibleIds,
sidebarVisibleIds
)
)
)
}
saveOperation.subscribe({
next: () => {
this.toastService.showInfo($localize`Views saved successfully.`)
this.savedViewService.clearCache()
this.reloadViews()
},
error: (error) => {
this.toastService.showError($localize`Error while saving views.`, error)
},
})
}
public canEditSavedView(view: SavedView): boolean {
return this.permissionsService.currentUserHasObjectPermissions(
PermissionAction.Change,
view
)
}
public canDeleteSavedView(view: SavedView): boolean {
return this.permissionsService.currentUserOwnsObject(view)
}
public editPermissions(savedView: SavedView): void {
const modal = this.modalService.open(PermissionsDialogComponent, {
backdrop: 'static',
})
const dialog = modal.componentInstance as PermissionsDialogComponent
dialog.object = savedView
dialog.note = $localize`Note: Sharing saved views does not share the underlying documents.`
modal.componentInstance.confirmClicked.subscribe(({ permissions }) => {
modal.componentInstance.buttonsEnabled = false
const view = {
id: savedView.id,
owner: permissions.owner,
}
view['set_permissions'] = permissions.set_permissions
this.savedViewService.patch(view as SavedView).subscribe({
next: () => { next: () => {
this.toastService.showInfo($localize`Views saved successfully.`) this.toastService.showInfo($localize`Permissions updated`)
this.store.next(this.savedViewsForm.value) modal.close()
this.reloadViews()
}, },
error: (error) => { error: (error) => {
this.toastService.showError( this.toastService.showError(
$localize`Error while saving views.`, $localize`Error updating permissions`,
error error
) )
}, },
}) })
} })
} }
} }

View File

@@ -128,15 +128,15 @@ export interface Document extends ObjectWithPermissions {
checksum?: string checksum?: string
// UTC // UTC
created?: Date created?: string // ISO string
modified?: Date modified?: string // ISO string
added?: Date added?: string // ISO string
mime_type?: string mime_type?: string
deleted_at?: Date deleted_at?: string // ISO string
original_file_name?: string original_file_name?: string

View File

@@ -62,6 +62,10 @@ export const SETTINGS_KEYS = {
'general-settings:update-checking:backend-setting', 'general-settings:update-checking:backend-setting',
SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE: SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE:
'general-settings:saved-views:warn-on-unsaved-change', 'general-settings:saved-views:warn-on-unsaved-change',
DASHBOARD_VIEWS_VISIBLE_IDS:
'general-settings:saved-views:dashboard-views-visible-ids',
SIDEBAR_VIEWS_VISIBLE_IDS:
'general-settings:saved-views:sidebar-views-visible-ids',
DASHBOARD_VIEWS_SORT_ORDER: DASHBOARD_VIEWS_SORT_ORDER:
'general-settings:saved-views:dashboard-views-sort-order', 'general-settings:saved-views:dashboard-views-sort-order',
SIDEBAR_VIEWS_SORT_ORDER: SIDEBAR_VIEWS_SORT_ORDER:
@@ -248,6 +252,16 @@ export const SETTINGS: UiSetting[] = [
type: 'array', type: 'array',
default: [], default: [],
}, },
{
key: SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS,
type: 'array',
default: [],
},
{
key: SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS,
type: 'array',
default: [],
},
{ {
key: SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER, key: SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER,
type: 'array', type: 'array',

View File

@@ -0,0 +1,7 @@
export interface WebsocketDocumentUpdatedMessage {
document_id: number
modified: string
owner_id?: number
users_can_view?: number[]
groups_can_view?: number[]
}

View File

@@ -57,6 +57,11 @@ describe(`Additional service tests for SavedViewService`, () => {
let settingsService let settingsService
it('should retrieve saved views and sort them', () => { it('should retrieve saved views and sort them', () => {
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS) return [1, 2, 3]
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS) return [1, 2, 3]
return []
})
service.reload() service.reload()
const req = httpTestingController.expectOne( const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000` `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
@@ -93,7 +98,9 @@ describe(`Additional service tests for SavedViewService`, () => {
it('should sort dashboard views', () => { it('should sort dashboard views', () => {
service['savedViews'] = saved_views service['savedViews'] = saved_views
jest.spyOn(settingsService, 'get').mockImplementation((key) => { jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS) return [1, 2, 3]
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [3, 1, 2] if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [3, 1, 2]
return []
}) })
expect(service.dashboardViews).toEqual([ expect(service.dashboardViews).toEqual([
saved_views[2], saved_views[2],
@@ -102,10 +109,21 @@ describe(`Additional service tests for SavedViewService`, () => {
]) ])
}) })
it('should use user-specific dashboard visibility when configured', () => {
service['savedViews'] = saved_views
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS) return [4, 2]
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return []
})
expect(service.dashboardViews).toEqual([saved_views[1], saved_views[3]])
})
it('should sort sidebar views', () => { it('should sort sidebar views', () => {
service['savedViews'] = saved_views service['savedViews'] = saved_views
jest.spyOn(settingsService, 'get').mockImplementation((key) => { jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS) return [1, 2, 3]
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return [3, 1, 2] if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return [3, 1, 2]
return []
}) })
expect(service.sidebarViews).toEqual([ expect(service.sidebarViews).toEqual([
saved_views[2], saved_views[2],
@@ -114,6 +132,15 @@ describe(`Additional service tests for SavedViewService`, () => {
]) ])
}) })
it('should use user-specific sidebar visibility when configured', () => {
service['savedViews'] = saved_views
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS) return [4, 2]
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return []
})
expect(service.sidebarViews).toEqual([saved_views[1], saved_views[3]])
})
it('should treat empty display_fields as null', () => { it('should treat empty display_fields as null', () => {
subscription = service subscription = service
.patch({ .patch({

View File

@@ -36,7 +36,9 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
return super.list(page, pageSize, sortField, sortReverse, extraParams).pipe( return super.list(page, pageSize, sortField, sortReverse, extraParams).pipe(
tap({ tap({
next: (r) => { next: (r) => {
this.savedViews = r.results const views = r.results.map((view) => this.withUserVisibility(view))
this.savedViews = views
r.results = views
this._loading = false this._loading = false
this.settingsService.dashboardIsEmpty = this.settingsService.dashboardIsEmpty =
this.dashboardViews.length === 0 this.dashboardViews.length === 0
@@ -65,8 +67,35 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
return this.savedViews return this.savedViews
} }
private getVisibleViewIds(setting: string): number[] {
const configured = this.settingsService.get(setting)
return Array.isArray(configured) ? configured : []
}
private withUserVisibility(view: SavedView): SavedView {
return {
...view,
show_on_dashboard: this.isDashboardVisible(view),
show_in_sidebar: this.isSidebarVisible(view),
}
}
private isDashboardVisible(view: SavedView): boolean {
const visibleIds = this.getVisibleViewIds(
SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS
)
return visibleIds.includes(view.id)
}
private isSidebarVisible(view: SavedView): boolean {
const visibleIds = this.getVisibleViewIds(
SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS
)
return visibleIds.includes(view.id)
}
get sidebarViews(): SavedView[] { get sidebarViews(): SavedView[] {
const sidebarViews = this.savedViews.filter((v) => v.show_in_sidebar) const sidebarViews = this.savedViews.filter((v) => this.isSidebarVisible(v))
const sorted: number[] = this.settingsService.get( const sorted: number[] = this.settingsService.get(
SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER
@@ -81,7 +110,9 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
} }
get dashboardViews(): SavedView[] { get dashboardViews(): SavedView[] {
const dashboardViews = this.savedViews.filter((v) => v.show_on_dashboard) const dashboardViews = this.savedViews.filter((v) =>
this.isDashboardVisible(v)
)
const sorted: number[] = this.settingsService.get( const sorted: number[] = this.settingsService.get(
SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER

View File

@@ -320,7 +320,7 @@ describe('SettingsService', () => {
expect(req.request.method).toEqual('POST') expect(req.request.method).toEqual('POST')
}) })
it('should update saved view sorting', () => { it('should update saved view sorting and visibility', () => {
httpTestingController httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`) .expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush(ui_settings) .flush(ui_settings)
@@ -341,6 +341,15 @@ describe('SettingsService', () => {
SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER, SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER,
[1, 4] [1, 4]
) )
settingsService.updateSavedViewsVisibility([1, 4], [4, 1])
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS,
[1, 4]
)
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS,
[4, 1]
)
httpTestingController httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`) .expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush(ui_settings) .flush(ui_settings)

View File

@@ -699,4 +699,17 @@ export class SettingsService {
]) ])
return this.storeSettings() return this.storeSettings()
} }
updateSavedViewsVisibility(
dashboardVisibleViewIds: number[],
sidebarVisibleViewIds: number[]
): Observable<any> {
this.set(SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS, [
...new Set(dashboardVisibleViewIds),
])
this.set(SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS, [
...new Set(sidebarVisibleViewIds),
])
return this.storeSettings()
}
} }

View File

@@ -416,4 +416,42 @@ describe('ConsumerStatusService', () => {
websocketStatusService.disconnect() websocketStatusService.disconnect()
expect(deleted).toBeTruthy() expect(deleted).toBeTruthy()
}) })
it('should trigger updated subject on document updated', () => {
let updated = false
websocketStatusService.onDocumentUpdated().subscribe((data) => {
updated = true
expect(data.document_id).toEqual(12)
})
websocketStatusService.connect()
server.send({
type: WebsocketStatusType.DOCUMENT_UPDATED,
data: {
document_id: 12,
modified: '2026-02-17T00:00:00Z',
owner_id: 1,
},
})
websocketStatusService.disconnect()
expect(updated).toBeTruthy()
})
it('should ignore document updated events the user cannot view', () => {
let updated = false
websocketStatusService.onDocumentUpdated().subscribe(() => {
updated = true
})
websocketStatusService.handleDocumentUpdated({
document_id: 12,
modified: '2026-02-17T00:00:00Z',
owner_id: 2,
users_can_view: [],
groups_can_view: [],
})
expect(updated).toBeFalsy()
})
}) })

View File

@@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { User } from '../data/user' import { User } from '../data/user'
import { WebsocketDocumentUpdatedMessage } from '../data/websocket-document-updated-message'
import { WebsocketDocumentsDeletedMessage } from '../data/websocket-documents-deleted-message' import { WebsocketDocumentsDeletedMessage } from '../data/websocket-documents-deleted-message'
import { WebsocketProgressMessage } from '../data/websocket-progress-message' import { WebsocketProgressMessage } from '../data/websocket-progress-message'
import { SettingsService } from './settings.service' import { SettingsService } from './settings.service'
@@ -9,6 +10,7 @@ import { SettingsService } from './settings.service'
export enum WebsocketStatusType { export enum WebsocketStatusType {
STATUS_UPDATE = 'status_update', STATUS_UPDATE = 'status_update',
DOCUMENTS_DELETED = 'documents_deleted', DOCUMENTS_DELETED = 'documents_deleted',
DOCUMENT_UPDATED = 'document_updated',
} }
// see ProgressStatusOptions in src/documents/plugins/helpers.py // see ProgressStatusOptions in src/documents/plugins/helpers.py
@@ -100,17 +102,20 @@ export enum UploadState {
providedIn: 'root', providedIn: 'root',
}) })
export class WebsocketStatusService { export class WebsocketStatusService {
private settingsService = inject(SettingsService) private readonly settingsService = inject(SettingsService)
private statusWebSocket: WebSocket private statusWebSocket: WebSocket
private consumerStatus: FileStatus[] = [] private consumerStatus: FileStatus[] = []
private documentDetectedSubject = new Subject<FileStatus>() private readonly documentDetectedSubject = new Subject<FileStatus>()
private documentConsumptionFinishedSubject = new Subject<FileStatus>() private readonly documentConsumptionFinishedSubject =
private documentConsumptionFailedSubject = new Subject<FileStatus>() new Subject<FileStatus>()
private documentDeletedSubject = new Subject<boolean>() private readonly documentConsumptionFailedSubject = new Subject<FileStatus>()
private connectionStatusSubject = new Subject<boolean>() private readonly documentDeletedSubject = new Subject<boolean>()
private readonly documentUpdatedSubject =
new Subject<WebsocketDocumentUpdatedMessage>()
private readonly connectionStatusSubject = new Subject<boolean>()
private get(taskId: string, filename?: string) { private get(taskId: string, filename?: string) {
let status = let status =
@@ -176,7 +181,10 @@ export class WebsocketStatusService {
data: messageData, data: messageData,
}: { }: {
type: WebsocketStatusType type: WebsocketStatusType
data: WebsocketProgressMessage | WebsocketDocumentsDeletedMessage data:
| WebsocketProgressMessage
| WebsocketDocumentsDeletedMessage
| WebsocketDocumentUpdatedMessage
} = JSON.parse(ev.data) } = JSON.parse(ev.data)
switch (type) { switch (type) {
@@ -184,6 +192,12 @@ export class WebsocketStatusService {
this.documentDeletedSubject.next(true) this.documentDeletedSubject.next(true)
break break
case WebsocketStatusType.DOCUMENT_UPDATED:
this.handleDocumentUpdated(
messageData as WebsocketDocumentUpdatedMessage
)
break
case WebsocketStatusType.STATUS_UPDATE: case WebsocketStatusType.STATUS_UPDATE:
this.handleProgressUpdate(messageData as WebsocketProgressMessage) this.handleProgressUpdate(messageData as WebsocketProgressMessage)
break break
@@ -191,7 +205,11 @@ export class WebsocketStatusService {
} }
} }
private canViewMessage(messageData: WebsocketProgressMessage): boolean { private canViewMessage(messageData: {
owner_id?: number
users_can_view?: number[]
groups_can_view?: number[]
}): boolean {
// see paperless.consumers.StatusConsumer._can_view // see paperless.consumers.StatusConsumer._can_view
const user: User = this.settingsService.currentUser const user: User = this.settingsService.currentUser
return ( return (
@@ -251,6 +269,15 @@ export class WebsocketStatusService {
} }
} }
handleDocumentUpdated(messageData: WebsocketDocumentUpdatedMessage) {
// fallback if backend didn't restrict message
if (!this.canViewMessage(messageData)) {
return
}
this.documentUpdatedSubject.next(messageData)
}
fail(status: FileStatus, message: string) { fail(status: FileStatus, message: string) {
status.message = message status.message = message
status.phase = FileStatusPhase.FAILED status.phase = FileStatusPhase.FAILED
@@ -304,6 +331,10 @@ export class WebsocketStatusService {
return this.documentDeletedSubject return this.documentDeletedSubject
} }
onDocumentUpdated() {
return this.documentUpdatedSubject
}
onConnectionStatus() { onConnectionStatus() {
return this.connectionStatusSubject.asObservable() return this.connectionStatusSubject.asObservable()
} }

View File

@@ -3,10 +3,10 @@ const base_url = new URL(document.baseURI)
export const environment = { export const environment = {
production: true, production: true,
apiBaseUrl: document.baseURI + 'api/', apiBaseUrl: document.baseURI + 'api/',
apiVersion: '9', // match src/paperless/settings.py apiVersion: '10', // match src/paperless/settings.py
appTitle: 'Paperless-ngx', appTitle: 'Paperless-ngx',
tag: 'prod', tag: 'prod',
version: '2.20.9', version: '2.20.10',
webSocketHost: window.location.host, webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/', webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@@ -15,6 +15,7 @@ class DocumentsConfig(AppConfig):
from documents.signals.handlers import add_to_index from documents.signals.handlers import add_to_index
from documents.signals.handlers import run_workflows_added from documents.signals.handlers import run_workflows_added
from documents.signals.handlers import run_workflows_updated from documents.signals.handlers import run_workflows_updated
from documents.signals.handlers import send_websocket_document_updated
from documents.signals.handlers import set_correspondent from documents.signals.handlers import set_correspondent
from documents.signals.handlers import set_document_type from documents.signals.handlers import set_document_type
from documents.signals.handlers import set_storage_path from documents.signals.handlers import set_storage_path
@@ -29,6 +30,7 @@ class DocumentsConfig(AppConfig):
document_consumption_finished.connect(run_workflows_added) document_consumption_finished.connect(run_workflows_added)
document_consumption_finished.connect(add_or_update_document_in_llm_index) document_consumption_finished.connect(add_or_update_document_in_llm_index)
document_updated.connect(run_workflows_updated) document_updated.connect(run_workflows_updated)
document_updated.connect(send_websocket_document_updated)
import documents.schema # noqa: F401 import documents.schema # noqa: F401

View File

@@ -1,5 +1,5 @@
from datetime import UTC
from datetime import datetime from datetime import datetime
from datetime import timezone
from typing import Any from typing import Any
from django.conf import settings from django.conf import settings
@@ -139,7 +139,7 @@ def thumbnail_last_modified(request: Any, pk: int) -> datetime | None:
# No cache, get the timestamp and cache the datetime # No cache, get the timestamp and cache the datetime
last_modified = datetime.fromtimestamp( last_modified = datetime.fromtimestamp(
doc.thumbnail_path.stat().st_mtime, doc.thumbnail_path.stat().st_mtime,
tz=timezone.utc, tz=UTC,
) )
cache.set(doc_key, last_modified, CACHE_50_MINUTES) cache.set(doc_key, last_modified, CACHE_50_MINUTES)
return last_modified return last_modified

View File

@@ -2,7 +2,7 @@ import datetime
import hashlib import hashlib
import os import os
import tempfile import tempfile
from enum import Enum from enum import StrEnum
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Final from typing import Final
@@ -11,6 +11,7 @@ import magic
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
from django.db.models import Max
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from filelock import FileLock from filelock import FileLock
@@ -20,6 +21,7 @@ from documents.classifier import load_classifier
from documents.data_models import ConsumableDocument from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentMetadataOverrides
from documents.file_handling import create_source_path_directory from documents.file_handling import create_source_path_directory
from documents.file_handling import generate_filename
from documents.file_handling import generate_unique_filename from documents.file_handling import generate_unique_filename
from documents.loggers import LoggingMixin from documents.loggers import LoggingMixin
from documents.models import Correspondent from documents.models import Correspondent
@@ -81,7 +83,7 @@ class ConsumerError(Exception):
pass pass
class ConsumerStatusShortMessage(str, Enum): class ConsumerStatusShortMessage(StrEnum):
DOCUMENT_ALREADY_EXISTS = "document_already_exists" DOCUMENT_ALREADY_EXISTS = "document_already_exists"
DOCUMENT_ALREADY_EXISTS_IN_TRASH = "document_already_exists_in_trash" DOCUMENT_ALREADY_EXISTS_IN_TRASH = "document_already_exists_in_trash"
ASN_ALREADY_EXISTS = "asn_already_exists" ASN_ALREADY_EXISTS = "asn_already_exists"
@@ -123,22 +125,6 @@ class ConsumerPluginMixin:
self.filename = self.metadata.filename or self.input_doc.original_file.name self.filename = self.metadata.filename or self.input_doc.original_file.name
if input_doc.root_document_id:
self.log.debug(
f"Document root document id: {input_doc.root_document_id}",
)
root_document = Document.objects.get(pk=input_doc.root_document_id)
version_index = Document.objects.filter(root_document=root_document).count()
filename_path = Path(self.filename)
if filename_path.suffix:
self.filename = str(
filename_path.with_name(
f"{filename_path.stem}_v{version_index}{filename_path.suffix}",
),
)
else:
self.filename = f"{self.filename}_v{version_index}"
def _send_progress( def _send_progress(
self, self,
current_progress: int, current_progress: int,
@@ -184,7 +170,7 @@ class ConsumerPlugin(
): ):
logging_name = LOGGING_NAME logging_name = LOGGING_NAME
def _clone_root_into_version( def _create_version_from_root(
self, self,
root_doc: Document, root_doc: Document,
*, *,
@@ -193,30 +179,38 @@ class ConsumerPlugin(
mime_type: str, mime_type: str,
) -> Document: ) -> Document:
self.log.debug("Saving record for updated version to database") self.log.debug("Saving record for updated version to database")
version_doc = Document.objects.get(pk=root_doc.pk) root_doc_frozen = Document.objects.select_for_update().get(pk=root_doc.pk)
setattr(version_doc, "pk", None) next_version_index = (
version_doc.root_document = root_doc Document.global_objects.filter(
root_document_id=root_doc_frozen.pk,
).aggregate(
max_index=Max("version_index"),
)["max_index"]
or 0
)
file_for_checksum = ( file_for_checksum = (
self.unmodified_original self.unmodified_original
if self.unmodified_original is not None if self.unmodified_original is not None
else self.working_copy else self.working_copy
) )
version_doc.checksum = hashlib.md5( version_doc = Document(
file_for_checksum.read_bytes(), root_document=root_doc_frozen,
).hexdigest() version_index=next_version_index + 1,
version_doc.content = text or "" checksum=hashlib.md5(
version_doc.page_count = page_count file_for_checksum.read_bytes(),
version_doc.mime_type = mime_type ).hexdigest(),
version_doc.original_filename = self.filename content=text or "",
version_doc.storage_path = root_doc.storage_path page_count=page_count,
# Clear unique file path fields so they can be generated uniquely later mime_type=mime_type,
version_doc.filename = None original_filename=self.filename,
version_doc.archive_filename = None owner_id=root_doc_frozen.owner_id,
version_doc.archive_checksum = None created=root_doc_frozen.created,
title=root_doc_frozen.title,
added=timezone.now(),
modified=timezone.now(),
)
if self.metadata.version_label is not None: if self.metadata.version_label is not None:
version_doc.version_label = self.metadata.version_label version_doc.version_label = self.metadata.version_label
version_doc.added = timezone.now()
version_doc.modified = timezone.now()
return version_doc return version_doc
def run_pre_consume_script(self) -> None: def run_pre_consume_script(self) -> None:
@@ -542,7 +536,7 @@ class ConsumerPlugin(
root_doc = Document.objects.get( root_doc = Document.objects.get(
pk=self.input_doc.root_document_id, pk=self.input_doc.root_document_id,
) )
original_document = self._clone_root_into_version( original_document = self._create_version_from_root(
root_doc, root_doc,
text=text, text=text,
page_count=page_count, page_count=page_count,
@@ -611,7 +605,19 @@ class ConsumerPlugin(
# After everything is in the database, copy the files into # After everything is in the database, copy the files into
# place. If this fails, we'll also rollback the transaction. # place. If this fails, we'll also rollback the transaction.
with FileLock(settings.MEDIA_LOCK): with FileLock(settings.MEDIA_LOCK):
document.filename = generate_unique_filename(document) generated_filename = generate_unique_filename(document)
if (
len(str(generated_filename))
> Document.MAX_STORED_FILENAME_LENGTH
):
self.log.warning(
"Generated source filename exceeds db path limit, falling back to default naming",
)
generated_filename = generate_filename(
document,
use_format=False,
)
document.filename = generated_filename
create_source_path_directory(document.source_path) create_source_path_directory(document.source_path)
self._write( self._write(
@@ -627,10 +633,23 @@ class ConsumerPlugin(
) )
if archive_path and Path(archive_path).is_file(): if archive_path and Path(archive_path).is_file():
document.archive_filename = generate_unique_filename( generated_archive_filename = generate_unique_filename(
document, document,
archive_filename=True, archive_filename=True,
) )
if (
len(str(generated_archive_filename))
> Document.MAX_STORED_FILENAME_LENGTH
):
self.log.warning(
"Generated archive filename exceeds db path limit, falling back to default naming",
)
generated_archive_filename = generate_filename(
document,
archive_filename=True,
use_format=False,
)
document.archive_filename = generated_archive_filename
create_source_path_directory(document.archive_path) create_source_path_directory(document.archive_path)
self._write( self._write(
archive_path, archive_path,

View File

@@ -127,23 +127,34 @@ def generate_filename(
*, *,
counter=0, counter=0,
archive_filename=False, archive_filename=False,
use_format=True,
) -> Path: ) -> Path:
# version docs use the root document for formatting, just with a suffix
context_doc = doc if doc.root_document_id is None else doc.root_document
version_suffix = (
f"_v{doc.version_index}"
if doc.root_document_id is not None and doc.version_index is not None
else ""
)
base_path: Path | None = None base_path: Path | None = None
# Determine the source of the format string # Determine the source of the format string
if doc.storage_path is not None: if use_format:
filename_format = doc.storage_path.path if context_doc.storage_path is not None:
elif settings.FILENAME_FORMAT is not None: filename_format = context_doc.storage_path.path
# Maybe convert old to new style elif settings.FILENAME_FORMAT is not None:
filename_format = convert_format_str_to_template_format( # Maybe convert old to new style
settings.FILENAME_FORMAT, filename_format = convert_format_str_to_template_format(
) settings.FILENAME_FORMAT,
)
else:
filename_format = None
else: else:
filename_format = None filename_format = None
# If we have one, render it # If we have one, render it
if filename_format is not None: if filename_format is not None:
rendered_path: str | None = format_filename(doc, filename_format) rendered_path: str | None = format_filename(context_doc, filename_format)
if rendered_path: if rendered_path:
base_path = Path(rendered_path) base_path = Path(rendered_path)
@@ -157,7 +168,7 @@ def generate_filename(
base_filename = base_path.name base_filename = base_path.name
# Build the final filename with counter and filetype # Build the final filename with counter and filetype
final_filename = f"{base_filename}{counter_str}{filetype_str}" final_filename = f"{base_filename}{version_suffix}{counter_str}{filetype_str}"
# If we have a directory component, include it # If we have a directory component, include it
if str(directory) != ".": if str(directory) != ".":
@@ -166,7 +177,9 @@ def generate_filename(
full_path = Path(final_filename) full_path = Path(final_filename)
else: else:
# No template, use document ID # No template, use document ID
final_filename = f"{doc.pk:07}{counter_str}{filetype_str}" final_filename = (
f"{context_doc.pk:07}{version_suffix}{counter_str}{filetype_str}"
)
full_path = Path(final_filename) full_path = Path(final_filename)
return full_path return full_path

View File

@@ -5,10 +5,10 @@ import math
import re import re
from collections import Counter from collections import Counter
from contextlib import contextmanager from contextlib import contextmanager
from datetime import UTC
from datetime import datetime from datetime import datetime
from datetime import time from datetime import time
from datetime import timedelta from datetime import timedelta
from datetime import timezone
from shutil import rmtree from shutil import rmtree
from time import sleep from time import sleep
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -437,7 +437,7 @@ class ManualResults:
class LocalDateParser(English): class LocalDateParser(English):
def reverse_timezone_offset(self, d): def reverse_timezone_offset(self, d):
return (d.replace(tzinfo=django_timezone.get_current_timezone())).astimezone( return (d.replace(tzinfo=django_timezone.get_current_timezone())).astimezone(
timezone.utc, UTC,
) )
def date_from(self, *args, **kwargs): def date_from(self, *args, **kwargs):
@@ -641,8 +641,8 @@ def rewrite_natural_date_keywords(query_string: str) -> str:
end = datetime(local_now.year - 1, 12, 31, 23, 59, 59, tzinfo=tz) end = datetime(local_now.year - 1, 12, 31, 23, 59, 59, tzinfo=tz)
# Convert to UTC and format # Convert to UTC and format
start_str = start.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S") start_str = start.astimezone(UTC).strftime("%Y%m%d%H%M%S")
end_str = end.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S") end_str = end.astimezone(UTC).strftime("%Y%m%d%H%M%S")
return f"{field}:[{start_str} TO {end_str}]" return f"{field}:[{start_str} TO {end_str}]"
return re.sub(pattern, repl, query_string, flags=re.IGNORECASE) return re.sub(pattern, repl, query_string, flags=re.IGNORECASE)

View File

@@ -6,11 +6,14 @@ Provides automatic progress bar and multiprocessing support with minimal boilerp
from __future__ import annotations from __future__ import annotations
import logging
import os import os
from collections.abc import Callable
from collections.abc import Iterable from collections.abc import Iterable
from collections.abc import Sized from collections.abc import Sized
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import as_completed from concurrent.futures import as_completed
from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Any from typing import Any
@@ -22,7 +25,11 @@ from django import db
from django.core.management import CommandError from django.core.management import CommandError
from django.db.models import QuerySet from django.db.models import QuerySet
from django_rich.management import RichCommand from django_rich.management import RichCommand
from rich import box
from rich.console import Console from rich.console import Console
from rich.console import Group
from rich.console import RenderableType
from rich.live import Live
from rich.progress import BarColumn from rich.progress import BarColumn
from rich.progress import MofNCompleteColumn from rich.progress import MofNCompleteColumn
from rich.progress import Progress from rich.progress import Progress
@@ -30,11 +37,11 @@ from rich.progress import SpinnerColumn
from rich.progress import TextColumn from rich.progress import TextColumn
from rich.progress import TimeElapsedColumn from rich.progress import TimeElapsedColumn
from rich.progress import TimeRemainingColumn from rich.progress import TimeRemainingColumn
from rich.table import Table
from rich.text import Text
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Generator from collections.abc import Generator
from collections.abc import Iterable
from collections.abc import Sequence from collections.abc import Sequence
from django.core.management import CommandParser from django.core.management import CommandParser
@@ -43,6 +50,78 @@ T = TypeVar("T")
R = TypeVar("R") R = TypeVar("R")
@dataclass(slots=True, frozen=True)
class _BufferedRecord:
level: int
name: str
message: str
class BufferingLogHandler(logging.Handler):
"""Captures log records during a command run for deferred rendering.
Attach to a logger before a long operation and call ``render()``
afterwards to emit the buffered records via Rich, optionally filtered
by minimum level.
"""
def __init__(self) -> None:
super().__init__()
self._records: list[_BufferedRecord] = []
def emit(self, record: logging.LogRecord) -> None:
self._records.append(
_BufferedRecord(
level=record.levelno,
name=record.name,
message=self.format(record),
),
)
def render(
self,
console: Console,
*,
min_level: int = logging.DEBUG,
title: str = "Log Output",
) -> None:
records = [r for r in self._records if r.level >= min_level]
if not records:
return
table = Table(
title=title,
show_header=True,
header_style="bold",
show_lines=False,
box=box.SIMPLE,
)
table.add_column("Level", style="bold", width=8)
table.add_column("Logger", style="dim")
table.add_column("Message", no_wrap=False)
_level_styles: dict[int, str] = {
logging.DEBUG: "dim",
logging.INFO: "cyan",
logging.WARNING: "yellow",
logging.ERROR: "red",
logging.CRITICAL: "bold red",
}
for record in records:
style = _level_styles.get(record.level, "")
table.add_row(
Text(logging.getLevelName(record.level), style=style),
record.name,
record.message,
)
console.print(table)
def clear(self) -> None:
self._records.clear()
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class ProcessResult(Generic[T, R]): class ProcessResult(Generic[T, R]):
""" """
@@ -91,6 +170,23 @@ class PaperlessCommand(RichCommand):
for result in self.process_parallel(process_doc, ids): for result in self.process_parallel(process_doc, ids):
if result.error: if result.error:
self.console.print(f"[red]Failed: {result.error}[/red]") self.console.print(f"[red]Failed: {result.error}[/red]")
class Command(PaperlessCommand):
help = "Import documents with live stats"
def handle(self, *args, **options):
stats = ImportStats()
def render_stats() -> Table:
... # build Rich Table from stats
for item in self.track_with_stats(
items,
description="Importing...",
stats_renderer=render_stats,
):
result = import_item(item)
stats.imported += 1
""" """
supports_progress_bar: ClassVar[bool] = True supports_progress_bar: ClassVar[bool] = True
@@ -128,13 +224,11 @@ class PaperlessCommand(RichCommand):
This is called by Django's command infrastructure after argument parsing This is called by Django's command infrastructure after argument parsing
but before handle(). We use it to set instance attributes from options. but before handle(). We use it to set instance attributes from options.
""" """
# Set progress bar state
if self.supports_progress_bar: if self.supports_progress_bar:
self.no_progress_bar = options.get("no_progress_bar", False) self.no_progress_bar = options.get("no_progress_bar", False)
else: else:
self.no_progress_bar = True self.no_progress_bar = True
# Set multiprocessing state
if self.supports_multiprocessing: if self.supports_multiprocessing:
self.process_count = options.get("processes", 1) self.process_count = options.get("processes", 1)
if self.process_count < 1: if self.process_count < 1:
@@ -144,9 +238,69 @@ class PaperlessCommand(RichCommand):
return super().execute(*args, **options) return super().execute(*args, **options)
@contextmanager
def buffered_logging(
self,
*logger_names: str,
level: int = logging.DEBUG,
) -> Generator[BufferingLogHandler, None, None]:
"""Context manager that captures log output from named loggers.
Installs a ``BufferingLogHandler`` on each named logger for the
duration of the block, suppressing propagation to avoid interleaving
with the Rich live display. The handler is removed on exit regardless
of whether an exception occurred.
Usage::
with self.buffered_logging("paperless", "documents") as log_buf:
# ... run progress loop ...
if options["verbose"]:
log_buf.render(self.console)
"""
handler = BufferingLogHandler()
handler.setFormatter(logging.Formatter("%(message)s"))
loggers: list[logging.Logger] = []
original_propagate: dict[str, bool] = {}
for name in logger_names:
log = logging.getLogger(name)
log.addHandler(handler)
original_propagate[name] = log.propagate
log.propagate = False
loggers.append(log)
try:
yield handler
finally:
for log in loggers:
log.removeHandler(handler)
log.propagate = original_propagate[log.name]
@staticmethod
def _progress_columns() -> tuple[Any, ...]:
"""
Return the standard set of progress bar columns.
Extracted so both _create_progress (standalone) and track_with_stats
(inside Live) use identical column configuration without duplication.
"""
return (
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
MofNCompleteColumn(),
TimeElapsedColumn(),
TimeRemainingColumn(),
)
def _create_progress(self, description: str) -> Progress: def _create_progress(self, description: str) -> Progress:
""" """
Create a configured Progress instance. Create a standalone Progress instance with its own stderr Console.
Use this for track(). For track_with_stats(), Progress is created
directly inside a Live context instead.
Progress output is directed to stderr to match the convention that Progress output is directed to stderr to match the convention that
progress bars are transient UI feedback, not command output. This progress bars are transient UI feedback, not command output. This
@@ -161,12 +315,7 @@ class PaperlessCommand(RichCommand):
A Progress instance configured with appropriate columns. A Progress instance configured with appropriate columns.
""" """
return Progress( return Progress(
SpinnerColumn(), *self._progress_columns(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
MofNCompleteColumn(),
TimeElapsedColumn(),
TimeRemainingColumn(),
console=Console(stderr=True), console=Console(stderr=True),
transient=False, transient=False,
) )
@@ -222,7 +371,6 @@ class PaperlessCommand(RichCommand):
yield from iterable yield from iterable
return return
# Attempt to determine total if not provided
if total is None: if total is None:
total = self._get_iterable_length(iterable) total = self._get_iterable_length(iterable)
@@ -232,6 +380,87 @@ class PaperlessCommand(RichCommand):
yield item yield item
progress.advance(task_id) progress.advance(task_id)
def track_with_stats(
self,
iterable: Iterable[T],
*,
description: str = "Processing...",
stats_renderer: Callable[[], RenderableType],
total: int | None = None,
) -> Generator[T, None, None]:
"""
Iterate over items with a progress bar and a live-updating stats display.
The progress bar and stats renderable are combined in a single Live
context, so the stats panel re-renders in place below the progress bar
after each item is processed.
Respects --no-progress-bar flag. When disabled, yields items without
any display (stats are still updated by the caller's loop body, so
they will be accurate for any post-loop summary the caller prints).
Args:
iterable: The items to iterate over.
description: Text to display alongside the progress bar.
stats_renderer: Zero-argument callable that returns a Rich
renderable. Called after each item to refresh the display.
The caller typically closes over a mutable dataclass and
rebuilds a Table from it on each call.
total: Total number of items. If None, attempts to determine
automatically via .count() (for querysets) or len().
Yields:
Items from the iterable.
Example:
@dataclass
class Stats:
processed: int = 0
failed: int = 0
stats = Stats()
def render_stats() -> Table:
table = Table(box=None)
table.add_column("Processed")
table.add_column("Failed")
table.add_row(str(stats.processed), str(stats.failed))
return table
for item in self.track_with_stats(
items,
description="Importing...",
stats_renderer=render_stats,
):
try:
import_item(item)
stats.processed += 1
except Exception:
stats.failed += 1
"""
if self.no_progress_bar:
yield from iterable
return
if total is None:
total = self._get_iterable_length(iterable)
stderr_console = Console(stderr=True)
# Progress is created without its own console so Live controls rendering.
progress = Progress(*self._progress_columns())
task_id = progress.add_task(description, total=total)
with Live(
Group(progress, stats_renderer()),
console=stderr_console,
refresh_per_second=4,
) as live:
for item in iterable:
yield item
progress.advance(task_id)
live.update(Group(progress, stats_renderer()))
def process_parallel( def process_parallel(
self, self,
fn: Callable[[T], R], fn: Callable[[T], R],
@@ -269,7 +498,7 @@ class PaperlessCommand(RichCommand):
total = len(items) total = len(items)
if self.process_count == 1: if self.process_count == 1:
# Sequential execution in main process - critical for testing # Sequential execution in main process - critical for testing, so we don't fork in fork, etc
yield from self._process_sequential(fn, items, description, total) yield from self._process_sequential(fn, items, description, total)
else: else:
# Parallel execution with ProcessPoolExecutor # Parallel execution with ProcessPoolExecutor
@@ -298,6 +527,7 @@ class PaperlessCommand(RichCommand):
total: int, total: int,
) -> Generator[ProcessResult[T, R], None, None]: ) -> Generator[ProcessResult[T, R], None, None]:
"""Process items in parallel using ProcessPoolExecutor.""" """Process items in parallel using ProcessPoolExecutor."""
# Close database connections before forking - required for PostgreSQL # Close database connections before forking - required for PostgreSQL
db.connections.close_all() db.connections.close_all()

View File

@@ -3,6 +3,8 @@ import json
import os import os
import shutil import shutil
import tempfile import tempfile
from itertools import chain
from itertools import islice
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -19,6 +21,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core import serializers from django.core import serializers
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from filelock import FileLock from filelock import FileLock
@@ -26,6 +29,8 @@ from guardian.models import GroupObjectPermission
from guardian.models import UserObjectPermission from guardian.models import UserObjectPermission
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Generator
from django.db.models import QuerySet from django.db.models import QuerySet
if settings.AUDIT_LOG_ENABLED: if settings.AUDIT_LOG_ENABLED:
@@ -60,6 +65,22 @@ from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule from paperless_mail.models import MailRule
def serialize_queryset_batched(
queryset: "QuerySet",
*,
batch_size: int = 500,
) -> "Generator[list[dict], None, None]":
"""Yield batches of serialized records from a QuerySet.
Each batch is a list of dicts in Django's Python serialization format.
Uses QuerySet.iterator() to avoid loading the full queryset into memory,
and islice to collect chunk-sized batches serialized in a single call.
"""
iterator = queryset.iterator(chunk_size=batch_size)
while chunk := list(islice(iterator, batch_size)):
yield serializers.serialize("python", chunk)
class Command(CryptMixin, BaseCommand): class Command(CryptMixin, BaseCommand):
help = ( help = (
"Decrypt and rename all files in our collection into a given target " "Decrypt and rename all files in our collection into a given target "
@@ -186,6 +207,17 @@ class Command(CryptMixin, BaseCommand):
help="If provided, is used to encrypt sensitive data in the export", help="If provided, is used to encrypt sensitive data in the export",
) )
parser.add_argument(
"--batch-size",
type=int,
default=500,
help=(
"Number of records to process per batch during serialization. "
"Lower values reduce peak memory usage; higher values improve "
"throughput. Default: 500."
),
)
def handle(self, *args, **options) -> None: def handle(self, *args, **options) -> None:
self.target = Path(options["target"]).resolve() self.target = Path(options["target"]).resolve()
self.split_manifest: bool = options["split_manifest"] self.split_manifest: bool = options["split_manifest"]
@@ -200,6 +232,7 @@ class Command(CryptMixin, BaseCommand):
self.data_only: bool = options["data_only"] self.data_only: bool = options["data_only"]
self.no_progress_bar: bool = options["no_progress_bar"] self.no_progress_bar: bool = options["no_progress_bar"]
self.passphrase: str | None = options.get("passphrase") self.passphrase: str | None = options.get("passphrase")
self.batch_size: int = options["batch_size"]
self.files_in_export_dir: set[Path] = set() self.files_in_export_dir: set[Path] = set()
self.exported_files: set[str] = set() self.exported_files: set[str] = set()
@@ -294,8 +327,13 @@ class Command(CryptMixin, BaseCommand):
# Build an overall manifest # Build an overall manifest
for key, object_query in manifest_key_to_object_query.items(): for key, object_query in manifest_key_to_object_query.items():
manifest_dict[key] = json.loads( manifest_dict[key] = list(
serializers.serialize("json", object_query), chain.from_iterable(
serialize_queryset_batched(
object_query,
batch_size=self.batch_size,
),
),
) )
self.encrypt_secret_fields(manifest_dict) self.encrypt_secret_fields(manifest_dict)
@@ -512,14 +550,24 @@ class Command(CryptMixin, BaseCommand):
self.files_in_export_dir.remove(target) self.files_in_export_dir.remove(target)
if self.compare_json: if self.compare_json:
target_checksum = hashlib.md5(target.read_bytes()).hexdigest() target_checksum = hashlib.md5(target.read_bytes()).hexdigest()
src_str = json.dumps(content, indent=2, ensure_ascii=False) src_str = json.dumps(
content,
cls=DjangoJSONEncoder,
indent=2,
ensure_ascii=False,
)
src_checksum = hashlib.md5(src_str.encode("utf-8")).hexdigest() src_checksum = hashlib.md5(src_str.encode("utf-8")).hexdigest()
if src_checksum == target_checksum: if src_checksum == target_checksum:
perform_write = False perform_write = False
if perform_write: if perform_write:
target.write_text( target.write_text(
json.dumps(content, indent=2, ensure_ascii=False), json.dumps(
content,
cls=DjangoJSONEncoder,
indent=2,
ensure_ascii=False,
),
encoding="utf-8", encoding="utf-8",
) )

View File

@@ -1,4 +1,12 @@
from __future__ import annotations
import logging import logging
from dataclasses import dataclass
from dataclasses import field
from typing import TYPE_CHECKING
from rich.table import Table
from rich.text import Text
from documents.classifier import load_classifier from documents.classifier import load_classifier
from documents.management.commands.base import PaperlessCommand from documents.management.commands.base import PaperlessCommand
@@ -8,9 +16,162 @@ from documents.signals.handlers import set_document_type
from documents.signals.handlers import set_storage_path from documents.signals.handlers import set_storage_path
from documents.signals.handlers import set_tags from documents.signals.handlers import set_tags
if TYPE_CHECKING:
from rich.console import RenderableType
from documents.models import Correspondent
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
logger = logging.getLogger("paperless.management.retagger") logger = logging.getLogger("paperless.management.retagger")
@dataclass(slots=True)
class RetaggerStats:
"""Cumulative counters updated as the retagger processes documents.
Mutable by design -- fields are incremented in the processing loop.
slots=True reduces per-instance memory overhead and speeds attribute access.
"""
correspondents: int = 0
document_types: int = 0
tags_added: int = 0
tags_removed: int = 0
storage_paths: int = 0
documents_processed: int = 0
@dataclass(slots=True)
class DocumentSuggestion:
"""Buffered classifier suggestions for a single document (suggest mode only).
Mutable by design -- fields are assigned incrementally as each setter runs.
"""
document: Document
correspondent: Correspondent | None = None
document_type: DocumentType | None = None
tags_to_add: frozenset[Tag] = field(default_factory=frozenset)
tags_to_remove: frozenset[Tag] = field(default_factory=frozenset)
storage_path: StoragePath | None = None
@property
def has_suggestions(self) -> bool:
return bool(
self.correspondent is not None
or self.document_type is not None
or self.tags_to_add
or self.tags_to_remove
or self.storage_path is not None,
)
def _build_stats_table(stats: RetaggerStats, *, suggest: bool) -> Table:
"""
Build the live-updating stats table shown below the progress bar.
In suggest mode the labels read "would set / would add" to make clear
that nothing has been written to the database.
"""
table = Table(box=None, padding=(0, 2), show_header=True, header_style="bold")
table.add_column("Documents")
table.add_column("Correspondents")
table.add_column("Doc Types")
table.add_column("Tags (+)")
table.add_column("Tags (-)")
table.add_column("Storage Paths")
verb = "would set" if suggest else "set"
table.add_row(
str(stats.documents_processed),
f"{stats.correspondents} {verb}",
f"{stats.document_types} {verb}",
f"+{stats.tags_added}",
f"-{stats.tags_removed}",
f"{stats.storage_paths} {verb}",
)
return table
def _build_suggestion_table(
suggestions: list[DocumentSuggestion],
base_url: str | None,
) -> Table:
"""
Build the final suggestion table printed after the progress bar completes.
Only documents with at least one suggestion are included.
"""
table = Table(
title="Suggested Changes",
show_header=True,
header_style="bold cyan",
show_lines=True,
)
table.add_column("Document", style="bold", no_wrap=False, min_width=20)
table.add_column("Correspondent")
table.add_column("Doc Type")
table.add_column("Tags")
table.add_column("Storage Path")
for suggestion in suggestions:
if not suggestion.has_suggestions:
continue
doc = suggestion.document
if base_url:
doc_cell = Text()
doc_cell.append(str(doc))
doc_cell.append(f"\n{base_url}/documents/{doc.pk}", style="dim")
else:
doc_cell = Text(f"{doc} [{doc.pk}]")
tag_parts: list[str] = []
for tag in sorted(suggestion.tags_to_add, key=lambda t: t.name):
tag_parts.append(f"[green]+{tag.name}[/green]")
for tag in sorted(suggestion.tags_to_remove, key=lambda t: t.name):
tag_parts.append(f"[red]-{tag.name}[/red]")
tag_cell = Text.from_markup(", ".join(tag_parts)) if tag_parts else Text("-")
table.add_row(
doc_cell,
str(suggestion.correspondent) if suggestion.correspondent else "-",
str(suggestion.document_type) if suggestion.document_type else "-",
tag_cell,
str(suggestion.storage_path) if suggestion.storage_path else "-",
)
return table
def _build_summary_table(stats: RetaggerStats) -> Table:
"""Build the final applied-changes summary table."""
table = Table(
title="Retagger Summary",
show_header=True,
header_style="bold cyan",
)
table.add_column("Metric", style="bold")
table.add_column("Count", justify="right")
table.add_row("Documents processed", str(stats.documents_processed))
table.add_row("Correspondents set", str(stats.correspondents))
table.add_row("Document types set", str(stats.document_types))
table.add_row("Tags added", str(stats.tags_added))
table.add_row("Tags removed", str(stats.tags_removed))
table.add_row("Storage paths set", str(stats.storage_paths))
return table
class Command(PaperlessCommand): class Command(PaperlessCommand):
help = ( help = (
"Using the current classification model, assigns correspondents, tags " "Using the current classification model, assigns correspondents, tags "
@@ -19,7 +180,7 @@ class Command(PaperlessCommand):
"modified) after their initial import." "modified) after their initial import."
) )
def add_arguments(self, parser): def add_arguments(self, parser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument("-c", "--correspondent", default=False, action="store_true") parser.add_argument("-c", "--correspondent", default=False, action="store_true")
parser.add_argument("-T", "--tags", default=False, action="store_true") parser.add_argument("-T", "--tags", default=False, action="store_true")
@@ -31,9 +192,9 @@ class Command(PaperlessCommand):
default=False, default=False,
action="store_true", action="store_true",
help=( help=(
"By default this command won't try to assign a correspondent " "By default this command will not try to assign a correspondent "
"if more than one matches the document. Use this flag if " "if more than one matches the document. Use this flag to pick "
"you'd rather it just pick the first one it finds." "the first match instead."
), ),
) )
parser.add_argument( parser.add_argument(
@@ -42,91 +203,140 @@ class Command(PaperlessCommand):
default=False, default=False,
action="store_true", action="store_true",
help=( help=(
"If set, the document retagger will overwrite any previously " "Overwrite any previously set correspondent, document type, and "
"set correspondent, document and remove correspondents, types " "remove tags that no longer match due to changed rules."
"and tags that do not match anymore due to changed rules."
), ),
) )
parser.add_argument( parser.add_argument(
"--suggest", "--suggest",
default=False, default=False,
action="store_true", action="store_true",
help="Return the suggestion, don't change anything.", help="Show what would be changed without applying anything.",
) )
parser.add_argument( parser.add_argument(
"--base-url", "--base-url",
help="The base URL to use to build the link to the documents.", help="Base URL used to build document links in suggest output.",
) )
parser.add_argument( parser.add_argument(
"--id-range", "--id-range",
help="A range of document ids on which the retagging should be applied.", help="Restrict retagging to documents within this ID range (inclusive).",
nargs=2, nargs=2,
type=int, type=int,
) )
def handle(self, *args, **options): def handle(self, *args, **options) -> None:
suggest: bool = options["suggest"]
overwrite: bool = options["overwrite"]
use_first: bool = options["use_first"]
base_url: str | None = options["base_url"]
do_correspondent: bool = options["correspondent"]
do_document_type: bool = options["document_type"]
do_tags: bool = options["tags"]
do_storage_path: bool = options["storage_path"]
if not any([do_correspondent, do_document_type, do_tags, do_storage_path]):
self.console.print(
"[yellow]No classifier targets specified. "
"Use -c, -T, -t, or -s to select what to retag.[/yellow]",
)
return
if options["inbox_only"]: if options["inbox_only"]:
queryset = Document.objects.filter(tags__is_inbox_tag=True) queryset = Document.objects.filter(tags__is_inbox_tag=True)
else: else:
queryset = Document.objects.all() queryset = Document.objects.all()
if options["id_range"]: if options["id_range"]:
queryset = queryset.filter( lo, hi = options["id_range"]
id__range=(options["id_range"][0], options["id_range"][1]), queryset = queryset.filter(id__range=(lo, hi))
)
documents = queryset.distinct() documents = queryset.distinct()
classifier = load_classifier() classifier = load_classifier()
for document in self.track(documents, description="Retagging..."): stats = RetaggerStats()
if options["correspondent"]: suggestions: list[DocumentSuggestion] = []
set_correspondent(
sender=None,
document=document,
classifier=classifier,
replace=options["overwrite"],
use_first=options["use_first"],
suggest=options["suggest"],
base_url=options["base_url"],
stdout=self.stdout,
style_func=self.style,
)
if options["document_type"]: def render_stats() -> RenderableType:
set_document_type( return _build_stats_table(stats, suggest=suggest)
sender=None,
document=document,
classifier=classifier,
replace=options["overwrite"],
use_first=options["use_first"],
suggest=options["suggest"],
base_url=options["base_url"],
stdout=self.stdout,
style_func=self.style,
)
if options["tags"]: with self.buffered_logging(
set_tags( "paperless",
sender=None, "paperless.handlers",
document=document, "documents",
classifier=classifier, ) as log_buf:
replace=options["overwrite"], for document in self.track_with_stats(
suggest=options["suggest"], documents,
base_url=options["base_url"], description="Retagging...",
stdout=self.stdout, stats_renderer=render_stats,
style_func=self.style, ):
) suggestion = DocumentSuggestion(document=document)
if options["storage_path"]: if do_correspondent:
set_storage_path( correspondent = set_correspondent(
sender=None, None,
document=document, document,
classifier=classifier, classifier=classifier,
replace=options["overwrite"], replace=overwrite,
use_first=options["use_first"], use_first=use_first,
suggest=options["suggest"], dry_run=suggest,
base_url=options["base_url"], )
stdout=self.stdout, if correspondent is not None:
style_func=self.style, stats.correspondents += 1
) suggestion.correspondent = correspondent
if do_document_type:
document_type = set_document_type(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if document_type is not None:
stats.document_types += 1
suggestion.document_type = document_type
if do_tags:
tags_to_add, tags_to_remove = set_tags(
None,
document,
classifier=classifier,
replace=overwrite,
dry_run=suggest,
)
stats.tags_added += len(tags_to_add)
stats.tags_removed += len(tags_to_remove)
suggestion.tags_to_add = frozenset(tags_to_add)
suggestion.tags_to_remove = frozenset(tags_to_remove)
if do_storage_path:
storage_path = set_storage_path(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if storage_path is not None:
stats.storage_paths += 1
suggestion.storage_path = storage_path
stats.documents_processed += 1
if suggest:
suggestions.append(suggestion)
# Post-loop output
if suggest:
visible = [s for s in suggestions if s.has_suggestions]
if visible:
self.console.print(_build_suggestion_table(visible, base_url))
else:
self.console.print("[green]No changes suggested.[/green]")
else:
self.console.print(_build_summary_table(stats))
log_buf.render(self.console, min_level=logging.INFO, title="Retagger Log")

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2026-01-15 22:08 # Generated by Django 5.2.11 on 2026-03-03 16:27
import datetime import datetime
@@ -21,6 +21,207 @@ class Migration(migrations.Migration):
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
replaces = [
("documents", "0001_initial"),
("documents", "0002_auto_20151226_1316"),
("documents", "0003_sender"),
("documents", "0004_auto_20160114_1844"),
(
"documents",
"0004_auto_20160114_1844_squashed_0011_auto_20160303_1929",
),
("documents", "0005_auto_20160123_0313"),
("documents", "0006_auto_20160123_0430"),
("documents", "0007_auto_20160126_2114"),
("documents", "0008_document_file_type"),
("documents", "0009_auto_20160214_0040"),
("documents", "0010_log"),
("documents", "0011_auto_20160303_1929"),
("documents", "0012_auto_20160305_0040"),
("documents", "0013_auto_20160325_2111"),
("documents", "0014_document_checksum"),
("documents", "0015_add_insensitive_to_match"),
(
"documents",
"0015_add_insensitive_to_match_squashed_0018_auto_20170715_1712",
),
("documents", "0016_auto_20170325_1558"),
("documents", "0017_auto_20170512_0507"),
("documents", "0018_auto_20170715_1712"),
("documents", "0019_add_consumer_user"),
("documents", "0020_document_added"),
("documents", "0021_document_storage_type"),
("documents", "0022_auto_20181007_1420"),
("documents", "0023_document_current_filename"),
("documents", "1000_update_paperless_all"),
("documents", "1001_auto_20201109_1636"),
("documents", "1002_auto_20201111_1105"),
("documents", "1003_mime_types"),
("documents", "1004_sanity_check_schedule"),
("documents", "1005_checksums"),
("documents", "1006_auto_20201208_2209"),
(
"documents",
"1006_auto_20201208_2209_squashed_1011_auto_20210101_2340",
),
("documents", "1007_savedview_savedviewfilterrule"),
("documents", "1008_auto_20201216_1736"),
("documents", "1009_auto_20201216_2005"),
("documents", "1010_auto_20210101_2159"),
("documents", "1011_auto_20210101_2340"),
("documents", "1012_fix_archive_files"),
("documents", "1013_migrate_tag_colour"),
("documents", "1014_auto_20210228_1614"),
("documents", "1015_remove_null_characters"),
("documents", "1016_auto_20210317_1351"),
(
"documents",
"1016_auto_20210317_1351_squashed_1020_merge_20220518_1839",
),
("documents", "1017_alter_savedviewfilterrule_rule_type"),
("documents", "1018_alter_savedviewfilterrule_value"),
("documents", "1019_storagepath_document_storage_path"),
("documents", "1019_uisettings"),
("documents", "1020_merge_20220518_1839"),
("documents", "1021_webp_thumbnail_conversion"),
("documents", "1022_paperlesstask"),
(
"documents",
"1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type",
),
("documents", "1023_add_comments"),
("documents", "1024_document_original_filename"),
("documents", "1025_alter_savedviewfilterrule_rule_type"),
("documents", "1026_transition_to_celery"),
(
"documents",
"1027_remove_paperlesstask_attempted_task_and_more",
),
(
"documents",
"1028_remove_paperlesstask_task_args_and_more",
),
("documents", "1029_alter_document_archive_serial_number"),
("documents", "1030_alter_paperlesstask_task_file_name"),
(
"documents",
"1031_remove_savedview_user_correspondent_owner_and_more",
),
(
"documents",
"1032_alter_correspondent_matching_algorithm_and_more",
),
(
"documents",
"1033_alter_documenttype_options_alter_tag_options_and_more",
),
("documents", "1034_alter_savedviewfilterrule_rule_type"),
("documents", "1035_rename_comment_note"),
("documents", "1036_alter_savedviewfilterrule_rule_type"),
("documents", "1037_webp_encrypted_thumbnail_conversion"),
("documents", "1038_sharelink"),
("documents", "1039_consumptiontemplate"),
(
"documents",
"1040_customfield_customfieldinstance_and_more",
),
("documents", "1041_alter_consumptiontemplate_sources"),
(
"documents",
"1042_consumptiontemplate_assign_custom_fields_and_more",
),
("documents", "1043_alter_savedviewfilterrule_rule_type"),
(
"documents",
"1044_workflow_workflowaction_workflowtrigger_and_more",
),
(
"documents",
"1045_alter_customfieldinstance_value_monetary",
),
(
"documents",
"1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at",
),
(
"documents",
"1046_workflowaction_remove_all_correspondents_and_more",
),
("documents", "1047_savedview_display_mode_and_more"),
("documents", "1048_alter_savedviewfilterrule_rule_type"),
(
"documents",
"1049_document_deleted_at_document_restored_at",
),
("documents", "1050_customfield_extra_data_and_more"),
(
"documents",
"1051_alter_correspondent_owner_alter_document_owner_and_more",
),
("documents", "1052_document_transaction_id"),
("documents", "1053_document_page_count"),
(
"documents",
"1054_customfieldinstance_value_monetary_amount_and_more",
),
("documents", "1055_alter_storagepath_path"),
(
"documents",
"1056_customfieldinstance_deleted_at_and_more",
),
("documents", "1057_paperlesstask_owner"),
(
"documents",
"1058_workflowtrigger_schedule_date_custom_field_and_more",
),
(
"documents",
"1059_workflowactionemail_workflowactionwebhook_and_more",
),
(
"documents",
"1060_alter_customfieldinstance_value_select",
),
("documents", "1061_workflowactionwebhook_as_json"),
("documents", "1062_alter_savedviewfilterrule_rule_type"),
(
"documents",
"1063_paperlesstask_type_alter_paperlesstask_task_name_and_more",
),
("documents", "1064_delete_log"),
(
"documents",
"1065_workflowaction_assign_custom_fields_values",
),
(
"documents",
"1066_alter_workflowtrigger_schedule_offset_days",
),
("documents", "1067_alter_document_created"),
("documents", "1068_alter_document_created"),
(
"documents",
"1069_workflowtrigger_filter_has_storage_path_and_more",
),
(
"documents",
"1070_customfieldinstance_value_long_text_and_more",
),
(
"documents",
"1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more",
),
(
"documents",
"1072_workflowtrigger_filter_custom_field_query_and_more",
),
("documents", "1073_migrate_workflow_title_jinja"),
(
"documents",
"1074_workflowrun_deleted_at_workflowrun_restored_at_and_more",
),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="WorkflowActionEmail", name="WorkflowActionEmail",
@@ -185,70 +386,6 @@ class Migration(migrations.Migration):
"abstract": False, "abstract": False,
}, },
), ),
migrations.CreateModel(
name="CustomField",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
models.DateTimeField(
db_index=True,
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
("name", models.CharField(max_length=128)),
(
"data_type",
models.CharField(
choices=[
("string", "String"),
("url", "URL"),
("date", "Date"),
("boolean", "Boolean"),
("integer", "Integer"),
("float", "Float"),
("monetary", "Monetary"),
("documentlink", "Document Link"),
("select", "Select"),
("longtext", "Long Text"),
],
editable=False,
max_length=50,
verbose_name="data type",
),
),
(
"extra_data",
models.JSONField(
blank=True,
help_text="Extra data for the custom field, such as select options",
null=True,
verbose_name="extra data",
),
),
],
options={
"verbose_name": "custom field",
"verbose_name_plural": "custom fields",
"ordering": ("created",),
"constraints": [
models.UniqueConstraint(
fields=("name",),
name="documents_customfield_unique_name",
),
],
},
),
migrations.CreateModel( migrations.CreateModel(
name="DocumentType", name="DocumentType",
fields=[ fields=[
@@ -733,17 +870,6 @@ class Migration(migrations.Migration):
verbose_name="correspondent", verbose_name="correspondent",
), ),
), ),
(
"owner",
models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="owner",
),
),
( (
"document_type", "document_type",
models.ForeignKey( models.ForeignKey(
@@ -767,12 +893,14 @@ class Migration(migrations.Migration):
), ),
), ),
( (
"tags", "owner",
models.ManyToManyField( models.ForeignKey(
blank=True, blank=True,
related_name="documents", default=None,
to="documents.tag", null=True,
verbose_name="tags", on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="owner",
), ),
), ),
], ],
@@ -782,6 +910,140 @@ class Migration(migrations.Migration):
"ordering": ("-created",), "ordering": ("-created",),
}, },
), ),
migrations.AddField(
model_name="document",
name="tags",
field=models.ManyToManyField(
blank=True,
related_name="documents",
to="documents.tag",
verbose_name="tags",
),
),
migrations.CreateModel(
name="Note",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("deleted_at", models.DateTimeField(blank=True, null=True)),
("restored_at", models.DateTimeField(blank=True, null=True)),
("transaction_id", models.UUIDField(blank=True, null=True)),
(
"note",
models.TextField(
blank=True,
help_text="Note for the document",
verbose_name="content",
),
),
(
"created",
models.DateTimeField(
db_index=True,
default=django.utils.timezone.now,
verbose_name="created",
),
),
(
"document",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notes",
to="documents.document",
verbose_name="document",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="notes",
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
],
options={
"verbose_name": "note",
"verbose_name_plural": "notes",
"ordering": ("created",),
},
),
migrations.CreateModel(
name="CustomField",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
models.DateTimeField(
db_index=True,
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
("name", models.CharField(max_length=128)),
(
"data_type",
models.CharField(
choices=[
("string", "String"),
("url", "URL"),
("date", "Date"),
("boolean", "Boolean"),
("integer", "Integer"),
("float", "Float"),
("monetary", "Monetary"),
("documentlink", "Document Link"),
("select", "Select"),
("longtext", "Long Text"),
],
editable=False,
max_length=50,
verbose_name="data type",
),
),
(
"extra_data",
models.JSONField(
blank=True,
help_text="Extra data for the custom field, such as select options",
null=True,
verbose_name="extra data",
),
),
],
options={
"verbose_name": "custom field",
"verbose_name_plural": "custom fields",
"ordering": ("created",),
"constraints": [
models.UniqueConstraint(
fields=("name",),
name="documents_customfield_unique_name",
),
],
},
),
migrations.CreateModel( migrations.CreateModel(
name="CustomFieldInstance", name="CustomFieldInstance",
fields=[ fields=[
@@ -880,66 +1142,6 @@ class Migration(migrations.Migration):
"ordering": ("created",), "ordering": ("created",),
}, },
), ),
migrations.CreateModel(
name="Note",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("deleted_at", models.DateTimeField(blank=True, null=True)),
("restored_at", models.DateTimeField(blank=True, null=True)),
("transaction_id", models.UUIDField(blank=True, null=True)),
(
"note",
models.TextField(
blank=True,
help_text="Note for the document",
verbose_name="content",
),
),
(
"created",
models.DateTimeField(
db_index=True,
default=django.utils.timezone.now,
verbose_name="created",
),
),
(
"document",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notes",
to="documents.document",
verbose_name="document",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="notes",
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
],
options={
"verbose_name": "note",
"verbose_name_plural": "notes",
"ordering": ("created",),
},
),
migrations.CreateModel( migrations.CreateModel(
name="PaperlessTask", name="PaperlessTask",
fields=[ fields=[
@@ -986,7 +1188,6 @@ class Migration(migrations.Migration):
("train_classifier", "Train Classifier"), ("train_classifier", "Train Classifier"),
("check_sanity", "Check Sanity"), ("check_sanity", "Check Sanity"),
("index_optimize", "Index Optimize"), ("index_optimize", "Index Optimize"),
("llmindex_update", "LLM Index Update"),
], ],
help_text="Name of the task that was run", help_text="Name of the task that was run",
max_length=255, max_length=255,
@@ -1380,6 +1581,7 @@ class Migration(migrations.Migration):
verbose_name="Workflow Action Type", verbose_name="Workflow Action Type",
), ),
), ),
("order", models.PositiveIntegerField(default=0, verbose_name="order")),
( (
"assign_title", "assign_title",
models.TextField( models.TextField(

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.9 on 2026-01-20 18:46 # Generated by Django 5.2.11 on 2026-03-03 16:27
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations from django.db import migrations
@@ -9,8 +9,14 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("documents", "0001_initial"), ("documents", "0001_squashed"),
("paperless_mail", "0001_initial"), ("paperless_mail", "0001_squashed"),
]
# This migration needs a "replaces", but it doesn't matter which.
# Chose the last 2.20.x migration
replaces = [
("documents", "1075_workflowaction_order"),
] ]
operations = [ operations = [

View File

@@ -6,7 +6,7 @@ from django.db import models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("documents", "0002_initial"), ("documents", "0002_squashed"),
] ]
operations = [ operations = [

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.2.11 on 2026-03-03 16:42
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "0013_document_root_document"),
]
operations = [
migrations.AlterField(
model_name="paperlesstask",
name="task_name",
field=models.CharField(
choices=[
("consume_file", "Consume File"),
("train_classifier", "Train Classifier"),
("check_sanity", "Check Sanity"),
("index_optimize", "Index Optimize"),
("llmindex_update", "LLM Index Update"),
],
help_text="Name of the task that was run",
max_length=255,
null=True,
verbose_name="Task Name",
),
),
]

View File

@@ -0,0 +1,153 @@
# Generated by Django 5.2.11 on 2026-02-20 22:05
from collections import defaultdict
from django.db import migrations
from django.db import models
# from src-ui/src/app/data/ui-settings.ts
SAVED_VIEWS_KEY = "saved_views"
DASHBOARD_VIEWS_VISIBLE_IDS_KEY = "dashboard_views_visible_ids"
SIDEBAR_VIEWS_VISIBLE_IDS_KEY = "sidebar_views_visible_ids"
def _parse_visible_ids(raw_value) -> set[int]:
"""Return integer SavedView IDs parsed from a JSON list value."""
if not isinstance(raw_value, list):
return set()
parsed_ids = set()
for raw_id in raw_value:
raw_id_string = str(raw_id)
if raw_id_string.isdigit():
parsed_ids.add(int(raw_id_string))
return parsed_ids
def _set_default_visibility_ids(apps, schema_editor):
"""
Move SavedView visibility from boolean model props into JSON UiSettings.settings.saved_views, specifically:
settings.saved_views.dashboard_views_visible_ids
settings.saved_views.sidebar_views_visible_ids
"""
SavedView = apps.get_model("documents", "SavedView")
UiSettings = apps.get_model("documents", "UiSettings")
User = apps.get_model("auth", "User")
dashboard_visible_ids_by_owner: defaultdict[int, list[int]] = defaultdict(list)
for owner_id, view_id in SavedView.objects.filter(
owner__isnull=False,
show_on_dashboard=True,
).values_list("owner_id", "id"):
dashboard_visible_ids_by_owner[owner_id].append(view_id)
sidebar_visible_ids_by_owner: defaultdict[int, list[int]] = defaultdict(list)
for owner_id, view_id in SavedView.objects.filter(
owner__isnull=False,
show_in_sidebar=True,
).values_list("owner_id", "id"):
sidebar_visible_ids_by_owner[owner_id].append(view_id)
for user in User.objects.all():
ui_settings, _ = UiSettings.objects.get_or_create(
user=user,
defaults={"settings": {}},
)
current_settings = ui_settings.settings
if not isinstance(current_settings, dict):
current_settings = {}
changed = False
saved_views_settings = current_settings.get(SAVED_VIEWS_KEY)
if not isinstance(saved_views_settings, dict):
saved_views_settings = {}
changed = True
if saved_views_settings.get(DASHBOARD_VIEWS_VISIBLE_IDS_KEY) is None:
saved_views_settings[DASHBOARD_VIEWS_VISIBLE_IDS_KEY] = (
dashboard_visible_ids_by_owner.get(user.id, [])
)
changed = True
if saved_views_settings.get(SIDEBAR_VIEWS_VISIBLE_IDS_KEY) is None:
saved_views_settings[SIDEBAR_VIEWS_VISIBLE_IDS_KEY] = (
sidebar_visible_ids_by_owner.get(user.id, [])
)
changed = True
current_settings[SAVED_VIEWS_KEY] = saved_views_settings
if changed:
ui_settings.settings = current_settings
ui_settings.save(update_fields=["settings"])
def _restore_visibility_fields(apps, schema_editor):
SavedView = apps.get_model("documents", "SavedView")
UiSettings = apps.get_model("documents", "UiSettings")
dashboard_visible_ids_by_owner: dict[int, set[int]] = {}
sidebar_visible_ids_by_owner: dict[int, set[int]] = {}
for ui_settings in UiSettings.objects.all():
current_settings = ui_settings.settings
if not isinstance(current_settings, dict):
continue
saved_views_settings = current_settings.get(SAVED_VIEWS_KEY)
if not isinstance(saved_views_settings, dict):
saved_views_settings = {}
dashboard_visible_ids_by_owner[ui_settings.user_id] = _parse_visible_ids(
saved_views_settings.get(DASHBOARD_VIEWS_VISIBLE_IDS_KEY),
)
sidebar_visible_ids_by_owner[ui_settings.user_id] = _parse_visible_ids(
saved_views_settings.get(SIDEBAR_VIEWS_VISIBLE_IDS_KEY),
)
SavedView.objects.update(show_on_dashboard=False, show_in_sidebar=False)
for owner_id, dashboard_visible_ids in dashboard_visible_ids_by_owner.items():
if not dashboard_visible_ids:
continue
SavedView.objects.filter(
owner_id=owner_id,
id__in=dashboard_visible_ids,
).update(
show_on_dashboard=True,
)
for owner_id, sidebar_visible_ids in sidebar_visible_ids_by_owner.items():
if not sidebar_visible_ids:
continue
SavedView.objects.filter(owner_id=owner_id, id__in=sidebar_visible_ids).update(
show_in_sidebar=True,
)
class Migration(migrations.Migration):
dependencies = [
("documents", "0014_alter_paperlesstask_task_name"),
]
operations = [
migrations.AlterField(
model_name="savedview",
name="show_on_dashboard",
field=models.BooleanField(default=False, verbose_name="show on dashboard"),
),
migrations.AlterField(
model_name="savedview",
name="show_in_sidebar",
field=models.BooleanField(default=False, verbose_name="show in sidebar"),
),
migrations.RunPython(
_set_default_visibility_ids,
reverse_code=_restore_visibility_fields,
),
migrations.RemoveField(
model_name="savedview",
name="show_on_dashboard",
),
migrations.RemoveField(
model_name="savedview",
name="show_in_sidebar",
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 5.2.11 on 2026-03-02 17:48
from django.conf import settings
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "0015_savedview_visibility_to_ui_settings"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="document",
name="version_index",
field=models.PositiveIntegerField(
blank=True,
db_index=True,
help_text="Index of this version within the root document.",
null=True,
verbose_name="version index",
),
),
migrations.AddConstraint(
model_name="document",
constraint=models.UniqueConstraint(
condition=models.Q(
("root_document__isnull", False),
("version_index__isnull", False),
),
fields=("root_document", "version_index"),
name="documents_document_root_version_index_uniq",
),
),
]

View File

@@ -75,7 +75,7 @@ class MatchingModel(ModelWithOwner):
is_insensitive = models.BooleanField(_("is insensitive"), default=True) is_insensitive = models.BooleanField(_("is insensitive"), default=True)
class Meta: class Meta(ModelWithOwner.Meta):
abstract = True abstract = True
ordering = ("name",) ordering = ("name",)
constraints = [ constraints = [
@@ -156,6 +156,8 @@ class StoragePath(MatchingModel):
class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-missing] class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-missing]
MAX_STORED_FILENAME_LENGTH: Final[int] = 1024
correspondent = models.ForeignKey( correspondent = models.ForeignKey(
Correspondent, Correspondent,
blank=True, blank=True,
@@ -262,7 +264,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
filename = models.FilePathField( filename = models.FilePathField(
_("filename"), _("filename"),
max_length=1024, max_length=MAX_STORED_FILENAME_LENGTH,
editable=False, editable=False,
default=None, default=None,
unique=True, unique=True,
@@ -272,7 +274,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
archive_filename = models.FilePathField( archive_filename = models.FilePathField(
_("archive filename"), _("archive filename"),
max_length=1024, max_length=MAX_STORED_FILENAME_LENGTH,
editable=False, editable=False,
default=None, default=None,
unique=True, unique=True,
@@ -282,7 +284,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
original_filename = models.CharField( original_filename = models.CharField(
_("original filename"), _("original filename"),
max_length=1024, max_length=MAX_STORED_FILENAME_LENGTH,
editable=False, editable=False,
default=None, default=None,
unique=False, unique=False,
@@ -317,6 +319,14 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
verbose_name=_("root document for this version"), verbose_name=_("root document for this version"),
) )
version_index = models.PositiveIntegerField(
_("version index"),
blank=True,
null=True,
db_index=True,
help_text=_("Index of this version within the root document."),
)
version_label = models.CharField( version_label = models.CharField(
_("version label"), _("version label"),
max_length=64, max_length=64,
@@ -329,6 +339,16 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
ordering = ("-created",) ordering = ("-created",)
verbose_name = _("document") verbose_name = _("document")
verbose_name_plural = _("documents") verbose_name_plural = _("documents")
constraints = [
models.UniqueConstraint(
fields=["root_document", "version_index"],
condition=models.Q(
root_document__isnull=False,
version_index__isnull=False,
),
name="documents_document_root_version_index_uniq",
),
]
def __str__(self) -> str: def __str__(self) -> str:
created = self.created.isoformat() created = self.created.isoformat()
@@ -473,13 +493,6 @@ class SavedView(ModelWithOwner):
name = models.CharField(_("name"), max_length=128) name = models.CharField(_("name"), max_length=128)
show_on_dashboard = models.BooleanField(
_("show on dashboard"),
)
show_in_sidebar = models.BooleanField(
_("show in sidebar"),
)
sort_field = models.CharField( sort_field = models.CharField(
_("sort field"), _("sort field"),
max_length=128, max_length=128,

View File

@@ -5,11 +5,7 @@ from abc import abstractmethod
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from types import TracebackType from types import TracebackType
from typing import Self
try:
from typing import Self
except ImportError:
from typing_extensions import Self
import dateparser import dateparser

View File

@@ -1,4 +1,5 @@
import enum import enum
from collections.abc import Mapping
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
@@ -8,7 +9,7 @@ if TYPE_CHECKING:
from channels_redis.pubsub import RedisPubSubChannelLayer from channels_redis.pubsub import RedisPubSubChannelLayer
class ProgressStatusOptions(str, enum.Enum): class ProgressStatusOptions(enum.StrEnum):
STARTED = "STARTED" STARTED = "STARTED"
WORKING = "WORKING" WORKING = "WORKING"
SUCCESS = "SUCCESS" SUCCESS = "SUCCESS"
@@ -47,7 +48,7 @@ class BaseStatusManager:
async_to_sync(self._channel.flush) async_to_sync(self._channel.flush)
self._channel = None self._channel = None
def send(self, payload: dict[str, str | int | None]) -> None: def send(self, payload: Mapping[str, object]) -> None:
# Ensure the layer is open # Ensure the layer is open
self.open() self.open()
@@ -73,26 +74,28 @@ class ProgressManager(BaseStatusManager):
max_progress: int, max_progress: int,
extra_args: dict[str, str | int | None] | None = None, extra_args: dict[str, str | int | None] | None = None,
) -> None: ) -> None:
payload = { data: dict[str, object] = {
"type": "status_update", "filename": self.filename,
"data": { "task_id": self.task_id,
"filename": self.filename, "current_progress": current_progress,
"task_id": self.task_id, "max_progress": max_progress,
"current_progress": current_progress, "status": status,
"max_progress": max_progress, "message": message,
"status": status,
"message": message,
},
} }
if extra_args is not None: if extra_args is not None:
payload["data"].update(extra_args) data.update(extra_args)
payload: dict[str, object] = {
"type": "status_update",
"data": data,
}
self.send(payload) self.send(payload)
class DocumentsStatusManager(BaseStatusManager): class DocumentsStatusManager(BaseStatusManager):
def send_documents_deleted(self, documents: list[int]) -> None: def send_documents_deleted(self, documents: list[int]) -> None:
payload = { payload: dict[str, object] = {
"type": "documents_deleted", "type": "documents_deleted",
"data": { "data": {
"documents": documents, "documents": documents,
@@ -100,3 +103,25 @@ class DocumentsStatusManager(BaseStatusManager):
} }
self.send(payload) self.send(payload)
def send_document_updated(
self,
*,
document_id: int,
modified: str,
owner_id: int | None = None,
users_can_view: list[int] | None = None,
groups_can_view: list[int] | None = None,
) -> None:
payload: dict[str, object] = {
"type": "document_updated",
"data": {
"document_id": document_id,
"modified": modified,
"owner_id": owner_id,
"users_can_view": users_can_view or [],
"groups_can_view": groups_can_view or [],
},
}
self.send(payload)

View File

@@ -1428,8 +1428,6 @@ class SavedViewSerializer(OwnedObjectSerializer):
fields = [ fields = [
"id", "id",
"name", "name",
"show_on_dashboard",
"show_in_sidebar",
"sort_field", "sort_field",
"sort_reverse", "sort_reverse",
"filter_rules", "filter_rules",
@@ -1439,6 +1437,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
"owner", "owner",
"permissions", "permissions",
"user_can_change", "user_can_change",
"set_permissions",
] ]
def validate(self, attrs): def validate(self, attrs):
@@ -1476,7 +1475,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
and len(validated_data["display_fields"]) == 0 and len(validated_data["display_fields"]) == 0
): ):
validated_data["display_fields"] = None validated_data["display_fields"] = None
super().update(instance, validated_data) instance = super().update(instance, validated_data)
if rules_data is not None: if rules_data is not None:
SavedViewFilterRule.objects.filter(saved_view=instance).delete() SavedViewFilterRule.objects.filter(saved_view=instance).delete()
for rule_data in rules_data: for rule_data in rules_data:
@@ -1488,7 +1487,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
if "user" in validated_data: if "user" in validated_data:
# backwards compatibility # backwards compatibility
validated_data["owner"] = validated_data.pop("user") validated_data["owner"] = validated_data.pop("user")
saved_view = SavedView.objects.create(**validated_data) saved_view = super().create(validated_data)
for rule_data in rules_data: for rule_data in rules_data:
SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data) SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data)
return saved_view return saved_view

View File

@@ -4,6 +4,7 @@ import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Any
from celery import shared_task from celery import shared_task
from celery import states from celery import states
@@ -23,6 +24,7 @@ from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from filelock import FileLock from filelock import FileLock
from rest_framework import serializers
from documents import matching from documents import matching
from documents.caching import clear_document_caches from documents.caching import clear_document_caches
@@ -32,12 +34,14 @@ from documents.file_handling import create_source_path_directory
from documents.file_handling import delete_empty_directories from documents.file_handling import delete_empty_directories
from documents.file_handling import generate_filename from documents.file_handling import generate_filename
from documents.file_handling import generate_unique_filename from documents.file_handling import generate_unique_filename
from documents.models import Correspondent
from documents.models import CustomField from documents.models import CustomField
from documents.models import CustomFieldInstance from documents.models import CustomFieldInstance
from documents.models import Document from documents.models import Document
from documents.models import MatchingModel from documents.models import DocumentType
from documents.models import PaperlessTask from documents.models import PaperlessTask
from documents.models import SavedView from documents.models import SavedView
from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import UiSettings from documents.models import UiSettings
from documents.models import Workflow from documents.models import Workflow
@@ -45,6 +49,7 @@ from documents.models import WorkflowAction
from documents.models import WorkflowRun from documents.models import WorkflowRun
from documents.models import WorkflowTrigger from documents.models import WorkflowTrigger
from documents.permissions import get_objects_for_user_owner_aware from documents.permissions import get_objects_for_user_owner_aware
from documents.plugins.helpers import DocumentsStatusManager
from documents.templating.utils import convert_format_str_to_template_format from documents.templating.utils import convert_format_str_to_template_format
from documents.workflows.actions import build_workflow_action_context from documents.workflows.actions import build_workflow_action_context
from documents.workflows.actions import execute_email_action from documents.workflows.actions import execute_email_action
@@ -66,6 +71,7 @@ if TYPE_CHECKING:
from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentMetadataOverrides
logger = logging.getLogger("paperless.handlers") logger = logging.getLogger("paperless.handlers")
DRF_DATETIME_FIELD = serializers.DateTimeField()
def add_inbox_tags(sender, document: Document, logging_group=None, **kwargs) -> None: def add_inbox_tags(sender, document: Document, logging_group=None, **kwargs) -> None:
@@ -81,47 +87,41 @@ def add_inbox_tags(sender, document: Document, logging_group=None, **kwargs) ->
document.add_nested_tags(inbox_tags) document.add_nested_tags(inbox_tags)
def _suggestion_printer(
stdout,
style_func,
suggestion_type: str,
document: Document,
selected: MatchingModel,
base_url: str | None = None,
) -> None:
"""
Smaller helper to reduce duplication when just outputting suggestions to the console
"""
doc_str = str(document)
if base_url is not None:
stdout.write(style_func.SUCCESS(doc_str))
stdout.write(style_func.SUCCESS(f"{base_url}/documents/{document.pk}"))
else:
stdout.write(style_func.SUCCESS(f"{doc_str} [{document.pk}]"))
stdout.write(f"Suggest {suggestion_type}: {selected}")
def set_correspondent( def set_correspondent(
sender, sender: object,
document: Document, document: Document,
*, *,
logging_group=None, logging_group: object = None,
classifier: DocumentClassifier | None = None, classifier: DocumentClassifier | None = None,
replace=False, replace: bool = False,
use_first=True, use_first: bool = True,
suggest=False, dry_run: bool = False,
base_url=None, **kwargs: Any,
stdout=None, ) -> Correspondent | None:
style_func=None, """
**kwargs, Assign a correspondent to a document based on classifier results.
) -> None:
Args:
document: The document to classify.
logging_group: Optional logging group for structured log output.
classifier: The trained classifier. If None, only rule-based matching runs.
replace: If True, overwrite an existing correspondent assignment.
use_first: If True, pick the first match when multiple correspondents
match. If False, skip assignment when multiple match.
dry_run: If True, compute and return the selection without saving.
**kwargs: Absorbed for Django signal compatibility (e.g. sender, signal).
Returns:
The correspondent that was (or would be) assigned, or None if no match
was found or assignment was skipped.
"""
if document.correspondent and not replace: if document.correspondent and not replace:
return return None
potential_correspondents = matching.match_correspondents(document, classifier) potential_correspondents = matching.match_correspondents(document, classifier)
potential_count = len(potential_correspondents) potential_count = len(potential_correspondents)
selected = potential_correspondents[0] if potential_correspondents else None selected = potential_correspondents[0] if potential_correspondents else None
if potential_count > 1: if potential_count > 1:
if use_first: if use_first:
logger.debug( logger.debug(
@@ -135,49 +135,53 @@ def set_correspondent(
f"not assigning any correspondent", f"not assigning any correspondent",
extra={"group": logging_group}, extra={"group": logging_group},
) )
return return None
if selected or replace: if (selected or replace) and not dry_run:
if suggest: logger.info(
_suggestion_printer( f"Assigning correspondent {selected} to {document}",
stdout, extra={"group": logging_group},
style_func, )
"correspondent", document.correspondent = selected
document, document.save(update_fields=("correspondent",))
selected,
base_url,
)
else:
logger.info(
f"Assigning correspondent {selected} to {document}",
extra={"group": logging_group},
)
document.correspondent = selected return selected
document.save(update_fields=("correspondent",))
def set_document_type( def set_document_type(
sender, sender: object,
document: Document, document: Document,
*, *,
logging_group=None, logging_group: object = None,
classifier: DocumentClassifier | None = None, classifier: DocumentClassifier | None = None,
replace=False, replace: bool = False,
use_first=True, use_first: bool = True,
suggest=False, dry_run: bool = False,
base_url=None, **kwargs: Any,
stdout=None, ) -> DocumentType | None:
style_func=None, """
**kwargs, Assign a document type to a document based on classifier results.
) -> None:
Args:
document: The document to classify.
logging_group: Optional logging group for structured log output.
classifier: The trained classifier. If None, only rule-based matching runs.
replace: If True, overwrite an existing document type assignment.
use_first: If True, pick the first match when multiple types match.
If False, skip assignment when multiple match.
dry_run: If True, compute and return the selection without saving.
**kwargs: Absorbed for Django signal compatibility (e.g. sender, signal).
Returns:
The document type that was (or would be) assigned, or None if no match
was found or assignment was skipped.
"""
if document.document_type and not replace: if document.document_type and not replace:
return return None
potential_document_type = matching.match_document_types(document, classifier) potential_document_types = matching.match_document_types(document, classifier)
potential_count = len(potential_document_types)
potential_count = len(potential_document_type) selected = potential_document_types[0] if potential_document_types else None
selected = potential_document_type[0] if potential_document_type else None
if potential_count > 1: if potential_count > 1:
if use_first: if use_first:
@@ -192,42 +196,64 @@ def set_document_type(
f"not assigning any document type", f"not assigning any document type",
extra={"group": logging_group}, extra={"group": logging_group},
) )
return return None
if selected or replace: if (selected or replace) and not dry_run:
if suggest: logger.info(
_suggestion_printer( f"Assigning document type {selected} to {document}",
stdout, extra={"group": logging_group},
style_func, )
"document type", document.document_type = selected
document, document.save(update_fields=("document_type",))
selected,
base_url,
)
else:
logger.info(
f"Assigning document type {selected} to {document}",
extra={"group": logging_group},
)
document.document_type = selected return selected
document.save(update_fields=("document_type",))
def set_tags( def set_tags(
sender, sender: object,
document: Document, document: Document,
*, *,
logging_group=None, logging_group: object = None,
classifier: DocumentClassifier | None = None, classifier: DocumentClassifier | None = None,
replace=False, replace: bool = False,
suggest=False, dry_run: bool = False,
base_url=None, **kwargs: Any,
stdout=None, ) -> tuple[set[Tag], set[Tag]]:
style_func=None, """
**kwargs, Assign tags to a document based on classifier results.
) -> None:
When replace=True, existing auto-matched and rule-matched tags are removed
before applying the new set (inbox tags and manually-added tags are preserved).
Args:
document: The document to classify.
logging_group: Optional logging group for structured log output.
classifier: The trained classifier. If None, only rule-based matching runs.
replace: If True, remove existing classifier-managed tags before applying
new ones. Inbox tags and manually-added tags are always preserved.
dry_run: If True, compute what would change without saving anything.
**kwargs: Absorbed for Django signal compatibility (e.g. sender, signal).
Returns:
A two-tuple of (tags_added, tags_removed). In non-replace mode,
tags_removed is always an empty set. In dry_run mode, neither set
is applied to the database.
"""
# Compute which tags would be removed under replace mode.
# The filter mirrors the .delete() call below: keep inbox tags and
# manually-added tags (match="" and not auto-matched).
if replace: if replace:
tags_to_remove: set[Tag] = set(
document.tags.exclude(
is_inbox_tag=True,
).exclude(
Q(match="") & ~Q(matching_algorithm=Tag.MATCH_AUTO),
),
)
else:
tags_to_remove = set()
if replace and not dry_run:
Document.tags.through.objects.filter(document=document).exclude( Document.tags.through.objects.filter(document=document).exclude(
Q(tag__is_inbox_tag=True), Q(tag__is_inbox_tag=True),
).exclude( ).exclude(
@@ -235,65 +261,53 @@ def set_tags(
).delete() ).delete()
current_tags = set(document.tags.all()) current_tags = set(document.tags.all())
matched_tags = matching.match_tags(document, classifier) matched_tags = matching.match_tags(document, classifier)
tags_to_add = set(matched_tags) - current_tags
relevant_tags = set(matched_tags) - current_tags if tags_to_add and not dry_run:
if suggest:
extra_tags = current_tags - set(matched_tags)
extra_tags = [
t for t in extra_tags if t.matching_algorithm == MatchingModel.MATCH_AUTO
]
if not relevant_tags and not extra_tags:
return
doc_str = style_func.SUCCESS(str(document))
if base_url:
stdout.write(doc_str)
stdout.write(f"{base_url}/documents/{document.pk}")
else:
stdout.write(doc_str + style_func.SUCCESS(f" [{document.pk}]"))
if relevant_tags:
stdout.write("Suggest tags: " + ", ".join([t.name for t in relevant_tags]))
if extra_tags:
stdout.write("Extra tags: " + ", ".join([t.name for t in extra_tags]))
else:
if not relevant_tags:
return
message = 'Tagging "{}" with "{}"'
logger.info( logger.info(
message.format(document, ", ".join([t.name for t in relevant_tags])), f'Tagging "{document}" with "{", ".join(t.name for t in tags_to_add)}"',
extra={"group": logging_group}, extra={"group": logging_group},
) )
document.add_nested_tags(tags_to_add)
document.add_nested_tags(relevant_tags) return tags_to_add, tags_to_remove
def set_storage_path( def set_storage_path(
sender, sender: object,
document: Document, document: Document,
*, *,
logging_group=None, logging_group: object = None,
classifier: DocumentClassifier | None = None, classifier: DocumentClassifier | None = None,
replace=False, replace: bool = False,
use_first=True, use_first: bool = True,
suggest=False, dry_run: bool = False,
base_url=None, **kwargs: Any,
stdout=None, ) -> StoragePath | None:
style_func=None, """
**kwargs, Assign a storage path to a document based on classifier results.
) -> None:
Args:
document: The document to classify.
logging_group: Optional logging group for structured log output.
classifier: The trained classifier. If None, only rule-based matching runs.
replace: If True, overwrite an existing storage path assignment.
use_first: If True, pick the first match when multiple paths match.
If False, skip assignment when multiple match.
dry_run: If True, compute and return the selection without saving.
**kwargs: Absorbed for Django signal compatibility (e.g. sender, signal).
Returns:
The storage path that was (or would be) assigned, or None if no match
was found or assignment was skipped.
"""
if document.storage_path and not replace: if document.storage_path and not replace:
return return None
potential_storage_path = matching.match_storage_paths( potential_storage_paths = matching.match_storage_paths(document, classifier)
document, potential_count = len(potential_storage_paths)
classifier, selected = potential_storage_paths[0] if potential_storage_paths else None
)
potential_count = len(potential_storage_path)
selected = potential_storage_path[0] if potential_storage_path else None
if potential_count > 1: if potential_count > 1:
if use_first: if use_first:
@@ -308,26 +322,17 @@ def set_storage_path(
f"not assigning any storage directory", f"not assigning any storage directory",
extra={"group": logging_group}, extra={"group": logging_group},
) )
return return None
if selected or replace: if (selected or replace) and not dry_run:
if suggest: logger.info(
_suggestion_printer( f"Assigning storage path {selected} to {document}",
stdout, extra={"group": logging_group},
style_func, )
"storage directory", document.storage_path = selected
document, document.save(update_fields=("storage_path",))
selected,
base_url,
)
else:
logger.info(
f"Assigning storage path {selected} to {document}",
extra={"group": logging_group},
)
document.storage_path = selected return selected
document.save(update_fields=("storage_path",))
# see empty_trash in documents/tasks.py for signal handling # see empty_trash in documents/tasks.py for signal handling
@@ -467,8 +472,22 @@ def update_filename_and_move_files(
old_filename = instance.filename old_filename = instance.filename
old_source_path = instance.source_path old_source_path = instance.source_path
move_original = False
old_archive_filename = instance.archive_filename
old_archive_path = instance.archive_path
move_archive = False
candidate_filename = generate_filename(instance) candidate_filename = generate_filename(instance)
if len(str(candidate_filename)) > Document.MAX_STORED_FILENAME_LENGTH:
msg = (
f"Document {instance!s}: Generated filename exceeds db path "
f"limit ({len(str(candidate_filename))} > "
f"{Document.MAX_STORED_FILENAME_LENGTH}): {candidate_filename!s}"
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
candidate_source_path = ( candidate_source_path = (
settings.ORIGINALS_DIR / candidate_filename settings.ORIGINALS_DIR / candidate_filename
).resolve() ).resolve()
@@ -487,11 +506,16 @@ def update_filename_and_move_files(
instance.filename = str(new_filename) instance.filename = str(new_filename)
move_original = old_filename != instance.filename move_original = old_filename != instance.filename
old_archive_filename = instance.archive_filename
old_archive_path = instance.archive_path
if instance.has_archive_version: if instance.has_archive_version:
archive_candidate = generate_filename(instance, archive_filename=True) archive_candidate = generate_filename(instance, archive_filename=True)
if len(str(archive_candidate)) > Document.MAX_STORED_FILENAME_LENGTH:
msg = (
f"Document {instance!s}: Generated archive filename exceeds "
f"db path limit ({len(str(archive_candidate))} > "
f"{Document.MAX_STORED_FILENAME_LENGTH}): {archive_candidate!s}"
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
archive_candidate_path = ( archive_candidate_path = (
settings.ARCHIVE_DIR / archive_candidate settings.ARCHIVE_DIR / archive_candidate
).resolve() ).resolve()
@@ -596,6 +620,16 @@ def update_filename_and_move_files(
root=settings.ARCHIVE_DIR, root=settings.ARCHIVE_DIR,
) )
# Keep version files in sync with root
if instance.root_document_id is None:
for version_doc in Document.objects.filter(root_document_id=instance.pk).only(
"pk",
):
update_filename_and_move_files(
Document,
version_doc,
)
@shared_task @shared_task
def process_cf_select_update(custom_field: CustomField) -> None: def process_cf_select_update(custom_field: CustomField) -> None:
@@ -762,6 +796,28 @@ def run_workflows_updated(
) )
def send_websocket_document_updated(
sender,
document: Document,
**kwargs,
) -> None:
# At this point, workflows may already have applied additional changes.
document.refresh_from_db()
from documents.data_models import DocumentMetadataOverrides
doc_overrides = DocumentMetadataOverrides.from_document(document)
with DocumentsStatusManager() as status_mgr:
status_mgr.send_document_updated(
document_id=document.id,
modified=DRF_DATETIME_FIELD.to_representation(document.modified),
owner_id=doc_overrides.owner_id,
users_can_view=doc_overrides.view_users,
groups_can_view=doc_overrides.view_groups,
)
def run_workflows( def run_workflows(
trigger_type: WorkflowTrigger.WorkflowTriggerType, trigger_type: WorkflowTrigger.WorkflowTriggerType,
document: Document | ConsumableDocument, document: Document | ConsumableDocument,
@@ -1043,7 +1099,11 @@ def add_or_update_document_in_llm_index(sender, document, **kwargs):
@receiver(models.signals.post_delete, sender=Document) @receiver(models.signals.post_delete, sender=Document)
def delete_document_from_llm_index(sender, instance: Document, **kwargs): def delete_document_from_llm_index(
sender: Any,
instance: Document,
**kwargs: Any,
) -> None:
""" """
Delete a document from the LLM index when it is deleted. Delete a document from the LLM index when it is deleted.
""" """

View File

@@ -62,6 +62,7 @@ from documents.sanity_checker import SanityCheckFailedException
from documents.signals import document_updated from documents.signals import document_updated
from documents.signals.handlers import cleanup_document_deletion from documents.signals.handlers import cleanup_document_deletion
from documents.signals.handlers import run_workflows from documents.signals.handlers import run_workflows
from documents.signals.handlers import send_websocket_document_updated
from documents.workflows.utils import get_workflows_for_trigger from documents.workflows.utils import get_workflows_for_trigger
from paperless.config import AIConfig from paperless.config import AIConfig
from paperless_ai.indexing import llm_index_add_or_update_document from paperless_ai.indexing import llm_index_add_or_update_document
@@ -572,6 +573,11 @@ def check_scheduled_workflows() -> None:
workflow_to_run=workflow, workflow_to_run=workflow,
document=document, document=document,
) )
# Scheduled workflows dont send document_updated signal, so send a websocket update here to ensure clients are updated
send_websocket_document_updated(
sender=None,
document=document,
)
def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None: def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None:

View File

@@ -1,3 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" {{ extra_attrs }} fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" {{ extra_attrs }} fill="currentColor">
<path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/> <path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 679 B

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