@case (CustomFieldDataType.Select) {
<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 {
<span [ngbTooltip]="nameTooltip">{{value}}</span>
}
-import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
-import { Component, Input, LOCALE_ID, OnInit, inject } from '@angular/core'
+import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
+import { Component, inject, Input, LOCALE_ID, OnInit } from '@angular/core'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
selector: 'pngx-custom-field-display',
templateUrl: './custom-field-display.component.html',
styleUrl: './custom-field-display.component.scss',
- imports: [CustomDatePipe, CurrencyPipe, NgbTooltipModule],
+ imports: [CustomDatePipe, CurrencyPipe, NgbTooltipModule, SlicePipe],
})
export class CustomFieldDisplayComponent
extends LoadingComponentWithPermissions
[allowNull]="true"
[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)">
<i-bs name="trash"></i-bs>
import { NumberComponent } from '../number/number.component'
import { SelectComponent } from '../select/select.component'
import { TextComponent } from '../text/text.component'
+import { TextAreaComponent } from '../textarea/textarea.component'
import { UrlComponent } from '../url/url.component'
@Component({
ReactiveFormsModule,
RouterModule,
NgxBootstrapIconsModule,
+ TextAreaComponent,
],
})
export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> {
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
+import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { AbstractInputComponent } from '../abstract-input'
selector: 'pngx-input-textarea',
templateUrl: './textarea.component.html',
styleUrls: ['./textarea.component.scss'],
- imports: [FormsModule, ReactiveFormsModule, SafeHtmlPipe],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ SafeHtmlPipe,
+ NgxBootstrapIconsModule,
+ ],
})
export class TextAreaComponent extends AbstractInputComponent<string> {
@Input()
(removed)="removeField(fieldInstance)"
[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>
}
import { SelectComponent } from '../common/input/select/select.component'
import { TagsComponent } from '../common/input/tags/tags.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 { PageHeaderComponent } from '../common/page-header/page-header.component'
import {
NgbDropdownModule,
NgxBootstrapIconsModule,
PdfViewerModule,
+ TextAreaComponent,
],
})
export class DocumentDetailComponent
[items]="field.extra_data.select_options" bindLabel="label" [allowNull]="true" [horizontal]="true">
</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)">
<i-bs name="x"></i-bs>
import { UrlComponent } from 'src/app/components/common/input/url/url.component'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { DocumentService } from 'src/app/services/rest/document.service'
+import { TextAreaComponent } from '../../../common/input/textarea/textarea.component'
@Component({
selector: 'pngx-custom-fields-bulk-edit-dialog',
FormsModule,
ReactiveFormsModule,
NgxBootstrapIconsModule,
+ TextAreaComponent,
],
})
export class CustomFieldsBulkEditDialogComponent {
CustomFieldQueryOperatorGroups.Exact,
CustomFieldQueryOperatorGroups.Subset,
],
+ [CustomFieldDataType.LongText]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.String,
+ ],
}
export const CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR = {
Monetary = 'monetary',
DocumentLink = 'documentlink',
Select = 'select',
+ LongText = 'longtext',
}
export const DATA_TYPE_LABELS = [
id: CustomFieldDataType.Select,
name: $localize`Select`,
},
+ {
+ id: CustomFieldDataType.LongText,
+ name: $localize`Long Text`,
+ },
]
export interface CustomField extends ObjectWithId {
| qs.filter(custom_fields__value_monetary__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_long_text__icontains=value)
)
else:
return qs
CustomField.FieldDataType.MONETARY: ("basic", "string", "arithmetic"),
CustomField.FieldDataType.DOCUMENTLINK: ("basic", "containment"),
CustomField.FieldDataType.SELECT: ("basic",),
+ CustomField.FieldDataType.LONG_TEXT: ("basic", "string"),
}
DATE_COMPONENTS = [
annotation = None
match field.data_type:
- case CustomField.FieldDataType.STRING:
+ case (
+ CustomField.FieldDataType.STRING
+ | CustomField.FieldDataType.LONG_TEXT
+ ):
annotation = Subquery(
CustomFieldInstance.objects.filter(
document_id=OuterRef("id"),
--- /dev/null
+# 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",
+ ),
+ ),
+ ]
MONETARY = ("monetary", _("Monetary"))
DOCUMENTLINK = ("documentlink", _("Document Link"))
SELECT = ("select", _("Select"))
+ LONG_TEXT = ("longtext", _("Long Text"))
created = models.DateTimeField(
_("created"),
CustomField.FieldDataType.MONETARY: "value_monetary",
CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
CustomField.FieldDataType.SELECT: "value_select",
+ CustomField.FieldDataType.LONG_TEXT: "value_long_text",
}
created = models.DateTimeField(
value_select = models.CharField(null=True, max_length=16)
+ value_long_text = models.TextField(null=True)
+
class Meta:
ordering = ("created",)
verbose_name = _("custom field instance")
CustomField.FieldDataType.MONETARY,
CustomField.FieldDataType.STRING,
CustomField.FieldDataType.URL,
+ CustomField.FieldDataType.LONG_TEXT,
}:
value = pathvalidate.sanitize_filename(
field_instance.value,