mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Enhancement: use stable unique IDs for custom field select options (#8299)
This commit is contained in:
@@ -17,7 +17,11 @@ const customFields: CustomField[] = [
|
||||
name: 'Field 4',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: {
|
||||
select_options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
{ label: 'Option 3', id: 'ghi-789' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -131,6 +135,8 @@ describe('CustomFieldDisplayComponent', () => {
|
||||
})
|
||||
|
||||
it('should show select value', () => {
|
||||
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
|
||||
expect(component.getSelectValue(customFields[3], 'ghi-789')).toEqual(
|
||||
'Option 3'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -117,8 +117,8 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
||||
}
|
||||
|
||||
public getSelectValue(field: CustomField, index: number): string {
|
||||
return field.extra_data.select_options[index]
|
||||
public getSelectValue(field: CustomField, id: string): string {
|
||||
return field.extra_data.select_options?.find((o) => o.id === id)?.label
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@@ -44,6 +44,8 @@
|
||||
<ng-select #fieldSelects
|
||||
class="paperless-input-select rounded-end"
|
||||
[items]="getSelectOptionsForField(atom.field)"
|
||||
bindLabel="label"
|
||||
bindValue="id"
|
||||
[(ngModel)]="atom.value"
|
||||
[disabled]="disabled"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
@@ -99,6 +101,8 @@
|
||||
<ng-select
|
||||
class="paperless-input-select rounded-end"
|
||||
[items]="getSelectOptionsForField(atom.field)"
|
||||
bindLabel="label"
|
||||
bindValue="id"
|
||||
[(ngModel)]="atom.value"
|
||||
[disabled]="disabled"
|
||||
[multiple]="true"
|
||||
|
@@ -39,7 +39,12 @@ const customFields = [
|
||||
id: 2,
|
||||
name: 'Test Select Field',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
||||
extra_data: {
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -128,11 +133,19 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
||||
id: 1,
|
||||
name: 'Test Field',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
||||
extra_data: {
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
],
|
||||
},
|
||||
}
|
||||
component.customFields = [field]
|
||||
const options = component.getSelectOptionsForField(1)
|
||||
expect(options).toEqual(['Option 1', 'Option 2'])
|
||||
expect(options).toEqual([
|
||||
{ label: 'Option 1', id: 'abc-123' },
|
||||
{ label: 'Option 2', id: 'def-456' },
|
||||
])
|
||||
|
||||
// Fallback to empty array if field is not found
|
||||
const options2 = component.getSelectOptionsForField(2)
|
||||
|
@@ -311,7 +311,9 @@ export class CustomFieldsQueryDropdownComponent implements OnDestroy {
|
||||
}))
|
||||
}
|
||||
|
||||
getSelectOptionsForField(fieldID: number): string[] {
|
||||
getSelectOptionsForField(
|
||||
fieldID: number
|
||||
): Array<{ label: string; id: string }> {
|
||||
const field = this.customFields.find((field) => field.id === fieldID)
|
||||
if (field) {
|
||||
return field.extra_data['select_options']
|
||||
|
@@ -21,8 +21,9 @@
|
||||
</button>
|
||||
<div formArrayName="select_options">
|
||||
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
|
||||
<div class="input-group input-group-sm my-2">
|
||||
<input #selectOption type="text" class="form-control" [formControl]="option" autocomplete="off">
|
||||
<div class="input-group input-group-sm my-2" [formGroup]="objectForm.controls.extra_data.controls.select_options.controls[i]">
|
||||
<input #selectOption type="text" class="form-control" formControlName="label" autocomplete="off">
|
||||
<input type="hidden" formControlName="id">
|
||||
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
|
||||
</div>
|
||||
}
|
||||
|
@@ -80,7 +80,11 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
name: 'Field 1',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: {
|
||||
select_options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
select_options: [
|
||||
{ label: 'Option 1', id: '123-xyz' },
|
||||
{ label: 'Option 2', id: '456-abc' },
|
||||
{ label: 'Option 3', id: '789-123' },
|
||||
],
|
||||
},
|
||||
}
|
||||
fixture.detectChanges()
|
||||
@@ -94,6 +98,10 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
fixture.detectChanges()
|
||||
component.ngOnInit()
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(0)
|
||||
component.addSelectOption()
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(1)
|
||||
@@ -101,14 +109,10 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(2)
|
||||
component.addSelectOption()
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(3)
|
||||
component.removeSelectOption(0)
|
||||
expect(
|
||||
component.objectForm.get('extra_data').get('select_options').value.length
|
||||
).toBe(2)
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('should focus on last select option input', () => {
|
||||
|
@@ -57,9 +57,16 @@ export class CustomFieldEditDialogComponent
|
||||
}
|
||||
if (this.object?.data_type === CustomFieldDataType.Select) {
|
||||
this.selectOptions.clear()
|
||||
this.object.extra_data.select_options.forEach((option) =>
|
||||
this.selectOptions.push(new FormControl(option))
|
||||
)
|
||||
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),
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +96,7 @@ export class CustomFieldEditDialogComponent
|
||||
name: new FormControl(null),
|
||||
data_type: new FormControl(null),
|
||||
extra_data: new FormGroup({
|
||||
select_options: new FormArray([new FormControl(null)]),
|
||||
select_options: new FormArray([]),
|
||||
default_currency: new FormControl(null),
|
||||
}),
|
||||
})
|
||||
@@ -104,7 +111,9 @@ export class CustomFieldEditDialogComponent
|
||||
}
|
||||
|
||||
public addSelectOption() {
|
||||
this.selectOptions.push(new FormControl(''))
|
||||
this.selectOptions.push(
|
||||
new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
|
||||
)
|
||||
}
|
||||
|
||||
public removeSelectOption(index: number) {
|
||||
|
@@ -132,12 +132,4 @@ describe('SelectComponent', () => {
|
||||
const expectedTitle = `Filter documents with this ${component.title}`
|
||||
expect(component.filterButtonTitle).toEqual(expectedTitle)
|
||||
})
|
||||
|
||||
it('should support setting items as a plain array', () => {
|
||||
component.itemsArray = ['foo', 'bar']
|
||||
expect(component.items).toEqual([
|
||||
{ id: 0, name: 'foo' },
|
||||
{ id: 1, name: 'bar' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@@ -34,11 +34,6 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
if (items && this.value) this.checkForPrivateItems(this.value)
|
||||
}
|
||||
|
||||
@Input()
|
||||
set itemsArray(items: any[]) {
|
||||
this._items = items.map((item, index) => ({ id: index, name: item }))
|
||||
}
|
||||
|
||||
writeValue(newValue: any): void {
|
||||
if (newValue && this._items) {
|
||||
this.checkForPrivateItems(newValue)
|
||||
|
@@ -190,7 +190,8 @@
|
||||
@case (CustomFieldDataType.Select) {
|
||||
<pngx-input-select formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
|
||||
[items]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
|
||||
bindLabel="label"
|
||||
[allowNull]="true"
|
||||
[horizontal]="true"
|
||||
[removable]="userIsOwner"
|
||||
|
@@ -56,7 +56,7 @@ export interface CustomField extends ObjectWithId {
|
||||
name: string
|
||||
created?: Date
|
||||
extra_data?: {
|
||||
select_options?: string[]
|
||||
select_options?: Array<{ label: string; id: string }>
|
||||
default_currency?: string
|
||||
}
|
||||
document_count?: number
|
||||
|
Reference in New Issue
Block a user