mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 19:17:13 -05:00 
			
		
		
		
	Enhancement: long text custom field (#10846)
--------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									d230514dd3
								
							
						
					
					
						commit
						feb5d534b5
					
				@ -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>
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,11 @@
 | 
			
		||||
          [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>
 | 
			
		||||
 | 
			
		||||
@ -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> {
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
@ -216,6 +216,14 @@
 | 
			
		||||
                      (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>
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,10 @@
 | 
			
		||||
                [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>
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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 = {
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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"),
 | 
			
		||||
 | 
			
		||||
@ -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"))
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user