Working removal of custom fields

This commit is contained in:
shamoon 2023-10-31 13:09:05 -07:00
parent 44728aa048
commit 7879b9e91b
12 changed files with 122 additions and 38 deletions

View File

@ -69,7 +69,7 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
private updateUnusedFields() { private updateUnusedFields() {
this.unusedFields = this.customFields.filter( this.unusedFields = this.customFields.filter(
(f) => !this.existingFields.find((e) => e.field.id === f.id) (f) => !this.existingFields?.find((e) => e.field.id === f.id)
) )
} }

View File

@ -1,4 +1,12 @@
import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core' import {
Directive,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core'
import { ControlValueAccessor } from '@angular/forms' import { ControlValueAccessor } from '@angular/forms'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -41,9 +49,18 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
@Input() @Input()
error: string error: string
@Input()
hint: string
@Input() @Input()
horizontal: boolean = false horizontal: boolean = false
@Input()
removable: boolean = false
@Output()
removed: EventEmitter<AbstractInputComponent<any>> = new EventEmitter()
value: T value: T
ngOnInit(): void { ngOnInit(): void {
@ -51,7 +68,4 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
} }
inputId: string inputId: string
@Input()
hint: string
} }

View File

@ -1,7 +1,12 @@
<div class="mb-3"> <div class="mb-3">
<div class="row"> <div class="row">
<div class="d-flex align-items-center" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label> <label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">

View File

@ -1,7 +1,12 @@
<div class="mb-3"> <div class="mb-3">
<div class="row"> <div class="row">
<div class="d-flex align-items-center" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label> <label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">

View File

@ -1,7 +1,12 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled"> <div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row"> <div class="row">
<div class="d-flex align-items-center" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label mb-md-0" [for]="inputId">{{title}}</label> <label *ngIf="title" class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div [class.input-group]="allowCreateNew || showFilter"> <div [class.input-group]="allowCreateNew || showFilter">

View File

@ -1,7 +1,12 @@
<div class="mb-3"> <div class="mb-3">
<div class="row"> <div class="row">
<div class="d-flex align-items-center" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label> <label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled"> <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">

View File

@ -51,7 +51,7 @@
<pngx-custom-fields-dropdown <pngx-custom-fields-dropdown
[documentId]="documentId" [documentId]="documentId"
[disabled]="!userIsOwner" [disabled]="!userIsOwner"
[existingFields]="customFields" [existingFields]="document?.custom_fields"
(added)="addField($event)"> (added)="addField($event)">
</pngx-custom-fields-dropdown> </pngx-custom-fields-dropdown>
@ -100,11 +100,11 @@
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" <pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select> (createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags> <pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
<ng-container *ngFor="let fieldInstance of customFields; let i = index"> <ng-container *ngFor="let fieldInstance of document?.custom_fields; let i = index">
<div [formGroup]="customFieldFormFields.controls[i]"> <div [formGroup]="customFieldFormFields.controls[i]">
<pngx-input-text formControlName="value" *ngIf="fieldInstance.field.data_type === PaperlessCustomFieldDataType.String" [title]="fieldInstance.field.name" [horizontal]="true"></pngx-input-text> <pngx-input-text formControlName="value" *ngIf="fieldInstance.field.data_type === PaperlessCustomFieldDataType.String" [title]="fieldInstance.field.name" [removable]="true" (removed)="removeField($event)" [horizontal]="true"></pngx-input-text>
<pngx-input-date formControlName="value" *ngIf="fieldInstance.field.data_type === PaperlessCustomFieldDataType.Date" [title]="fieldInstance.field.name" [horizontal]="true"></pngx-input-date> <pngx-input-date formControlName="value" *ngIf="fieldInstance.field.data_type === PaperlessCustomFieldDataType.Date" [title]="fieldInstance.field.name" [removable]="true" (removed)="removeField($event)" [horizontal]="true"></pngx-input-date>
<pngx-input-number formControlName="value" *ngIf="fieldInstance.field.data_type === PaperlessCustomFieldDataType.Integer" [title]="fieldInstance.field.name" [horizontal]="true" [showAdd]="false"></pngx-input-number> <pngx-input-number formControlName="value" *ngIf="fieldInstance.field.data_type === PaperlessCustomFieldDataType.Integer" [title]="fieldInstance.field.name" [removable]="true" (removed)="removeField($event)" [horizontal]="true" [showAdd]="false"></pngx-input-number>
</div> </div>
</ng-container> </ng-container>
</div> </div>

View File

@ -68,6 +68,7 @@ import {
PaperlessCustomFieldDataType, PaperlessCustomFieldDataType,
} from 'src/app/data/paperless-custom-field' } from 'src/app/data/paperless-custom-field'
import { PaperlessCustomFieldInstance } from 'src/app/data/paperless-custom-field-instance' import { PaperlessCustomFieldInstance } from 'src/app/data/paperless-custom-field-instance'
import { AbstractInputComponent } from '../common/input/abstract-input'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@ -141,7 +142,6 @@ export class DocumentDetailComponent
ogDate: Date ogDate: Date
public readonly PaperlessCustomFieldDataType = PaperlessCustomFieldDataType public readonly PaperlessCustomFieldDataType = PaperlessCustomFieldDataType
customFields: PaperlessCustomFieldInstance[] = []
@ViewChild('nav') nav: NgbNav @ViewChild('nav') nav: NgbNav
@ViewChild('pdfPreview') set pdfPreview(element) { @ViewChild('pdfPreview') set pdfPreview(element) {
@ -394,7 +394,7 @@ export class DocumentDetailComponent
updateComponent(doc: PaperlessDocument) { updateComponent(doc: PaperlessDocument) {
this.document = doc this.document = doc
this.requiresPassword = false this.requiresPassword = false
this.customFields = doc.custom_fields // this.customFields = doc.custom_fields.concat([])
this.updateFormForCustomFields() this.updateFormForCustomFields()
this.documentsService this.documentsService
.getMetadata(doc.id) .getMetadata(doc.id)
@ -448,23 +448,6 @@ export class DocumentDetailComponent
return this.documentForm.get('custom_fields') as FormArray return this.documentForm.get('custom_fields') as FormArray
} }
updateFormForCustomFields() {
this.customFieldFormFields.clear()
this.customFields.forEach((fieldInstance) => {
this.customFieldFormFields.push(
new FormGroup({
field: new FormGroup({
id: new FormControl(fieldInstance.field.id),
name: new FormControl(fieldInstance.field.name),
data_type: new FormControl(fieldInstance.field.data_type),
}),
value: new FormControl(fieldInstance.value),
}),
{ emitEvent: false }
)
})
}
createDocumentType(newName: string) { createDocumentType(newName: string) {
var modal = this.modalService.open(DocumentTypeEditDialogComponent, { var modal = this.modalService.open(DocumentTypeEditDialogComponent, {
backdrop: 'static', backdrop: 'static',
@ -542,9 +525,8 @@ export class DocumentDetailComponent
set_permissions: doc.permissions, set_permissions: doc.permissions,
} }
this.title = doc.title this.title = doc.title
this.documentForm.patchValue(doc)
this.customFields = doc.custom_fields
this.updateFormForCustomFields() this.updateFormForCustomFields()
this.documentForm.patchValue(doc)
this.openDocumentService.setDirty(doc, false) this.openDocumentService.setDirty(doc, false)
}, },
error: () => { error: () => {
@ -855,13 +837,41 @@ export class DocumentDetailComponent
this.documentListViewService.quickFilter(filterRules) this.documentListViewService.quickFilter(filterRules)
} }
updateFormForCustomFields(emitEvent: boolean = false) {
this.customFieldFormFields.clear({ emitEvent: false })
this.document.custom_fields.forEach((fieldInstance) => {
this.customFieldFormFields.push(
new FormGroup({
field: new FormGroup({
id: new FormControl(fieldInstance.field.id),
name: new FormControl(fieldInstance.field.name),
data_type: new FormControl(fieldInstance.field.data_type),
}),
value: new FormControl(fieldInstance.value),
}),
{ emitEvent }
)
})
}
addField(field: PaperlessCustomField) { addField(field: PaperlessCustomField) {
this.customFields.push({ this.document.custom_fields.push({
field, field,
value: null, value: null,
document: this.documentId, document: this.documentId,
created: new Date(), created: new Date(),
}) })
this.updateFormForCustomFields() this.updateFormForCustomFields(true)
}
removeField(input: AbstractInputComponent<any>) {
// ok for now as custom field name unique is a constraint
const customFieldIndex = this.document.custom_fields.findIndex(
(f) => f.field.name === input.title
)
if (customFieldIndex) {
this.document.custom_fields.splice(customFieldIndex, 1)
this.updateFormForCustomFields(true)
}
} }
} }

View File

@ -661,3 +661,17 @@ code {
.cdk-drag-animating { .cdk-drag-animating {
transition: transform 300ms cubic-bezier(0, 0, 0.2, 1); transition: transform 300ms cubic-bezier(0, 0, 0.2, 1);
} }
.hidden-button-container {
button {
opacity: 0;
pointer-events: none;
transition: opacity .2s ease;
}
&:hover {
button {
opacity: 1;
pointer-events: initial;
}
}
}

View File

@ -219,6 +219,13 @@ class Migration(migrations.Migration):
), ),
], ],
), ),
migrations.AddConstraint(
model_name="customfield",
constraint=models.UniqueConstraint(
fields=("name",),
name="documents_customfield_unique_name",
),
),
migrations.AddConstraint( migrations.AddConstraint(
model_name="customfieldinstance", model_name="customfieldinstance",
constraint=models.UniqueConstraint( constraint=models.UniqueConstraint(

View File

@ -916,6 +916,12 @@ class CustomField(models.Model):
ordering = ("created",) ordering = ("created",)
verbose_name = _("custom field") verbose_name = _("custom field")
verbose_name_plural = _("custom fields") verbose_name_plural = _("custom fields")
constraints = [
models.UniqueConstraint(
fields=["name"],
name="%(app_label)s_%(class)s_unique_name",
),
]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.name} : {self.data_type}" return f"{self.name} : {self.data_type}"

View File

@ -464,7 +464,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer):
field_instance["value"] = data_custom_field["value"] field_instance["value"] = data_custom_field["value"]
return values return values
def update(self, instance, validated_data): def update(self, instance: Document, validated_data):
if "custom_fields" in validated_data: if "custom_fields" in validated_data:
custom_fields = validated_data.pop("custom_fields") custom_fields = validated_data.pop("custom_fields")
for field_data in custom_fields: for field_data in custom_fields:
@ -473,6 +473,19 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer):
field=field_data["field"], field=field_data["field"],
value=field_data["value"], value=field_data["value"],
) )
existing_fields = CustomFieldInstance.objects.filter(document=instance)
for existing_field in existing_fields:
if (
not len(
[
f
for f in custom_fields
if f["field"]["id"] == existing_field.field.id
],
)
> 0
):
existing_field.delete()
if "created_date" in validated_data and "created" not in validated_data: if "created_date" in validated_data and "created" not in validated_data:
new_datetime = datetime.datetime.combine( new_datetime = datetime.datetime.combine(
validated_data.get("created_date"), validated_data.get("created_date"),