mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: management list button improvements (#7848)
This commit is contained in:
		| @@ -322,20 +322,24 @@ | ||||
|           <context context-type="linenumber">128</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">90</context> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">54</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">90</context> | ||||
|           <context context-type="linenumber">101</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">90</context> | ||||
|           <context context-type="linenumber">101</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">90</context> | ||||
|           <context context-type="linenumber">101</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">101</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4930506384627295710" datatype="html"> | ||||
| @@ -1493,7 +1497,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">34</context> | ||||
|           <context context-type="linenumber">36</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">48</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||
| @@ -1529,35 +1537,35 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">84</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">84</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">84</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">84</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">96</context> | ||||
|           <context context-type="linenumber">95</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">96</context> | ||||
|           <context context-type="linenumber">95</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">96</context> | ||||
|           <context context-type="linenumber">95</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">96</context> | ||||
|           <context context-type="linenumber">95</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> | ||||
| @@ -2219,7 +2227,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">73</context> | ||||
|           <context context-type="linenumber">80</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||
| @@ -2418,7 +2426,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">31</context> | ||||
|           <context context-type="linenumber">35</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">45</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||
| @@ -2438,35 +2450,35 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">93</context> | ||||
|           <context context-type="linenumber">92</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">93</context> | ||||
|           <context context-type="linenumber">92</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">93</context> | ||||
|           <context context-type="linenumber">92</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">93</context> | ||||
|           <context context-type="linenumber">92</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> | ||||
| @@ -2570,7 +2582,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">75</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||
| @@ -3286,7 +3298,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">56</context> | ||||
|           <context context-type="linenumber">63</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1841172489943868696" datatype="html"> | ||||
| @@ -3297,7 +3309,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">63</context> | ||||
|           <context context-type="linenumber">70</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4465085913683915434" datatype="html"> | ||||
| @@ -7475,39 +7487,62 @@ | ||||
|           <context context-type="linenumber">18</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6209318295562170730" datatype="html"> | ||||
|         <source>Filter Documents (<x id="INTERPOLATION" equiv-text="{{ field.document_count }}"/>)</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">38</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">85</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">85</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">85</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">85</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="651372623796033489" datatype="html"> | ||||
|         <source>No fields defined.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> | ||||
|           <context context-type="linenumber">42</context> | ||||
|           <context context-type="linenumber">63</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3032792139967609806" datatype="html"> | ||||
|         <source>Confirm delete field</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">71</context> | ||||
|           <context context-type="linenumber">78</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2939457975223185057" datatype="html"> | ||||
|         <source>This operation will permanently delete this field.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">72</context> | ||||
|           <context context-type="linenumber">79</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5137089475515834162" datatype="html"> | ||||
|         <source>Deleted field</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">81</context> | ||||
|           <context context-type="linenumber">88</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6352403551920829405" datatype="html"> | ||||
|         <source>Error deleting field.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> | ||||
|           <context context-type="linenumber">86</context> | ||||
|           <context context-type="linenumber">93</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8084492669582894778" datatype="html"> | ||||
| @@ -7799,42 +7834,23 @@ | ||||
|           <context context-type="linenumber">39</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7376880254267897616" datatype="html"> | ||||
|         <source>Filter Documents</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">82</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8095412801504464756" datatype="html"> | ||||
|         <source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION_2"/>}}</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">110</context> | ||||
|           <context context-type="linenumber">116</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">110</context> | ||||
|           <context context-type="linenumber">116</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">110</context> | ||||
|           <context context-type="linenumber">116</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||
|           <context context-type="linenumber">110</context> | ||||
|           <context context-type="linenumber">116</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="810888510148304696" datatype="html"> | ||||
|   | ||||
| @@ -26,7 +26,21 @@ | ||||
|         <div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div> | ||||
|         <div class="col d-flex align-items-center">{{getDataType(field)}}</div> | ||||
|         <div class="col"> | ||||
|           <div class="btn-group"> | ||||
|           <div class="btn-group d-block d-sm-none"> | ||||
|             <div ngbDropdown container="body" class="d-inline-block"> | ||||
|               <button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle> | ||||
|                 <i-bs name="three-dots-vertical"></i-bs> | ||||
|               </button> | ||||
|               <div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> | ||||
|                 <button (click)="editField(field)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" ngbDropdownItem i18n>Edit</button> | ||||
|                 <button class="text-danger" (click)="deleteField(field)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" ngbDropdownItem i18n>Delete</button> | ||||
|                 @if (field.document_count > 0) { | ||||
|                   <button (click)="filterDocuments(field)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ field.document_count }})</button> | ||||
|                 } | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="btn-group d-none d-sm-inline-block"> | ||||
|             <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)"> | ||||
|               <i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container> | ||||
|             </button> | ||||
| @@ -34,6 +48,13 @@ | ||||
|               <i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container> | ||||
|             </button> | ||||
|           </div> | ||||
|           @if (field.document_count > 0) { | ||||
|             <div class="btn-group d-none d-sm-inline-block ms-2"> | ||||
|               <button class="btn btn-sm btn-outline-secondary" type="button" (click)="filterDocuments(field)"> | ||||
|                 <i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ field.document_count }}</span> | ||||
|               </button> | ||||
|             </div> | ||||
|           } | ||||
|         </div> | ||||
|       </div> | ||||
|     </li> | ||||
|   | ||||
| @@ -0,0 +1,4 @@ | ||||
| // hide caret on mobile dropdown | ||||
| .d-block.d-sm-none .dropdown-toggle::after { | ||||
|     display: none; | ||||
| } | ||||
|   | ||||
| @@ -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]], | ||||
|         ]), | ||||
|       }, | ||||
|     ]) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -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]], | ||||
|         ]), | ||||
|       }, | ||||
|     ]) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -79,16 +79,15 @@ | ||||
|                   <i-bs name="three-dots-vertical"></i-bs> | ||||
|                 </button> | ||||
|                 <div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> | ||||
|                   <button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents</button> | ||||
|                   <button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button> | ||||
|                   <button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button> | ||||
|                   @if (object.document_count > 0) { | ||||
|                     <button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button> | ||||
|                   } | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="btn-group d-none d-sm-block"> | ||||
|               <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> | ||||
|                 <i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container> | ||||
|               </button> | ||||
|             <div class="btn-group d-none d-sm-inline-block"> | ||||
|               <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)"> | ||||
|                 <i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container> | ||||
|               </button> | ||||
| @@ -96,6 +95,13 @@ | ||||
|                 <i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container> | ||||
|               </button> | ||||
|             </div> | ||||
|             @if (object.document_count > 0) { | ||||
|               <div class="btn-group d-none d-sm-inline-block ms-2"> | ||||
|                 <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> | ||||
|                   <i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span> | ||||
|                 </button> | ||||
|               </div> | ||||
|             } | ||||
|           </td> | ||||
|         </tr> | ||||
|       } | ||||
|   | ||||
| @@ -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() }, | ||||
|   | ||||
| @@ -59,4 +59,5 @@ export interface CustomField extends ObjectWithId { | ||||
|     select_options?: string[] | ||||
|     default_currency?: string | ||||
|   } | ||||
|   document_count?: number | ||||
| } | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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,) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon