From 54293bedb1c55edccede13938082cc2a1ac69060 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:00:28 -0700 Subject: [PATCH] Enhancement: management list button improvements (#7848) --- src-ui/messages.xlf | 126 ++++++++++-------- .../custom-fields.component.html | 23 +++- .../custom-fields.component.scss | 4 + .../custom-fields.component.spec.ts | 24 +++- .../custom-fields/custom-fields.component.ts | 21 ++- .../management-list.component.html | 16 ++- .../management-list.component.spec.ts | 9 +- src-ui/src/app/data/custom-field.ts | 1 + src/documents/serialisers.py | 3 + src/documents/tests/test_api_custom_fields.py | 49 +++++++ src/documents/views.py | 26 ++++ 11 files changed, 236 insertions(+), 66 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 3fffe4f6e..3570a77c1 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -322,20 +322,24 @@ 128 - src/app/components/manage/management-list/management-list.component.html - 90 + src/app/components/manage/custom-fields/custom-fields.component.html + 54 src/app/components/manage/management-list/management-list.component.html - 90 + 101 src/app/components/manage/management-list/management-list.component.html - 90 + 101 src/app/components/manage/management-list/management-list.component.html - 90 + 101 + + + src/app/components/manage/management-list/management-list.component.html + 101 @@ -1493,7 +1497,11 @@ src/app/components/manage/custom-fields/custom-fields.component.html - 34 + 36 + + + src/app/components/manage/custom-fields/custom-fields.component.html + 48 src/app/components/manage/mail/mail.component.html @@ -1529,35 +1537,35 @@ src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.ts @@ -2219,7 +2227,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 73 + 80 src/app/components/manage/mail/mail.component.ts @@ -2418,7 +2426,11 @@ src/app/components/manage/custom-fields/custom-fields.component.html - 31 + 35 + + + src/app/components/manage/custom-fields/custom-fields.component.html + 45 src/app/components/manage/mail/mail.component.html @@ -2438,35 +2450,35 @@ src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/workflows/workflows.component.html @@ -2570,7 +2582,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 75 + 82 src/app/components/manage/mail/mail.component.ts @@ -3286,7 +3298,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 56 + 63 @@ -3297,7 +3309,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 63 + 70 @@ -7475,39 +7487,62 @@ 18 + + Filter Documents () + + src/app/components/manage/custom-fields/custom-fields.component.html + 38 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + No fields defined. src/app/components/manage/custom-fields/custom-fields.component.html - 42 + 63 Confirm delete field src/app/components/manage/custom-fields/custom-fields.component.ts - 71 + 78 This operation will permanently delete this field. src/app/components/manage/custom-fields/custom-fields.component.ts - 72 + 79 Deleted field src/app/components/manage/custom-fields/custom-fields.component.ts - 81 + 88 Error deleting field. src/app/components/manage/custom-fields/custom-fields.component.ts - 86 + 93 @@ -7799,42 +7834,23 @@ 39 - - Filter Documents - - src/app/components/manage/management-list/management-list.component.html - 82 - - - src/app/components/manage/management-list/management-list.component.html - 82 - - - src/app/components/manage/management-list/management-list.component.html - 82 - - - src/app/components/manage/management-list/management-list.component.html - 82 - - {VAR_PLURAL, plural, =1 {One } other { total }} src/app/components/manage/management-list/management-list.component.html - 110 + 116 src/app/components/manage/management-list/management-list.component.html - 110 + 116 src/app/components/manage/management-list/management-list.component.html - 110 + 116 src/app/components/manage/management-list/management-list.component.html - 110 + 116 diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html index c87a93050..8439cd1a7 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html @@ -26,7 +26,21 @@
{{getDataType(field)}}
-
+
+
+ +
+ + + @if (field.document_count > 0) { + + } +
+
+
+
@@ -34,6 +48,13 @@  Delete
+ @if (field.document_count > 0) { +
+ +
+ }
diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss index e69de29bb..dfdd20433 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss @@ -0,0 +1,4 @@ +// hide caret on mobile dropdown +.d-block.d-sm-none .dropdown-toggle::after { + display: none; +} diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts index 2bb6c82d7..5feb17055 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts @@ -22,6 +22,12 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type' +import { + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from 'src/app/data/custom-field-query' const fields: CustomField[] = [ { @@ -42,6 +48,7 @@ describe('CustomFieldsComponent', () => { let customFieldsService: CustomFieldsService let modalService: NgbModal let toastService: ToastService + let listViewService: DocumentListViewService beforeEach(() => { TestBed.configureTestingModule({ @@ -83,6 +90,7 @@ describe('CustomFieldsComponent', () => { ) modalService = TestBed.inject(NgbModal) toastService = TestBed.inject(ToastService) + listViewService = TestBed.inject(DocumentListViewService) fixture = TestBed.createComponent(CustomFieldsComponent) component = fixture.componentInstance @@ -145,7 +153,7 @@ describe('CustomFieldsComponent', () => { const deleteSpy = jest.spyOn(customFieldsService, 'delete') const reloadSpy = jest.spyOn(component, 'reload') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4] + const deleteButton = fixture.debugElement.queryAll(By.css('button'))[5] deleteButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -162,4 +170,18 @@ describe('CustomFieldsComponent', () => { editDialog.confirmClicked.emit() expect(reloadSpy).toHaveBeenCalled() }) + + it('should support filter documents', () => { + const filterSpy = jest.spyOn(listViewService, 'quickFilter') + component.filterDocuments(fields[0]) + expect(filterSpy).toHaveBeenCalledWith([ + { + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.Or, + [[fields[0].id, CustomFieldQueryOperator.Exists, true]], + ]), + }, + ]) + }) }) diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts index 33f2751a9..60bbcc09c 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts @@ -9,6 +9,12 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' +import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type' +import { + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from 'src/app/data/custom-field-query' @Component({ selector: 'pngx-custom-fields', @@ -26,7 +32,8 @@ export class CustomFieldsComponent private customFieldsService: CustomFieldsService, public permissionsService: PermissionsService, private modalService: NgbModal, - private toastService: ToastService + private toastService: ToastService, + private documentListViewService: DocumentListViewService ) { super() } @@ -92,4 +99,16 @@ export class CustomFieldsComponent getDataType(field: CustomField): string { return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name } + + filterDocuments(field: CustomField) { + this.documentListViewService.quickFilter([ + { + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.Or, + [[field.id, CustomFieldQueryOperator.Exists, true]], + ]), + }, + ]) + } } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html index a180f4941..e9a181819 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.html +++ b/src-ui/src/app/components/manage/management-list/management-list.component.html @@ -79,16 +79,15 @@
- + @if (object.document_count > 0) { + + }
-
- +
@@ -96,6 +95,13 @@  Delete
+ @if (object.document_count > 0) { +
+ +
+ } } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts index 557d5f388..9aa876da2 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts @@ -49,16 +49,19 @@ const tags: Tag[] = [ name: 'Tag1 Foo', matching_algorithm: MATCH_LITERAL, match: 'foo', + document_count: 35, }, { id: 2, name: 'Tag2', matching_algorithm: MATCH_NONE, + document_count: 0, }, { id: 3, name: 'Tag3', matching_algorithm: MATCH_AUTO, + document_count: 5, }, ] @@ -180,7 +183,7 @@ describe('ManagementListComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reloadData') - const editButton = fixture.debugElement.queryAll(By.css('button'))[7] + const editButton = fixture.debugElement.queryAll(By.css('button'))[6] editButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -205,7 +208,7 @@ describe('ManagementListComponent', () => { const deleteSpy = jest.spyOn(tagService, 'delete') const reloadSpy = jest.spyOn(component, 'reloadData') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8] + const deleteButton = fixture.debugElement.queryAll(By.css('button'))[7] deleteButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -225,7 +228,7 @@ describe('ManagementListComponent', () => { it('should support quick filter for objects', () => { const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - const filterButton = fixture.debugElement.queryAll(By.css('button'))[6] + const filterButton = fixture.debugElement.queryAll(By.css('button'))[8] filterButton.triggerEventHandler('click') expect(qfSpy).toHaveBeenCalledWith([ { rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() }, diff --git a/src-ui/src/app/data/custom-field.ts b/src-ui/src/app/data/custom-field.ts index 7e52d0785..bca77dd51 100644 --- a/src-ui/src/app/data/custom-field.ts +++ b/src-ui/src/app/data/custom-field.ts @@ -59,4 +59,5 @@ export interface CustomField extends ObjectWithId { select_options?: string[] default_currency?: string } + document_count?: number } diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 30f3dd26d..f326b4eee 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -494,6 +494,8 @@ class CustomFieldSerializer(serializers.ModelSerializer): read_only=False, ) + document_count = serializers.IntegerField(read_only=True) + class Meta: model = CustomField fields = [ @@ -501,6 +503,7 @@ class CustomFieldSerializer(serializers.ModelSerializer): "name", "data_type", "extra_data", + "document_count", ] def validate(self, attrs): diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py index 6ffe14681..bfe352d56 100644 --- a/src/documents/tests/test_api_custom_fields.py +++ b/src/documents/tests/test_api_custom_fields.py @@ -1,6 +1,7 @@ import json from datetime import date +from django.contrib.auth.models import Permission from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase @@ -933,3 +934,51 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results), 1) self.assertEqual(results[0]["name"], custom_field_int.name) + + def test_custom_fields_document_count(self): + custom_field_string = CustomField.objects.create( + name="Test Custom Field String", + data_type=CustomField.FieldDataType.STRING, + ) + doc = Document.objects.create( + title="WOW", + content="the content", + checksum="123", + mime_type="application/pdf", + owner=self.user, + ) + + response = self.client.get( + f"{self.ENDPOINT}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(results[0]["document_count"], 0) + + CustomFieldInstance.objects.create( + document=doc, + field=custom_field_string, + value_text="test value", + ) + + response = self.client.get( + f"{self.ENDPOINT}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(results[0]["document_count"], 1) + + # Test as user without access to the document + non_superuser = User.objects.create_user(username="non_superuser") + non_superuser.user_permissions.add( + *Permission.objects.all(), + ) + non_superuser.save() + self.client.force_authenticate(user=non_superuser) + self.client.force_login(user=non_superuser) + response = self.client.get( + f"{self.ENDPOINT}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(results[0]["document_count"], 0) diff --git a/src/documents/views.py b/src/documents/views.py index c870c15b5..94674a83f 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1897,6 +1897,32 @@ class CustomFieldViewSet(ModelViewSet): queryset = CustomField.objects.all().order_by("-created") + def get_queryset(self): + filter = ( + Q(fields__document__deleted_at__isnull=True) + if self.request.user is None or self.request.user.is_superuser + else ( + Q( + fields__document__deleted_at__isnull=True, + fields__document__id__in=get_objects_for_user_owner_aware( + self.request.user, + "documents.view_document", + Document, + ).values_list("id", flat=True), + ) + ) + ) + return ( + super() + .get_queryset() + .annotate( + document_count=Count( + "fields", + filter=filter, + ), + ) + ) + class SystemStatusView(PassUserMixin): permission_classes = (IsAuthenticated,)