mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-22 00:49:35 -06:00
Compare commits
19 Commits
feature-li
...
feature-82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7331a4ec3 | ||
|
|
da0ca837ea | ||
|
|
e92352384f | ||
|
|
e588321ea3 | ||
|
|
a7ff3fac6b | ||
|
|
7bcfcf1e49 | ||
|
|
34bdf41931 | ||
|
|
773f0b2d1b | ||
|
|
accd5cd574 | ||
|
|
4b4c2766e6 | ||
|
|
16767b1433 | ||
|
|
45456042cb | ||
|
|
258777cac9 | ||
|
|
32ef2913b1 | ||
|
|
ef723a5c93 | ||
|
|
77554f6b36 | ||
|
|
e69e543ba2 | ||
|
|
e9d87ef049 | ||
|
|
e0e517358d |
@@ -344,6 +344,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: Could not resolve manager type for "documents.models.Document.deleted_objects" [django-manager-missing]
|
src/documents/models.py:0: error: Could not resolve manager type for "documents.models.Document.deleted_objects" [django-manager-missing]
|
||||||
src/documents/models.py:0: error: Could not resolve manager type for "documents.models.Document.global_objects" [django-manager-missing]
|
src/documents/models.py:0: error: Could not resolve manager type for "documents.models.Document.global_objects" [django-manager-missing]
|
||||||
@@ -1193,6 +1196,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]
|
||||||
@@ -1569,7 +1580,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]
|
||||||
@@ -1625,8 +1635,6 @@ 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 [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: Incompatible type for lookup 'owner': (got "User | AnonymousUser", expected "User | int | None") [misc]
|
|
||||||
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]
|
||||||
@@ -1692,11 +1700,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]
|
||||||
|
|||||||
@@ -451,3 +451,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.
|
||||||
|
|||||||
@@ -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,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,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,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,
|
||||||
|
|||||||
@@ -99,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>
|
||||||
@@ -134,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -452,7 +452,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',
|
||||||
@@ -463,7 +463,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')
|
||||||
@@ -475,6 +484,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,
|
||||||
@@ -506,16 +549,44 @@ 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')
|
||||||
|
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 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({
|
openModal.componentInstance.saveClicked.next({
|
||||||
name: 'Foo Bar',
|
name: 'Foo Bar',
|
||||||
show_on_dashboard: true,
|
showOnDashboard: true,
|
||||||
show_in_sidebar: true,
|
showInSideBar: true,
|
||||||
|
permissions_form: permissions,
|
||||||
})
|
})
|
||||||
expect(savedViewServiceCreate).toHaveBeenCalled()
|
expect(savedViewServiceCreate).toHaveBeenCalled()
|
||||||
|
expect(savedViewServiceCreate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'Foo Bar',
|
||||||
|
owner: permissions.owner,
|
||||||
|
set_permissions: permissions.set_permissions,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(updateVisibilitySpy).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([modifiedView.id]),
|
||||||
|
expect.arrayContaining([modifiedView.id])
|
||||||
|
)
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
expect(modalCloseSpy).toHaveBeenCalled()
|
expect(modalCloseSpy).toHaveBeenCalled()
|
||||||
@@ -549,6 +620,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(
|
||||||
() =>
|
() =>
|
||||||
@@ -561,9 +636,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'] })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
import { filter, first, map, of, Subject, switchMap, takeUntil } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
DEFAULT_DISPLAY_FIELDS,
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
DisplayField,
|
DisplayField,
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -256,11 +275,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,
|
||||||
@@ -284,6 +305,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 = []
|
||||||
@@ -366,7 +388,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,
|
||||||
@@ -380,6 +402,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.`
|
||||||
@@ -401,6 +424,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(() => {
|
||||||
@@ -418,24 +446,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
|
||||||
@@ -449,6 +501,32 @@ export class DocumentListComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private saveCreatedViewVisibility(
|
||||||
|
createdView: SavedView,
|
||||||
|
showOnDashboard: boolean,
|
||||||
|
showInSideBar: boolean
|
||||||
|
) {
|
||||||
|
if (!showOnDashboard && !showInSideBar) {
|
||||||
|
return of(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
|||||||
@@ -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 [users]="users" 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>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -13,17 +13,27 @@ 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 { 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 { 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)
|
||||||
|
private userService = inject(UserService)
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
public saveClicked = new EventEmitter()
|
public saveClicked = new EventEmitter()
|
||||||
@@ -36,6 +46,8 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
|
|
||||||
closeEnabled = false
|
closeEnabled = false
|
||||||
|
|
||||||
|
users: User[]
|
||||||
|
|
||||||
_defaultName = ''
|
_defaultName = ''
|
||||||
|
|
||||||
get defaultName() {
|
get defaultName() {
|
||||||
@@ -52,6 +64,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 {
|
||||||
@@ -59,10 +72,22 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.closeEnabled = true
|
this.closeEnabled = true
|
||||||
})
|
})
|
||||||
|
this.userService.listAll().subscribe((r) => {
|
||||||
|
this.users = r.results
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
|
|||||||
@@ -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>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">
|
||||||
|
|||||||
@@ -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,46 @@ 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 support delete saved view', () => {
|
it('should support delete saved view', () => {
|
||||||
@@ -162,14 +188,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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ 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 { 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'
|
||||||
@@ -41,8 +47,10 @@ export class SavedViewsComponent
|
|||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
private savedViewService = inject(SavedViewService)
|
private savedViewService = inject(SavedViewService)
|
||||||
|
private permissionsService = inject(PermissionsService)
|
||||||
private settings = inject(SettingsService)
|
private settings = inject(SettingsService)
|
||||||
private toastService = inject(ToastService)
|
private toastService = inject(ToastService)
|
||||||
|
private modalService = inject(NgbModal)
|
||||||
|
|
||||||
DisplayMode = DisplayMode
|
DisplayMode = DisplayMode
|
||||||
|
|
||||||
@@ -65,11 +73,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
|
.listAll(null, null, { full_perms: true })
|
||||||
this.initialize()
|
.subscribe((r) => {
|
||||||
})
|
this.savedViews = r.results
|
||||||
|
this.initialize()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -95,16 +109,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 +151,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 +160,119 @@ 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
|
||||||
|
|
||||||
|
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
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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.7',
|
version: '2.20.7',
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-02-20 22:05
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# from src-ui/src/app/data/ui-settings.ts
|
||||||
|
DASHBOARD_VIEWS_VISIBLE_IDS_KEY = (
|
||||||
|
"general-settings:saved-views:dashboard-views-visible-ids"
|
||||||
|
)
|
||||||
|
SIDEBAR_VIEWS_VISIBLE_IDS_KEY = "general-settings:saved-views:sidebar-views-visible-ids"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_visible_ids(raw_value) -> set[int]:
|
||||||
|
if not isinstance(raw_value, list):
|
||||||
|
return set()
|
||||||
|
|
||||||
|
parsed_ids = set()
|
||||||
|
for raw_id in raw_value:
|
||||||
|
if isinstance(raw_id, int):
|
||||||
|
parsed_ids.add(raw_id)
|
||||||
|
elif isinstance(raw_id, str) and raw_id.isdigit():
|
||||||
|
parsed_ids.add(int(raw_id))
|
||||||
|
return parsed_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _set_default_visibility_ids(apps, schema_editor):
|
||||||
|
SavedView = apps.get_model("documents", "SavedView")
|
||||||
|
UiSettings = apps.get_model("documents", "UiSettings")
|
||||||
|
User = apps.get_model("auth", "User")
|
||||||
|
|
||||||
|
dashboard_visible_ids_by_owner: dict[int, list[int]] = {}
|
||||||
|
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.setdefault(owner_id, []).append(view_id)
|
||||||
|
|
||||||
|
sidebar_visible_ids_by_owner: dict[int, list[int]] = {}
|
||||||
|
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.setdefault(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
|
||||||
|
|
||||||
|
if current_settings.get(DASHBOARD_VIEWS_VISIBLE_IDS_KEY) is None:
|
||||||
|
current_settings[DASHBOARD_VIEWS_VISIBLE_IDS_KEY] = (
|
||||||
|
dashboard_visible_ids_by_owner.get(user.id, [])
|
||||||
|
)
|
||||||
|
changed = True
|
||||||
|
if current_settings.get(SIDEBAR_VIEWS_VISIBLE_IDS_KEY) is None:
|
||||||
|
current_settings[SIDEBAR_VIEWS_VISIBLE_IDS_KEY] = (
|
||||||
|
sidebar_visible_ids_by_owner.get(user.id, [])
|
||||||
|
)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
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
|
||||||
|
dashboard_visible_ids_by_owner[ui_settings.user_id] = _parse_visible_ids(
|
||||||
|
current_settings.get(DASHBOARD_VIEWS_VISIBLE_IDS_KEY),
|
||||||
|
)
|
||||||
|
sidebar_visible_ids_by_owner[ui_settings.user_id] = _parse_visible_ids(
|
||||||
|
current_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", "0011_optimize_integer_field_sizes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -443,13 +443,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,
|
||||||
|
|||||||
@@ -1383,8 +1383,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",
|
||||||
@@ -1394,6 +1392,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
|
|||||||
"owner",
|
"owner",
|
||||||
"permissions",
|
"permissions",
|
||||||
"user_can_change",
|
"user_can_change",
|
||||||
|
"set_permissions",
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
@@ -1431,7 +1430,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:
|
||||||
@@ -1443,7 +1442,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
|
||||||
|
|||||||
@@ -2014,69 +2014,93 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
mock_get_date_parser.assert_not_called()
|
mock_get_date_parser.assert_not_called()
|
||||||
|
|
||||||
def test_saved_views(self) -> None:
|
def test_saved_views(self) -> None:
|
||||||
u1 = User.objects.create_superuser("user1")
|
u1 = User.objects.create_user("user1")
|
||||||
u2 = User.objects.create_superuser("user2")
|
u2 = User.objects.create_user("user2")
|
||||||
|
u3 = User.objects.create_user("user3")
|
||||||
|
|
||||||
|
view_perm = Permission.objects.get(codename="view_savedview")
|
||||||
|
change_perm = Permission.objects.get(codename="change_savedview")
|
||||||
|
for user in [u1, u2, u3]:
|
||||||
|
user.user_permissions.add(view_perm, change_perm)
|
||||||
|
|
||||||
v1 = SavedView.objects.create(
|
v1 = SavedView.objects.create(
|
||||||
owner=u1,
|
owner=u1,
|
||||||
name="test1",
|
name="test1",
|
||||||
sort_field="",
|
sort_field="",
|
||||||
show_on_dashboard=False,
|
|
||||||
show_in_sidebar=False,
|
|
||||||
)
|
)
|
||||||
SavedView.objects.create(
|
v2 = SavedView.objects.create(
|
||||||
owner=u2,
|
owner=u2,
|
||||||
name="test2",
|
name="test2",
|
||||||
sort_field="",
|
sort_field="",
|
||||||
show_on_dashboard=False,
|
|
||||||
show_in_sidebar=False,
|
|
||||||
)
|
)
|
||||||
SavedView.objects.create(
|
v3 = SavedView.objects.create(
|
||||||
owner=u2,
|
owner=u2,
|
||||||
name="test3",
|
name="test3",
|
||||||
sort_field="",
|
sort_field="",
|
||||||
show_on_dashboard=False,
|
|
||||||
show_in_sidebar=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get("/api/saved_views/")
|
assign_perm("view_savedview", u1, v2)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
assign_perm("change_savedview", u1, v2)
|
||||||
self.assertEqual(response.data["count"], 0)
|
assign_perm("view_savedview", u1, v3)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
self.client.get(f"/api/saved_views/{v1.id}/").status_code,
|
|
||||||
status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=u1)
|
self.client.force_authenticate(user=u1)
|
||||||
|
|
||||||
response = self.client.get("/api/saved_views/")
|
response = self.client.get("/api/saved_views/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["count"], 1)
|
self.assertEqual(response.data["count"], 3)
|
||||||
|
|
||||||
|
for view_id in [v1.id, v2.id, v3.id]:
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.get(f"/api/saved_views/{view_id}/").status_code,
|
||||||
|
status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v2.id}/",
|
||||||
|
{"sort_field": "added"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v3.id}/",
|
||||||
|
{"sort_field": "added"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.client.get(f"/api/saved_views/{v1.id}/").status_code,
|
response.status_code,
|
||||||
status.HTTP_200_OK,
|
status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.client.force_authenticate(user=u2)
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v2.id}/",
|
||||||
|
{
|
||||||
|
"set_permissions": {
|
||||||
|
"view": {"users": [u3.id]},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v2.id}/",
|
||||||
|
{"owner": u1.id},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
self.client.force_authenticate(user=u3)
|
||||||
|
|
||||||
response = self.client.get("/api/saved_views/")
|
response = self.client.get("/api/saved_views/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["count"], 2)
|
self.assertEqual(response.data["count"], 0)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
self.client.get(f"/api/saved_views/{v1.id}/").status_code,
|
|
||||||
status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_saved_view_create_update_patch(self) -> None:
|
def test_saved_view_create_update_patch(self) -> None:
|
||||||
User.objects.create_user("user1")
|
User.objects.create_user("user1")
|
||||||
|
|
||||||
view = {
|
view = {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"show_on_dashboard": True,
|
|
||||||
"show_in_sidebar": True,
|
|
||||||
"sort_field": "created2",
|
"sort_field": "created2",
|
||||||
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||||
}
|
}
|
||||||
@@ -2091,13 +2115,13 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
|
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
f"/api/saved_views/{v1.id}/",
|
f"/api/saved_views/{v1.id}/",
|
||||||
{"show_in_sidebar": False},
|
{"sort_reverse": True},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
v1 = SavedView.objects.get(id=v1.id)
|
v1 = SavedView.objects.get(id=v1.id)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertFalse(v1.show_in_sidebar)
|
self.assertTrue(v1.sort_reverse)
|
||||||
self.assertEqual(v1.filter_rules.count(), 1)
|
self.assertEqual(v1.filter_rules.count(), 1)
|
||||||
|
|
||||||
view["filter_rules"] = [{"rule_type": 12, "value": "secret"}]
|
view["filter_rules"] = [{"rule_type": 12, "value": "secret"}]
|
||||||
@@ -2131,8 +2155,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
|
|
||||||
view = {
|
view = {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"show_on_dashboard": True,
|
|
||||||
"show_in_sidebar": True,
|
|
||||||
"sort_field": "created2",
|
"sort_field": "created2",
|
||||||
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||||
"page_size": 20,
|
"page_size": 20,
|
||||||
@@ -2220,8 +2242,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
"""
|
"""
|
||||||
view = {
|
view = {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"show_on_dashboard": True,
|
|
||||||
"show_in_sidebar": True,
|
|
||||||
"sort_field": "created2",
|
"sort_field": "created2",
|
||||||
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||||
"page_size": 20,
|
"page_size": 20,
|
||||||
@@ -2297,8 +2317,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
owner=self.user,
|
owner=self.user,
|
||||||
name="test",
|
name="test",
|
||||||
sort_field=SavedView.DisplayFields.CUSTOM_FIELD % custom_field.id,
|
sort_field=SavedView.DisplayFields.CUSTOM_FIELD % custom_field.id,
|
||||||
show_on_dashboard=True,
|
|
||||||
show_in_sidebar=True,
|
|
||||||
display_fields=[
|
display_fields=[
|
||||||
SavedView.DisplayFields.TITLE,
|
SavedView.DisplayFields.TITLE,
|
||||||
SavedView.DisplayFields.CREATED,
|
SavedView.DisplayFields.CREATED,
|
||||||
|
|||||||
@@ -1307,13 +1307,12 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
tag1 = Tag.objects.create(name="bank tag1")
|
tag1 = Tag.objects.create(name="bank tag1")
|
||||||
Tag.objects.create(name="tag2")
|
Tag.objects.create(name="tag2")
|
||||||
|
|
||||||
SavedView.objects.create(
|
shared_view = SavedView.objects.create(
|
||||||
name="bank view",
|
name="bank view",
|
||||||
show_on_dashboard=True,
|
|
||||||
show_in_sidebar=True,
|
|
||||||
sort_field="",
|
sort_field="",
|
||||||
owner=user1,
|
owner=user2,
|
||||||
)
|
)
|
||||||
|
assign_perm("view_savedview", user1, shared_view)
|
||||||
mail_account1 = MailAccount.objects.create(name="bank mail account 1")
|
mail_account1 = MailAccount.objects.create(name="bank mail account 1")
|
||||||
mail_account2 = MailAccount.objects.create(name="mail account 2")
|
mail_account2 = MailAccount.objects.create(name="mail account 2")
|
||||||
mail_rule1 = MailRule.objects.create(
|
mail_rule1 = MailRule.objects.create(
|
||||||
|
|||||||
175
src/documents/tests/test_migration_saved_view_visibility.py
Normal file
175
src/documents/tests/test_migration_saved_view_visibility.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
from documents.tests.utils import TestMigrations
|
||||||
|
|
||||||
|
DASHBOARD_VIEWS_VISIBLE_IDS_KEY = (
|
||||||
|
"general-settings:saved-views:dashboard-views-visible-ids"
|
||||||
|
)
|
||||||
|
SIDEBAR_VIEWS_VISIBLE_IDS_KEY = "general-settings:saved-views:sidebar-views-visible-ids"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigrateSavedViewVisibilityToUiSettings(TestMigrations):
|
||||||
|
migrate_from = "0011_optimize_integer_field_sizes"
|
||||||
|
migrate_to = "0012_savedview_visibility_to_ui_settings"
|
||||||
|
|
||||||
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
|
User = apps.get_model("auth", "User")
|
||||||
|
SavedView = apps.get_model("documents", "SavedView")
|
||||||
|
UiSettings = apps.get_model("documents", "UiSettings")
|
||||||
|
|
||||||
|
self.user_with_empty_settings = User.objects.create(username="user1")
|
||||||
|
self.user_with_existing_settings = User.objects.create(username="user2")
|
||||||
|
self.user_with_owned_views = User.objects.create(username="user3")
|
||||||
|
self.user_with_empty_settings_id = self.user_with_empty_settings.id
|
||||||
|
self.user_with_existing_settings_id = self.user_with_existing_settings.id
|
||||||
|
self.user_with_owned_views_id = self.user_with_owned_views.id
|
||||||
|
|
||||||
|
self.dashboard_view = SavedView.objects.create(
|
||||||
|
owner=self.user_with_empty_settings,
|
||||||
|
name="dashboard",
|
||||||
|
show_on_dashboard=True,
|
||||||
|
show_in_sidebar=True,
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
self.sidebar_only_view = SavedView.objects.create(
|
||||||
|
owner=self.user_with_empty_settings,
|
||||||
|
name="sidebar-only",
|
||||||
|
show_on_dashboard=False,
|
||||||
|
show_in_sidebar=True,
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
self.hidden_view = SavedView.objects.create(
|
||||||
|
owner=self.user_with_empty_settings,
|
||||||
|
name="hidden",
|
||||||
|
show_on_dashboard=False,
|
||||||
|
show_in_sidebar=False,
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
self.other_owner_visible_view = SavedView.objects.create(
|
||||||
|
owner=self.user_with_owned_views,
|
||||||
|
name="other-owner-visible",
|
||||||
|
show_on_dashboard=True,
|
||||||
|
show_in_sidebar=True,
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
|
||||||
|
UiSettings.objects.create(user=self.user_with_empty_settings, settings={})
|
||||||
|
UiSettings.objects.create(
|
||||||
|
user=self.user_with_existing_settings,
|
||||||
|
settings={
|
||||||
|
DASHBOARD_VIEWS_VISIBLE_IDS_KEY: [self.sidebar_only_view.id],
|
||||||
|
SIDEBAR_VIEWS_VISIBLE_IDS_KEY: [self.dashboard_view.id],
|
||||||
|
"preserve": "value",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_visibility_defaults_are_seeded_and_existing_values_preserved(self) -> None:
|
||||||
|
UiSettings = self.apps.get_model("documents", "UiSettings")
|
||||||
|
|
||||||
|
seeded_settings = UiSettings.objects.get(
|
||||||
|
user_id=self.user_with_empty_settings_id,
|
||||||
|
).settings
|
||||||
|
self.assertCountEqual(
|
||||||
|
seeded_settings[DASHBOARD_VIEWS_VISIBLE_IDS_KEY],
|
||||||
|
[self.dashboard_view.id],
|
||||||
|
)
|
||||||
|
self.assertCountEqual(
|
||||||
|
seeded_settings[SIDEBAR_VIEWS_VISIBLE_IDS_KEY],
|
||||||
|
[self.dashboard_view.id, self.sidebar_only_view.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_settings = UiSettings.objects.get(
|
||||||
|
user_id=self.user_with_existing_settings_id,
|
||||||
|
).settings
|
||||||
|
self.assertEqual(
|
||||||
|
existing_settings[DASHBOARD_VIEWS_VISIBLE_IDS_KEY],
|
||||||
|
[self.sidebar_only_view.id],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
existing_settings[SIDEBAR_VIEWS_VISIBLE_IDS_KEY],
|
||||||
|
[self.dashboard_view.id],
|
||||||
|
)
|
||||||
|
self.assertEqual(existing_settings["preserve"], "value")
|
||||||
|
|
||||||
|
created_settings = UiSettings.objects.get(
|
||||||
|
user_id=self.user_with_owned_views_id,
|
||||||
|
).settings
|
||||||
|
self.assertCountEqual(
|
||||||
|
created_settings[DASHBOARD_VIEWS_VISIBLE_IDS_KEY],
|
||||||
|
[self.other_owner_visible_view.id],
|
||||||
|
)
|
||||||
|
self.assertCountEqual(
|
||||||
|
created_settings[SIDEBAR_VIEWS_VISIBLE_IDS_KEY],
|
||||||
|
[self.other_owner_visible_view.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestReverseMigrateSavedViewVisibilityFromUiSettings(TestMigrations):
|
||||||
|
migrate_from = "0012_savedview_visibility_to_ui_settings"
|
||||||
|
migrate_to = "0011_optimize_integer_field_sizes"
|
||||||
|
|
||||||
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
|
User = apps.get_model("auth", "User")
|
||||||
|
SavedView = apps.get_model("documents", "SavedView")
|
||||||
|
UiSettings = apps.get_model("documents", "UiSettings")
|
||||||
|
|
||||||
|
user1 = User.objects.create(username="user1")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
user3 = User.objects.create(username="user3")
|
||||||
|
|
||||||
|
self.view1 = SavedView.objects.create(
|
||||||
|
owner=user1,
|
||||||
|
name="view-1",
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
self.view2 = SavedView.objects.create(
|
||||||
|
owner=user1,
|
||||||
|
name="view-2",
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
self.view3 = SavedView.objects.create(
|
||||||
|
owner=user1,
|
||||||
|
name="view-3",
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
self.view4 = SavedView.objects.create(
|
||||||
|
owner=user2,
|
||||||
|
name="view-4",
|
||||||
|
sort_field="created",
|
||||||
|
)
|
||||||
|
|
||||||
|
UiSettings.objects.create(
|
||||||
|
user=user1,
|
||||||
|
settings={
|
||||||
|
DASHBOARD_VIEWS_VISIBLE_IDS_KEY: [self.view1.id],
|
||||||
|
SIDEBAR_VIEWS_VISIBLE_IDS_KEY: [self.view2.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
UiSettings.objects.create(
|
||||||
|
user=user2,
|
||||||
|
settings={
|
||||||
|
DASHBOARD_VIEWS_VISIBLE_IDS_KEY: [
|
||||||
|
self.view2.id,
|
||||||
|
self.view3.id,
|
||||||
|
self.view4.id,
|
||||||
|
],
|
||||||
|
SIDEBAR_VIEWS_VISIBLE_IDS_KEY: [self.view4.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
UiSettings.objects.create(user=user3, settings={})
|
||||||
|
|
||||||
|
def test_visibility_fields_restored_from_owner_visibility(self) -> None:
|
||||||
|
SavedView = self.apps.get_model("documents", "SavedView")
|
||||||
|
|
||||||
|
restored_view1 = SavedView.objects.get(pk=self.view1.id)
|
||||||
|
restored_view2 = SavedView.objects.get(pk=self.view2.id)
|
||||||
|
restored_view3 = SavedView.objects.get(pk=self.view3.id)
|
||||||
|
restored_view4 = SavedView.objects.get(pk=self.view4.id)
|
||||||
|
|
||||||
|
self.assertTrue(restored_view1.show_on_dashboard)
|
||||||
|
self.assertFalse(restored_view2.show_on_dashboard)
|
||||||
|
self.assertFalse(restored_view3.show_on_dashboard)
|
||||||
|
self.assertTrue(restored_view4.show_on_dashboard)
|
||||||
|
|
||||||
|
self.assertFalse(restored_view1.show_in_sidebar)
|
||||||
|
self.assertTrue(restored_view2.show_in_sidebar)
|
||||||
|
self.assertFalse(restored_view3.show_in_sidebar)
|
||||||
|
self.assertTrue(restored_view4.show_in_sidebar)
|
||||||
@@ -1660,24 +1660,21 @@ class LogViewSet(ViewSet):
|
|||||||
return Response(existing_logs)
|
return Response(existing_logs)
|
||||||
|
|
||||||
|
|
||||||
class SavedViewViewSet(ModelViewSet, PassUserMixin):
|
@extend_schema_view(**generate_object_with_permissions_schema(SavedViewSerializer))
|
||||||
|
class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet):
|
||||||
model = SavedView
|
model = SavedView
|
||||||
|
|
||||||
queryset = SavedView.objects.all()
|
queryset = SavedView.objects.select_related("owner").prefetch_related(
|
||||||
|
"filter_rules",
|
||||||
|
)
|
||||||
serializer_class = SavedViewSerializer
|
serializer_class = SavedViewSerializer
|
||||||
pagination_class = StandardPagination
|
pagination_class = StandardPagination
|
||||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
|
filter_backends = (
|
||||||
def get_queryset(self):
|
OrderingFilter,
|
||||||
user = self.request.user
|
ObjectOwnedOrGrantedPermissionsFilter,
|
||||||
return (
|
)
|
||||||
SavedView.objects.filter(owner=user)
|
ordering_fields = ("name",)
|
||||||
.select_related("owner")
|
|
||||||
.prefetch_related("filter_rules")
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_create(self, serializer) -> None:
|
|
||||||
serializer.save(owner=self.request.user)
|
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
@@ -2201,7 +2198,11 @@ class GlobalSearchView(PassUserMixin):
|
|||||||
)
|
)
|
||||||
docs = docs[:OBJECT_LIMIT]
|
docs = docs[:OBJECT_LIMIT]
|
||||||
saved_views = (
|
saved_views = (
|
||||||
SavedView.objects.filter(owner=request.user, name__icontains=query)
|
get_objects_for_user_owner_aware(
|
||||||
|
request.user,
|
||||||
|
"view_savedview",
|
||||||
|
SavedView,
|
||||||
|
).filter(name__icontains=query)
|
||||||
if request.user.has_perm("documents.view_savedview")
|
if request.user.has_perm("documents.view_savedview")
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -374,10 +374,10 @@ REST_FRAMEWORK = {
|
|||||||
"rest_framework.authentication.SessionAuthentication",
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
],
|
],
|
||||||
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning",
|
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning",
|
||||||
"DEFAULT_VERSION": "9", # match src-ui/src/environments/environment.prod.ts
|
"DEFAULT_VERSION": "10", # match src-ui/src/environments/environment.prod.ts
|
||||||
# Make sure these are ordered and that the most recent version appears
|
# Make sure these are ordered and that the most recent version appears
|
||||||
# last. See api.md#api-versioning when adding new versions.
|
# last. See api.md#api-versioning when adding new versions.
|
||||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
|
||||||
# DRF Spectacular default schema
|
# DRF Spectacular default schema
|
||||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user