]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: long text custom field (#10846)
authorjojo2357 <66704796+jojo2357@users.noreply.github.com>
Sun, 14 Sep 2025 03:19:00 +0000 (21:19 -0600)
committerGitHub <noreply@github.com>
Sun, 14 Sep 2025 03:19:00 +0000 (03:19 +0000)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
15 files changed:
src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html
src-ui/src/app/components/common/custom-field-display/custom-field-display.component.ts
src-ui/src/app/components/common/input/custom-fields-values/custom-fields-values.component.html
src-ui/src/app/components/common/input/custom-fields-values/custom-fields-values.component.ts
src-ui/src/app/components/common/input/textarea/textarea.component.ts
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/components/document-detail/document-detail.component.ts
src-ui/src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html
src-ui/src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.ts
src-ui/src/app/data/custom-field-query.ts
src-ui/src/app/data/custom-field.ts
src/documents/filters.py
src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py [new file with mode: 0644]
src/documents/models.py
src/documents/templating/filepath.py

index bfac6ef23a095f229aba9cb85e5797bd1fb246f1..388c314d35fcf85bc653626d81313400e7ffd02f 100644 (file)
@@ -35,6 +35,9 @@
             @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>
             }
index 44ec0bdaa22546e1bc40d8386729c0ed2feb9c72..7885f5c28ff1dfca0438c4efcb05ba225b17c198 100644 (file)
@@ -1,5 +1,5 @@
-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'
@@ -14,7 +14,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
   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
index f0886b1c2d240343c1c2eee1905ac6d3eb363478..0c20a620a3241ce6e9bc35086b0844c0de2e22ce 100644 (file)
           [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>
index e318a35a79ea91ff281ce10ea181de496ee4713f..e32ffaf76f978bafd5ec0833f97a627db13abde8 100644 (file)
@@ -24,6 +24,7 @@ import { MonetaryComponent } from '../monetary/monetary.component'
 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({
@@ -51,6 +52,7 @@ import { UrlComponent } from '../url/url.component'
     ReactiveFormsModule,
     RouterModule,
     NgxBootstrapIconsModule,
+    TextAreaComponent,
   ],
 })
 export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> {
index 6f486a46e7ba2f7db96730e23e62d77ed975e2a0..733c3f18aa0211056ded4c2bc5feec05b85a9d58 100644 (file)
@@ -4,6 +4,7 @@ import {
   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'
 
@@ -18,7 +19,12 @@ 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()
index 42b307e58a1572b797c7824246d9553524273cfb..d8cd2d756ac927a6902bdfb8e418ca8fe330dc82 100644 (file)
                       (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>
               }
index 08c9a637cc1604a94928bf8fb6bfc240321d06f9..bea5577dc609ca20b01a78f3719967fd7521b176 100644 (file)
@@ -98,6 +98,7 @@ import { PermissionsFormComponent } from '../common/input/permissions/permission
 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 {
@@ -173,6 +174,7 @@ export enum ZoomSetting {
     NgbDropdownModule,
     NgxBootstrapIconsModule,
     PdfViewerModule,
+    TextAreaComponent,
   ],
 })
 export class DocumentDetailComponent
index 637832a01d7f699ffe6ac31afe1ee37173e90b4e..78bce22a23b5ee93934a3a2138bb809f585d6dea 100644 (file)
                 [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>
index ea51045399abc5c215629fa8bf881352e50142c1..8452e5388432a362ce55ab7123b41b295ae765ff 100644 (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 { 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',
@@ -35,6 +36,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
     FormsModule,
     ReactiveFormsModule,
     NgxBootstrapIconsModule,
+    TextAreaComponent,
   ],
 })
 export class CustomFieldsBulkEditDialogComponent {
index 9f69fac76d798aea53f89ca213453708751f4c34..a0cc77131816e2357698385632cb947b9e16e81c 100644 (file)
@@ -114,6 +114,10 @@ export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = {
     CustomFieldQueryOperatorGroups.Exact,
     CustomFieldQueryOperatorGroups.Subset,
   ],
+  [CustomFieldDataType.LongText]: [
+    CustomFieldQueryOperatorGroups.Basic,
+    CustomFieldQueryOperatorGroups.String,
+  ],
 }
 
 export const CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR = {
index c5130a756ff338048184557973c41d1c6a332885..a3e90a8e7b96d4c9d4c47f3bc393859608eb3238 100644 (file)
@@ -10,6 +10,7 @@ export enum CustomFieldDataType {
   Monetary = 'monetary',
   DocumentLink = 'documentlink',
   Select = 'select',
+  LongText = 'longtext',
 }
 
 export const DATA_TYPE_LABELS = [
@@ -49,6 +50,10 @@ export const DATA_TYPE_LABELS = [
     id: CustomFieldDataType.Select,
     name: $localize`Select`,
   },
+  {
+    id: CustomFieldDataType.LongText,
+    name: $localize`Long Text`,
+  },
 ]
 
 export interface CustomField extends ObjectWithId {
index 6ac434e667a3c6ebd9ec2a91345a88fb614ae354..87274f9fad3deab6511d9e344cf115ea8ba2bd65 100644 (file)
@@ -230,6 +230,7 @@ class CustomFieldsFilter(Filter):
                 | 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
@@ -314,6 +315,7 @@ class CustomFieldQueryParser:
         CustomField.FieldDataType.MONETARY: ("basic", "string", "arithmetic"),
         CustomField.FieldDataType.DOCUMENTLINK: ("basic", "containment"),
         CustomField.FieldDataType.SELECT: ("basic",),
+        CustomField.FieldDataType.LONG_TEXT: ("basic", "string"),
     }
 
     DATE_COMPONENTS = [
@@ -845,7 +847,10 @@ class DocumentsOrderingFilter(OrderingFilter):
 
             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"),
diff --git a/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py b/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py
new file mode 100644 (file)
index 0000000..69c77d2
--- /dev/null
@@ -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",
+            ),
+        ),
+    ]
index 0404065cb0f4bd82758720d711ce941a28374b0c..700320d381b2fdf33f4c9693849e3d13b30bcb4a 100644 (file)
@@ -759,6 +759,7 @@ class CustomField(models.Model):
         MONETARY = ("monetary", _("Monetary"))
         DOCUMENTLINK = ("documentlink", _("Document Link"))
         SELECT = ("select", _("Select"))
+        LONG_TEXT = ("longtext", _("Long Text"))
 
     created = models.DateTimeField(
         _("created"),
@@ -816,6 +817,7 @@ class CustomFieldInstance(SoftDeleteModel):
         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(
@@ -883,6 +885,8 @@ class CustomFieldInstance(SoftDeleteModel):
 
     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")
index a3354109528d0ca5ecbf57b023d509543689f045..00de8de2c19c87b9a32976a18aac009f9038ed74 100644 (file)
@@ -202,6 +202,7 @@ def get_custom_fields_context(
             CustomField.FieldDataType.MONETARY,
             CustomField.FieldDataType.STRING,
             CustomField.FieldDataType.URL,
+            CustomField.FieldDataType.LONG_TEXT,
         }:
             value = pathvalidate.sanitize_filename(
                 field_instance.value,