mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-16 21:55:37 -05:00
Enhancement: long text custom field (#10846)
--------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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> {
|
||||||
|
@@ -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()
|
||||||
|
@@ -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>
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 = {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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"),
|
||||||
|
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -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")
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user