Enhancement: long text custom field (#10846)

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
jojo2357
2025-09-13 21:19:00 -06:00
committed by GitHub
parent d230514dd3
commit feb5d534b5
15 changed files with 95 additions and 5 deletions

View File

@@ -35,6 +35,9 @@
@case (CustomFieldDataType.Select) { @case (CustomFieldDataType.Select) {
<span [ngbTooltip]="nameTooltip">{{getSelectValue(field, value)}}</span> <span [ngbTooltip]="nameTooltip">{{getSelectValue(field, value)}}</span>
} }
@case (CustomFieldDataType.LongText) {
<p class="mb-0" [ngbTooltip]="nameTooltip">{{value | slice:0:20}}{{value.length > 20 ? '...' : ''}}</p>
}
@default { @default {
<span [ngbTooltip]="nameTooltip">{{value}}</span> <span [ngbTooltip]="nameTooltip">{{value}}</span>
} }

View File

@@ -1,5 +1,5 @@
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common' import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
import { Component, Input, LOCALE_ID, OnInit, inject } from '@angular/core' import { Component, inject, Input, LOCALE_ID, OnInit } from '@angular/core'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
@@ -14,7 +14,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
selector: 'pngx-custom-field-display', selector: 'pngx-custom-field-display',
templateUrl: './custom-field-display.component.html', templateUrl: './custom-field-display.component.html',
styleUrl: './custom-field-display.component.scss', styleUrl: './custom-field-display.component.scss',
imports: [CustomDatePipe, CurrencyPipe, NgbTooltipModule], imports: [CustomDatePipe, CurrencyPipe, NgbTooltipModule, SlicePipe],
}) })
export class CustomFieldDisplayComponent export class CustomFieldDisplayComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions

View File

@@ -68,6 +68,11 @@
[allowNull]="true" [allowNull]="true"
[horizontal]="true"></pngx-input-select> [horizontal]="true"></pngx-input-select>
} }
@case (CustomFieldDataType.LongText) {
<pngx-input-textarea [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
[title]="getCustomField(fieldId)?.name"
class="flex-grow-1"></pngx-input-textarea>
}
} }
<button type="button" class="btn btn-link text-danger" (click)="removeSelectedField.next(fieldId)"> <button type="button" class="btn btn-link text-danger" (click)="removeSelectedField.next(fieldId)">
<i-bs name="trash"></i-bs> <i-bs name="trash"></i-bs>

View File

@@ -24,6 +24,7 @@ import { MonetaryComponent } from '../monetary/monetary.component'
import { NumberComponent } from '../number/number.component' import { NumberComponent } from '../number/number.component'
import { SelectComponent } from '../select/select.component' import { SelectComponent } from '../select/select.component'
import { TextComponent } from '../text/text.component' import { TextComponent } from '../text/text.component'
import { TextAreaComponent } from '../textarea/textarea.component'
import { UrlComponent } from '../url/url.component' import { UrlComponent } from '../url/url.component'
@Component({ @Component({
@@ -51,6 +52,7 @@ import { UrlComponent } from '../url/url.component'
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
TextAreaComponent,
], ],
}) })
export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> { export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> {

View File

@@ -4,6 +4,7 @@ import {
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { AbstractInputComponent } from '../abstract-input' import { AbstractInputComponent } from '../abstract-input'
@@ -18,7 +19,12 @@ import { AbstractInputComponent } from '../abstract-input'
selector: 'pngx-input-textarea', selector: 'pngx-input-textarea',
templateUrl: './textarea.component.html', templateUrl: './textarea.component.html',
styleUrls: ['./textarea.component.scss'], styleUrls: ['./textarea.component.scss'],
imports: [FormsModule, ReactiveFormsModule, SafeHtmlPipe], imports: [
FormsModule,
ReactiveFormsModule,
SafeHtmlPipe,
NgxBootstrapIconsModule,
],
}) })
export class TextAreaComponent extends AbstractInputComponent<string> { export class TextAreaComponent extends AbstractInputComponent<string> {
@Input() @Input()

View File

@@ -216,6 +216,14 @@
(removed)="removeField(fieldInstance)" (removed)="removeField(fieldInstance)"
[error]="getCustomFieldError(i)"></pngx-input-select> [error]="getCustomFieldError(i)"></pngx-input-select>
} }
@case (CustomFieldDataType.LongText) {
<pngx-input-textarea formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
[removable]="userCanEdit"
(removed)="removeField(fieldInstance)"
[horizontal]="true"
[error]="getCustomFieldError(i)"></pngx-input-textarea>
}
} }
</div> </div>
} }

View File

@@ -98,6 +98,7 @@ import { PermissionsFormComponent } from '../common/input/permissions/permission
import { SelectComponent } from '../common/input/select/select.component' import { SelectComponent } from '../common/input/select/select.component'
import { TagsComponent } from '../common/input/tags/tags.component' import { TagsComponent } from '../common/input/tags/tags.component'
import { TextComponent } from '../common/input/text/text.component' import { TextComponent } from '../common/input/text/text.component'
import { TextAreaComponent } from '../common/input/textarea/textarea.component'
import { UrlComponent } from '../common/input/url/url.component' import { UrlComponent } from '../common/input/url/url.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component' import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { import {
@@ -173,6 +174,7 @@ export enum ZoomSetting {
NgbDropdownModule, NgbDropdownModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
PdfViewerModule, PdfViewerModule,
TextAreaComponent,
], ],
}) })
export class DocumentDetailComponent export class DocumentDetailComponent

View File

@@ -56,6 +56,10 @@
[items]="field.extra_data.select_options" bindLabel="label" [allowNull]="true" [horizontal]="true"> [items]="field.extra_data.select_options" bindLabel="label" [allowNull]="true" [horizontal]="true">
</pngx-input-select> </pngx-input-select>
} }
@case (CustomFieldDataType.LongText) {
<pngx-input-textarea formControlName="{{field.id}}" class="w-100" [title]="field.name" [horizontal]="true">
</pngx-input-textarea>
}
} }
<button type="button" class="btn btn-outline-danger mb-3" (click)="removeField(field.id)"> <button type="button" class="btn btn-outline-danger mb-3" (click)="removeField(field.id)">
<i-bs name="x"></i-bs> <i-bs name="x"></i-bs>

View File

@@ -18,6 +18,7 @@ import { TextComponent } from 'src/app/components/common/input/text/text.compone
import { UrlComponent } from 'src/app/components/common/input/url/url.component' import { UrlComponent } from 'src/app/components/common/input/url/url.component'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { TextAreaComponent } from '../../../common/input/textarea/textarea.component'
@Component({ @Component({
selector: 'pngx-custom-fields-bulk-edit-dialog', selector: 'pngx-custom-fields-bulk-edit-dialog',
@@ -35,6 +36,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
TextAreaComponent,
], ],
}) })
export class CustomFieldsBulkEditDialogComponent { export class CustomFieldsBulkEditDialogComponent {

View File

@@ -114,6 +114,10 @@ export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = {
CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.Exact,
CustomFieldQueryOperatorGroups.Subset, CustomFieldQueryOperatorGroups.Subset,
], ],
[CustomFieldDataType.LongText]: [
CustomFieldQueryOperatorGroups.Basic,
CustomFieldQueryOperatorGroups.String,
],
} }
export const CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR = { export const CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR = {

View File

@@ -10,6 +10,7 @@ export enum CustomFieldDataType {
Monetary = 'monetary', Monetary = 'monetary',
DocumentLink = 'documentlink', DocumentLink = 'documentlink',
Select = 'select', Select = 'select',
LongText = 'longtext',
} }
export const DATA_TYPE_LABELS = [ export const DATA_TYPE_LABELS = [
@@ -49,6 +50,10 @@ export const DATA_TYPE_LABELS = [
id: CustomFieldDataType.Select, id: CustomFieldDataType.Select,
name: $localize`Select`, name: $localize`Select`,
}, },
{
id: CustomFieldDataType.LongText,
name: $localize`Long Text`,
},
] ]
export interface CustomField extends ObjectWithId { export interface CustomField extends ObjectWithId {

View File

@@ -230,6 +230,7 @@ class CustomFieldsFilter(Filter):
| qs.filter(custom_fields__value_monetary__icontains=value) | qs.filter(custom_fields__value_monetary__icontains=value)
| qs.filter(custom_fields__value_document_ids__icontains=value) | qs.filter(custom_fields__value_document_ids__icontains=value)
| qs.filter(custom_fields__value_select__in=option_ids) | qs.filter(custom_fields__value_select__in=option_ids)
| qs.filter(custom_fields__value_long_text__icontains=value)
) )
else: else:
return qs return qs
@@ -314,6 +315,7 @@ class CustomFieldQueryParser:
CustomField.FieldDataType.MONETARY: ("basic", "string", "arithmetic"), CustomField.FieldDataType.MONETARY: ("basic", "string", "arithmetic"),
CustomField.FieldDataType.DOCUMENTLINK: ("basic", "containment"), CustomField.FieldDataType.DOCUMENTLINK: ("basic", "containment"),
CustomField.FieldDataType.SELECT: ("basic",), CustomField.FieldDataType.SELECT: ("basic",),
CustomField.FieldDataType.LONG_TEXT: ("basic", "string"),
} }
DATE_COMPONENTS = [ DATE_COMPONENTS = [
@@ -845,7 +847,10 @@ class DocumentsOrderingFilter(OrderingFilter):
annotation = None annotation = None
match field.data_type: match field.data_type:
case CustomField.FieldDataType.STRING: case (
CustomField.FieldDataType.STRING
| CustomField.FieldDataType.LONG_TEXT
):
annotation = Subquery( annotation = Subquery(
CustomFieldInstance.objects.filter( CustomFieldInstance.objects.filter(
document_id=OuterRef("id"), document_id=OuterRef("id"),

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.2.6 on 2025-09-13 17:11
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1069_workflowtrigger_filter_has_storage_path_and_more"),
]
operations = [
migrations.AddField(
model_name="customfieldinstance",
name="value_long_text",
field=models.TextField(null=True),
),
migrations.AlterField(
model_name="customfield",
name="data_type",
field=models.CharField(
choices=[
("string", "String"),
("url", "URL"),
("date", "Date"),
("boolean", "Boolean"),
("integer", "Integer"),
("float", "Float"),
("monetary", "Monetary"),
("documentlink", "Document Link"),
("select", "Select"),
("longtext", "Long Text"),
],
editable=False,
max_length=50,
verbose_name="data type",
),
),
]

View File

@@ -759,6 +759,7 @@ class CustomField(models.Model):
MONETARY = ("monetary", _("Monetary")) MONETARY = ("monetary", _("Monetary"))
DOCUMENTLINK = ("documentlink", _("Document Link")) DOCUMENTLINK = ("documentlink", _("Document Link"))
SELECT = ("select", _("Select")) SELECT = ("select", _("Select"))
LONG_TEXT = ("longtext", _("Long Text"))
created = models.DateTimeField( created = models.DateTimeField(
_("created"), _("created"),
@@ -816,6 +817,7 @@ class CustomFieldInstance(SoftDeleteModel):
CustomField.FieldDataType.MONETARY: "value_monetary", CustomField.FieldDataType.MONETARY: "value_monetary",
CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids", CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
CustomField.FieldDataType.SELECT: "value_select", CustomField.FieldDataType.SELECT: "value_select",
CustomField.FieldDataType.LONG_TEXT: "value_long_text",
} }
created = models.DateTimeField( created = models.DateTimeField(
@@ -883,6 +885,8 @@ class CustomFieldInstance(SoftDeleteModel):
value_select = models.CharField(null=True, max_length=16) value_select = models.CharField(null=True, max_length=16)
value_long_text = models.TextField(null=True)
class Meta: class Meta:
ordering = ("created",) ordering = ("created",)
verbose_name = _("custom field instance") verbose_name = _("custom field instance")

View File

@@ -202,6 +202,7 @@ def get_custom_fields_context(
CustomField.FieldDataType.MONETARY, CustomField.FieldDataType.MONETARY,
CustomField.FieldDataType.STRING, CustomField.FieldDataType.STRING,
CustomField.FieldDataType.URL, CustomField.FieldDataType.URL,
CustomField.FieldDataType.LONG_TEXT,
}: }:
value = pathvalidate.sanitize_filename( value = pathvalidate.sanitize_filename(
field_instance.value, field_instance.value,