]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Performance fix: add paging for custom field select options (#10755)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 2 Sep 2025 18:46:54 +0000 (11:46 -0700)
committerGitHub <noreply@github.com>
Tue, 2 Sep 2025 18:46:54 +0000 (11:46 -0700)
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.spec.ts
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts
src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts

index b4216e41c6ad99dc07e17d4bfab3d1e1e9f1f47d..ea57e1746931828947c2c99e09250bace93be0cb 100644 (file)
               </div>
             }
           </div>
+          @if (allSelectOptions.length > SELECT_OPTION_PAGE_SIZE) {
+            <ngb-pagination
+              class="d-flex justify-content-end"
+              [pageSize]="SELECT_OPTION_PAGE_SIZE"
+              [collectionSize]="allSelectOptions.length"
+              [(page)]="selectOptionsPage"
+              [maxSize]="5"
+              size="sm"
+            ></ngb-pagination>
+          }
           @if (object?.id) {
             <small class="d-block mt-2" i18n>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</small>
           }
index 62a0954a1f80bbf744f02ecfe9646866942de233..4486003dec65630c33b1323c3c18d1cce2f285a3 100644 (file)
@@ -125,4 +125,42 @@ describe('CustomFieldEditDialogComponent', () => {
     fixture.detectChanges()
     expect(document.activeElement).toBe(selectOptionInputs.last.nativeElement)
   })
+
+  it('should send all select options including those changed in form on save', () => {
+    component.dialogMode = EditDialogMode.EDIT
+    component.object = {
+      id: 1,
+      name: 'Field 1',
+      data_type: CustomFieldDataType.Select,
+      extra_data: {
+        select_options: Array.from({ length: 50 }, (_, i) => ({
+          label: `Option ${i + 1}`,
+          id: `${i + 1}-xyz`,
+        })),
+      },
+    }
+    fixture.detectChanges()
+    component.ngOnInit()
+    component.selectOptionsPage = 2
+    fixture.detectChanges()
+    component.objectForm
+      .get('extra_data')
+      .get('select_options')
+      .get('0')
+      .get('label')
+      .setValue('Updated Option 9')
+    const formValues = (component as any).getFormValues()
+    // first item unchanged
+    expect(formValues.extra_data.select_options[0]).toEqual({
+      label: 'Option 1',
+      id: '1-xyz',
+    })
+    // page 2 first item updated
+    expect(
+      formValues.extra_data.select_options[component.SELECT_OPTION_PAGE_SIZE]
+    ).toEqual({
+      label: 'Updated Option 9',
+      id: '9-xyz',
+    })
+  })
 })
index ce3be7e661f4ae13f8395a394ed136fe47c3717e..617d825b22f1959d8f6e251928355e31dd412501 100644 (file)
@@ -14,6 +14,7 @@ import {
   FormsModule,
   ReactiveFormsModule,
 } from '@angular/forms'
+import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 import { takeUntil } from 'rxjs'
 import {
@@ -28,6 +29,8 @@ import { SelectComponent } from '../../input/select/select.component'
 import { TextComponent } from '../../input/text/text.component'
 import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
 
+const SELECT_OPTION_PAGE_SIZE = 8
+
 @Component({
   selector: 'pngx-custom-field-edit-dialog',
   templateUrl: './custom-field-edit-dialog.component.html',
@@ -37,6 +40,7 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
     TextComponent,
     FormsModule,
     ReactiveFormsModule,
+    NgbPaginationModule,
     NgxBootstrapIconsModule,
   ],
 })
@@ -45,6 +49,21 @@ export class CustomFieldEditDialogComponent
   implements OnInit, AfterViewInit
 {
   CustomFieldDataType = CustomFieldDataType
+  SELECT_OPTION_PAGE_SIZE = SELECT_OPTION_PAGE_SIZE
+
+  private _allSelectOptions: any[] = []
+  public get allSelectOptions(): any[] {
+    return this._allSelectOptions
+  }
+
+  private _selectOptionsPage: number
+  public get selectOptionsPage(): number {
+    return this._selectOptionsPage
+  }
+  public set selectOptionsPage(v: number) {
+    this._selectOptionsPage = v
+    this.updateSelectOptions()
+  }
 
   @ViewChildren('selectOption')
   private selectOptionInputs: QueryList<ElementRef>
@@ -67,17 +86,10 @@ export class CustomFieldEditDialogComponent
       this.objectForm.get('data_type').disable()
     }
     if (this.object?.data_type === CustomFieldDataType.Select) {
-      this.selectOptions.clear()
-      this.object.extra_data.select_options
-        .filter((option) => option)
-        .forEach((option) =>
-          this.selectOptions.push(
-            new FormGroup({
-              label: new FormControl(option.label),
-              id: new FormControl(option.id),
-            })
-          )
-        )
+      this._allSelectOptions = [
+        ...(this.object.extra_data.select_options ?? []),
+      ]
+      this.selectOptionsPage = 1
     }
   }
 
@@ -87,6 +99,19 @@ export class CustomFieldEditDialogComponent
       .subscribe(() => {
         this.selectOptionInputs.last?.nativeElement.focus()
       })
+
+    this.objectForm.valueChanges
+      .pipe(takeUntil(this.unsubscribeNotifier))
+      .subscribe((change) => {
+        // Update the relevant select options values if changed in the form, which is only a page of the entire list
+        this.objectForm
+          .get('extra_data.select_options')
+          ?.value.forEach((option, index) => {
+            this._allSelectOptions[
+              index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE
+            ] = option
+          })
+      })
   }
 
   getCreateTitle() {
@@ -108,6 +133,17 @@ export class CustomFieldEditDialogComponent
     })
   }
 
+  protected getFormValues() {
+    const formValues = super.getFormValues()
+    if (
+      this.objectForm.get('data_type')?.value === CustomFieldDataType.Select
+    ) {
+      // Make sure we send all select options, with updated values
+      formValues.extra_data.select_options = this._allSelectOptions
+    }
+    return formValues
+  }
+
   getDataTypes() {
     return DATA_TYPE_LABELS
   }
@@ -116,13 +152,35 @@ export class CustomFieldEditDialogComponent
     return this.dialogMode === EditDialogMode.EDIT
   }
 
+  private updateSelectOptions() {
+    this.selectOptions.clear()
+    this._allSelectOptions
+      .slice(
+        (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
+        this.selectOptionsPage * SELECT_OPTION_PAGE_SIZE
+      )
+      .forEach((option) =>
+        this.selectOptions.push(
+          new FormGroup({
+            label: new FormControl(option.label),
+            id: new FormControl(option.id),
+          })
+        )
+      )
+  }
+
   public addSelectOption() {
-    this.selectOptions.push(
-      new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
+    this._allSelectOptions.push({ label: null, id: null })
+    this.selectOptionsPage = Math.ceil(
+      this.allSelectOptions.length / SELECT_OPTION_PAGE_SIZE
     )
   }
 
   public removeSelectOption(index: number) {
     this.selectOptions.removeAt(index)
+    this._allSelectOptions.splice(
+      index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
+      1
+    )
   }
 }
index fa35dc6bf56132d58cd7c1257966946954a91151..75534a777b8e619d17d5d44c5a226f49e4017e8f 100644 (file)
@@ -147,9 +147,13 @@ export abstract class EditDialogComponent<
     )
   }
 
+  protected getFormValues(): any {
+    return Object.assign({}, this.objectForm.value)
+  }
+
   save() {
     this.error = null
-    const formValues = Object.assign({}, this.objectForm.value)
+    const formValues = this.getFormValues()
     const permissionsObject: PermissionsFormObject =
       this.objectForm.get('permissions_form')?.value
     if (permissionsObject) {