mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Make separate properties, assign_custom_fields_values
This commit is contained in:
		| @@ -188,7 +188,8 @@ | |||||||
|             <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> |             <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> | ||||||
|             <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select> |             <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select> | ||||||
|             <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select> |             <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select> | ||||||
|             <pngx-input-custom-fields-select i18n-title title="Assign custom fields" formControlName="assign_custom_fields_w_values"></pngx-input-custom-fields-select> |             <pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select> | ||||||
|  |             <pngx-input-custom-fields-values i18n-title title="Assign custom field values" [selectedFields]="formGroup.get('assign_custom_fields').value" formControlName="assign_custom_fields_values"></pngx-input-custom-fields-values> | ||||||
|           </div> |           </div> | ||||||
|           <div class="col"> |           <div class="col"> | ||||||
|             <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select> |             <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select> | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | |||||||
| import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | ||||||
| import { first } from 'rxjs' | import { first } from 'rxjs' | ||||||
| import { Correspondent } from 'src/app/data/correspondent' | import { Correspondent } from 'src/app/data/correspondent' | ||||||
| import { CustomField } from 'src/app/data/custom-field' | import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' | ||||||
| import { DocumentType } from 'src/app/data/document-type' | import { DocumentType } from 'src/app/data/document-type' | ||||||
| import { MailRule } from 'src/app/data/mail-rule' | import { MailRule } from 'src/app/data/mail-rule' | ||||||
| import { | import { | ||||||
| @@ -38,6 +38,7 @@ import { | |||||||
|   WorkflowTriggerType, |   WorkflowTriggerType, | ||||||
| } from 'src/app/data/workflow-trigger' | } from 'src/app/data/workflow-trigger' | ||||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||||
|  | import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' | ||||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||||
| import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||||
| @@ -46,7 +47,7 @@ import { WorkflowService } from 'src/app/services/rest/workflow.service' | |||||||
| import { SettingsService } from 'src/app/services/settings.service' | import { SettingsService } from 'src/app/services/settings.service' | ||||||
| import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component' | import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component' | ||||||
| import { CheckComponent } from '../../input/check/check.component' | import { CheckComponent } from '../../input/check/check.component' | ||||||
| import { CustomFieldsSelectComponent } from '../../input/custom-fields-select/custom-fields-select.component' | import { CustomFieldsValuesComponent } from '../../input/custom-fields-values/custom-fields-values.component' | ||||||
| import { EntriesComponent } from '../../input/entries/entries.component' | import { EntriesComponent } from '../../input/entries/entries.component' | ||||||
| import { NumberComponent } from '../../input/number/number.component' | import { NumberComponent } from '../../input/number/number.component' | ||||||
| import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component' | import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component' | ||||||
| @@ -148,10 +149,10 @@ const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter( | |||||||
|     SwitchComponent, |     SwitchComponent, | ||||||
|     NumberComponent, |     NumberComponent, | ||||||
|     TextComponent, |     TextComponent, | ||||||
|     CustomFieldsSelectComponent, |  | ||||||
|     SelectComponent, |     SelectComponent, | ||||||
|     TextAreaComponent, |     TextAreaComponent, | ||||||
|     TagsComponent, |     TagsComponent, | ||||||
|  |     CustomFieldsValuesComponent, | ||||||
|     PermissionsGroupComponent, |     PermissionsGroupComponent, | ||||||
|     PermissionsUserComponent, |     PermissionsUserComponent, | ||||||
|     ConfirmButtonComponent, |     ConfirmButtonComponent, | ||||||
| @@ -169,12 +170,14 @@ export class WorkflowEditDialogComponent | |||||||
| { | { | ||||||
|   public WorkflowTriggerType = WorkflowTriggerType |   public WorkflowTriggerType = WorkflowTriggerType | ||||||
|   public WorkflowActionType = WorkflowActionType |   public WorkflowActionType = WorkflowActionType | ||||||
|  |   public CustomFieldDataType = CustomFieldDataType | ||||||
|  |  | ||||||
|   templates: Workflow[] |   templates: Workflow[] | ||||||
|   correspondents: Correspondent[] |   correspondents: Correspondent[] | ||||||
|   documentTypes: DocumentType[] |   documentTypes: DocumentType[] | ||||||
|   storagePaths: StoragePath[] |   storagePaths: StoragePath[] | ||||||
|   mailRules: MailRule[] |   mailRules: MailRule[] | ||||||
|  |   customFields: CustomField[] | ||||||
|   dateCustomFields: CustomField[] |   dateCustomFields: CustomField[] | ||||||
|  |  | ||||||
|   expandedItem: number = null |   expandedItem: number = null | ||||||
| @@ -189,7 +192,8 @@ export class WorkflowEditDialogComponent | |||||||
|     storagePathService: StoragePathService, |     storagePathService: StoragePathService, | ||||||
|     mailRuleService: MailRuleService, |     mailRuleService: MailRuleService, | ||||||
|     userService: UserService, |     userService: UserService, | ||||||
|     settingsService: SettingsService |     settingsService: SettingsService, | ||||||
|  |     customFieldsService: CustomFieldsService | ||||||
|   ) { |   ) { | ||||||
|     super(service, activeModal, userService, settingsService) |     super(service, activeModal, userService, settingsService) | ||||||
|  |  | ||||||
| @@ -212,6 +216,16 @@ export class WorkflowEditDialogComponent | |||||||
|       .listAll() |       .listAll() | ||||||
|       .pipe(first()) |       .pipe(first()) | ||||||
|       .subscribe((result) => (this.mailRules = result.results)) |       .subscribe((result) => (this.mailRules = result.results)) | ||||||
|  |  | ||||||
|  |     customFieldsService | ||||||
|  |       .listAll() | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((result) => { | ||||||
|  |         this.customFields = result.results | ||||||
|  |         this.dateCustomFields = this.customFields?.filter( | ||||||
|  |           (f) => f.data_type === CustomFieldDataType.Date | ||||||
|  |         ) | ||||||
|  |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
| @@ -252,8 +266,6 @@ export class WorkflowEditDialogComponent | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private checkRemovalActionFields(formWorkflow: Workflow) { |   private checkRemovalActionFields(formWorkflow: Workflow) { | ||||||
|     console.log('checkRemovalActionFields', formWorkflow) |  | ||||||
|  |  | ||||||
|     formWorkflow.actions |     formWorkflow.actions | ||||||
|       .filter((action) => action.type === WorkflowActionType.Removal) |       .filter((action) => action.type === WorkflowActionType.Removal) | ||||||
|       .forEach((action, i) => { |       .forEach((action, i) => { | ||||||
| @@ -429,8 +441,9 @@ export class WorkflowEditDialogComponent | |||||||
|         assign_view_groups: new FormControl(action.assign_view_groups), |         assign_view_groups: new FormControl(action.assign_view_groups), | ||||||
|         assign_change_users: new FormControl(action.assign_change_users), |         assign_change_users: new FormControl(action.assign_change_users), | ||||||
|         assign_change_groups: new FormControl(action.assign_change_groups), |         assign_change_groups: new FormControl(action.assign_change_groups), | ||||||
|         assign_custom_fields_w_values: new FormControl( |         assign_custom_fields: new FormControl(action.assign_custom_fields), | ||||||
|           action.assign_custom_fields_w_values |         assign_custom_fields_values: new FormControl( | ||||||
|  |           action.assign_custom_fields_values | ||||||
|         ), |         ), | ||||||
|         remove_tags: new FormControl(action.remove_tags), |         remove_tags: new FormControl(action.remove_tags), | ||||||
|         remove_all_tags: new FormControl(action.remove_all_tags), |         remove_all_tags: new FormControl(action.remove_all_tags), | ||||||
| @@ -557,7 +570,8 @@ export class WorkflowEditDialogComponent | |||||||
|       assign_view_groups: [], |       assign_view_groups: [], | ||||||
|       assign_change_users: [], |       assign_change_users: [], | ||||||
|       assign_change_groups: [], |       assign_change_groups: [], | ||||||
|       assign_custom_fields_w_values: [], |       assign_custom_fields: [], | ||||||
|  |       assign_custom_fields_values: {}, | ||||||
|       remove_tags: [], |       remove_tags: [], | ||||||
|       remove_all_tags: false, |       remove_all_tags: false, | ||||||
|       remove_document_types: [], |       remove_document_types: [], | ||||||
| @@ -636,4 +650,8 @@ export class WorkflowEditDialogComponent | |||||||
|       }) |       }) | ||||||
|     super.save() |     super.save() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public getCustomField(id: number): CustomField { | ||||||
|  |     return this.customFields.find((field) => field.id === id) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,113 +0,0 @@ | |||||||
| <div class="mb-3 paperless-input-select" [class.disabled]="disabled"> |  | ||||||
|   <div class="row"> |  | ||||||
|     <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> |  | ||||||
|       @if (title) { |  | ||||||
|         <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> |  | ||||||
|       } |  | ||||||
|       </div> |  | ||||||
|       <div [class.col-md-9]="horizontal"> |  | ||||||
|         <div [class.is-invalid]="error"> |  | ||||||
|           <ng-select name="inputId" [(ngModel)]="selectedFields" |  | ||||||
|             [disabled]="disabled" |  | ||||||
|             [clearable]="true" |  | ||||||
|             [items]="fields" |  | ||||||
|             [addTag]="false" |  | ||||||
|             notFoundText="No fields found" |  | ||||||
|             i18n-notFoundText |  | ||||||
|             [multiple]="true" |  | ||||||
|             bindLabel="name" |  | ||||||
|             bindValue="id" |  | ||||||
|             (change)="onChange(value)"> |  | ||||||
|             <ng-template ng-option-tmp let-item="item"> |  | ||||||
|                 <span [title]="item.name">{{item.name}}</span> |  | ||||||
|             </ng-template> |  | ||||||
|           </ng-select> |  | ||||||
|           @if (selectedFields.length) { |  | ||||||
|             <div class="list-group mt-3 selected-fields"> |  | ||||||
|               @for (fieldId of selectedFields; track fieldId) { |  | ||||||
|                 <div class="list-group-item |  | ||||||
|                   d-flex |  | ||||||
|                   justify-content-between |  | ||||||
|                   align-items-center"> |  | ||||||
|                   @switch (getCustomField(fieldId)?.data_type) { |  | ||||||
|                     @case (CustomFieldDataType.String) { |  | ||||||
|                       <pngx-input-text [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true"></pngx-input-text> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Date) { |  | ||||||
|                       <pngx-input-date [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true"></pngx-input-date> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Integer) { |  | ||||||
|                       <pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true" |  | ||||||
|                       [showAdd]="false"></pngx-input-number> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Float) { |  | ||||||
|                       <pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true" |  | ||||||
|                       [showAdd]="false" |  | ||||||
|                       [step]=".1"></pngx-input-number> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Monetary) { |  | ||||||
|                       <pngx-input-monetary [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [defaultCurrency]="getCustomField(fieldId)?.extra_data?.default_currency" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true"></pngx-input-monetary> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Boolean) { |  | ||||||
|                       <pngx-input-check [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true"></pngx-input-check> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Url) { |  | ||||||
|                       <pngx-input-url [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true"></pngx-input-url> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.DocumentLink) { |  | ||||||
|                       <pngx-input-document-link [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [horizontal]="true"></pngx-input-document-link> |  | ||||||
|                     } |  | ||||||
|                     @case (CustomFieldDataType.Select) { |  | ||||||
|                       <pngx-input-select [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" |  | ||||||
|                       [title]="getCustomField(fieldId)?.name" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       [items]="getCustomField(fieldId)?.extra_data.select_options" |  | ||||||
|                       class="flex-grow-1" |  | ||||||
|                       bindLabel="label" |  | ||||||
|                       [allowNull]="true" |  | ||||||
|                       [horizontal]="true"></pngx-input-select> |  | ||||||
|                     } |  | ||||||
|                   } |  | ||||||
|                   <button type="button" class="btn btn-link text-danger" (click)="removeField(fieldId)"> |  | ||||||
|                     <i-bs name="trash"></i-bs> |  | ||||||
|                   </button> |  | ||||||
|                 </div> |  | ||||||
|               } |  | ||||||
|             </div> |  | ||||||
|           } |  | ||||||
|         </div> |  | ||||||
|         <div class="invalid-feedback"> |  | ||||||
|           {{error}} |  | ||||||
|         </div> |  | ||||||
|         @if (hint) { |  | ||||||
|           <small class="form-text text-muted">{{hint}}</small> |  | ||||||
|         } |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| // styles for ng-select child are in styles.scss |  | ||||||
| .paperless-input-select.disabled { |  | ||||||
|     .input-group, |  | ||||||
|     div > div { |  | ||||||
|         cursor: not-allowed; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     ::ng-deep ng-select { |  | ||||||
|         pointer-events: none; |  | ||||||
|  |  | ||||||
|         .ng-select-container { |  | ||||||
|             background-color: var(--pngx-bg-disabled) !important; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ::ng-deep .private .ng-value-container { |  | ||||||
|     font-style: italic; |  | ||||||
|     opacity: .75; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ::ng-deep .is-invalid ng-select .ng-select-container input { |  | ||||||
|     // replicate bootstrap |  | ||||||
|     padding-right: calc(1.5em + 0.75rem) !important; |  | ||||||
|     background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important; |  | ||||||
|     background-repeat: no-repeat !important; |  | ||||||
|     background-position: right calc(0.375em + 0.1875rem) center !important; |  | ||||||
|     background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .input-group .ng-select-taggable:first-child:nth-last-child(2) { |  | ||||||
|     max-width: calc(100% - 45px); // fudge factor for (1x) ng-select button width |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .input-group .ng-select-taggable:first-child:nth-last-child(3) { |  | ||||||
|     max-width: calc(100% - 90px); // fudge factor for (2x) ng-select button width |  | ||||||
| } |  | ||||||
|  |  | ||||||
| :host ::ng-deep .list-group-item .mb-3 { |  | ||||||
|     margin-bottom: 0 !important; |  | ||||||
| } |  | ||||||
| @@ -1,135 +0,0 @@ | |||||||
| import { |  | ||||||
|   ComponentFixture, |  | ||||||
|   TestBed, |  | ||||||
|   fakeAsync, |  | ||||||
|   tick, |  | ||||||
| } from '@angular/core/testing' |  | ||||||
| import { |  | ||||||
|   FormsModule, |  | ||||||
|   NG_VALUE_ACCESSOR, |  | ||||||
|   ReactiveFormsModule, |  | ||||||
| } from '@angular/forms' |  | ||||||
| import { RouterTestingModule } from '@angular/router/testing' |  | ||||||
| import { NgSelectModule } from '@ng-select/ng-select' |  | ||||||
| import { |  | ||||||
|   DEFAULT_MATCHING_ALGORITHM, |  | ||||||
|   MATCH_ALL, |  | ||||||
| } from 'src/app/data/matching-model' |  | ||||||
| import { Tag } from 'src/app/data/tag' |  | ||||||
| import { SelectComponent } from './select.component' |  | ||||||
|  |  | ||||||
| const items: Tag[] = [ |  | ||||||
|   { |  | ||||||
|     id: 1, |  | ||||||
|     name: 'Tag1', |  | ||||||
|     is_inbox_tag: false, |  | ||||||
|     matching_algorithm: DEFAULT_MATCHING_ALGORITHM, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 2, |  | ||||||
|     name: 'Tag2', |  | ||||||
|     is_inbox_tag: true, |  | ||||||
|     matching_algorithm: MATCH_ALL, |  | ||||||
|     match: 'str', |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 10, |  | ||||||
|     name: 'Tag10', |  | ||||||
|     is_inbox_tag: false, |  | ||||||
|     matching_algorithm: DEFAULT_MATCHING_ALGORITHM, |  | ||||||
|   }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| describe('SelectComponent', () => { |  | ||||||
|   let component: SelectComponent |  | ||||||
|   let fixture: ComponentFixture<SelectComponent> |  | ||||||
|   let input: HTMLInputElement |  | ||||||
|  |  | ||||||
|   beforeEach(async () => { |  | ||||||
|     TestBed.configureTestingModule({ |  | ||||||
|       providers: [], |  | ||||||
|       imports: [ |  | ||||||
|         FormsModule, |  | ||||||
|         ReactiveFormsModule, |  | ||||||
|         NgSelectModule, |  | ||||||
|         RouterTestingModule, |  | ||||||
|         SelectComponent, |  | ||||||
|       ], |  | ||||||
|     }).compileComponents() |  | ||||||
|  |  | ||||||
|     fixture = TestBed.createComponent(SelectComponent) |  | ||||||
|     fixture.debugElement.injector.get(NG_VALUE_ACCESSOR) |  | ||||||
|     component = fixture.componentInstance |  | ||||||
|     fixture.detectChanges() |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   it('should support private items', () => { |  | ||||||
|     component.value = 3 |  | ||||||
|     component.items = items |  | ||||||
|     expect(component.items).toContainEqual({ |  | ||||||
|       id: 3, |  | ||||||
|       name: 'Private', |  | ||||||
|       private: true, |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     component.checkForPrivateItems([4, 5]) |  | ||||||
|     expect(component.items).toContainEqual({ |  | ||||||
|       id: 4, |  | ||||||
|       name: 'Private', |  | ||||||
|       private: true, |  | ||||||
|     }) |  | ||||||
|     expect(component.items).toContainEqual({ |  | ||||||
|       id: 5, |  | ||||||
|       name: 'Private', |  | ||||||
|       private: true, |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   it('should support suggestions', () => { |  | ||||||
|     expect(component.value).toBeUndefined() |  | ||||||
|     component.items = items |  | ||||||
|     component.suggestions = [1, 2] |  | ||||||
|     fixture.detectChanges() |  | ||||||
|     const suggestionAnchor: HTMLAnchorElement = |  | ||||||
|       fixture.nativeElement.querySelector('a') |  | ||||||
|     suggestionAnchor.click() |  | ||||||
|     expect(component.value).toEqual(1) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   it('should support create new and emit the value', () => { |  | ||||||
|     expect(component.allowCreateNew).toBeFalsy() |  | ||||||
|     component.items = items |  | ||||||
|     let createNewVal |  | ||||||
|     component.createNew.subscribe((v) => (createNewVal = v)) |  | ||||||
|     expect(component.allowCreateNew).toBeTruthy() |  | ||||||
|     component.onSearch({ term: 'foo' }) |  | ||||||
|     component.addItem(undefined) |  | ||||||
|     expect(createNewVal).toEqual('foo') |  | ||||||
|     component.addItem('bar') |  | ||||||
|     expect(createNewVal).toEqual('bar') |  | ||||||
|     component.onSearch({ term: 'baz' }) |  | ||||||
|     component.clickNew() |  | ||||||
|     expect(createNewVal).toEqual('baz') |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   it('should clear search term on blur after delay', fakeAsync(() => { |  | ||||||
|     const clearSpy = jest.spyOn(component, 'clearLastSearchTerm') |  | ||||||
|     component.onBlur() |  | ||||||
|     tick(3000) |  | ||||||
|     expect(clearSpy).toHaveBeenCalled() |  | ||||||
|   })) |  | ||||||
|  |  | ||||||
|   it('should emit filtered documents', () => { |  | ||||||
|     component.value = 10 |  | ||||||
|     component.items = items |  | ||||||
|     const emitSpy = jest.spyOn(component.filterDocuments, 'emit') |  | ||||||
|     component.onFilterDocuments() |  | ||||||
|     expect(emitSpy).toHaveBeenCalledWith([items[2]]) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   it('should return the correct filter button title', () => { |  | ||||||
|     component.title = 'Tag' |  | ||||||
|     const expectedTitle = `Filter documents with this ${component.title}` |  | ||||||
|     expect(component.filterButtonTitle).toEqual(expectedTitle) |  | ||||||
|   }) |  | ||||||
| }) |  | ||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | <div class="list-group mt-3 selected-fields"> | ||||||
|  |   @for (fieldId of selectedFields; track fieldId) { | ||||||
|  |     <div class="list-group-item | ||||||
|  |       d-flex | ||||||
|  |       justify-content-between | ||||||
|  |       align-items-center"> | ||||||
|  |       @switch (getCustomField(fieldId)?.data_type) { | ||||||
|  |         @case (CustomFieldDataType.String) { | ||||||
|  |           <pngx-input-text [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true"></pngx-input-text> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Date) { | ||||||
|  |           <pngx-input-date [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true"></pngx-input-date> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Integer) { | ||||||
|  |           <pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true" | ||||||
|  |           [showAdd]="false"></pngx-input-number> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Float) { | ||||||
|  |           <pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true" | ||||||
|  |           [showAdd]="false" | ||||||
|  |           [step]=".1"></pngx-input-number> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Monetary) { | ||||||
|  |           <pngx-input-monetary [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [defaultCurrency]="getCustomField(fieldId)?.extra_data?.default_currency" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true"></pngx-input-monetary> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Boolean) { | ||||||
|  |           <pngx-input-check [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true"></pngx-input-check> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Url) { | ||||||
|  |           <pngx-input-url [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true"></pngx-input-url> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.DocumentLink) { | ||||||
|  |           <pngx-input-document-link [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [horizontal]="true"></pngx-input-document-link> | ||||||
|  |         } | ||||||
|  |         @case (CustomFieldDataType.Select) { | ||||||
|  |           <pngx-input-select [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)" | ||||||
|  |           [title]="getCustomField(fieldId)?.name" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           [items]="getCustomField(fieldId)?.extra_data.select_options" | ||||||
|  |           class="flex-grow-1" | ||||||
|  |           bindLabel="label" | ||||||
|  |           [allowNull]="true" | ||||||
|  |           [horizontal]="true"></pngx-input-select> | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       <button type="button" class="btn btn-link text-danger" (click)="removeField(fieldId)"> | ||||||
|  |         <i-bs name="trash"></i-bs> | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |   } | ||||||
|  | </div> | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | :host ::ng-deep .list-group-item .mb-3 { | ||||||
|  |   margin-bottom: 0 !important; | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Component, forwardRef } from '@angular/core' | import { Component, forwardRef, Input } from '@angular/core' | ||||||
| import { | import { | ||||||
|   FormsModule, |   FormsModule, | ||||||
|   NG_VALUE_ACCESSOR, |   NG_VALUE_ACCESSOR, | ||||||
| @@ -23,13 +23,13 @@ import { UrlComponent } from '../url/url.component' | |||||||
|   providers: [ |   providers: [ | ||||||
|     { |     { | ||||||
|       provide: NG_VALUE_ACCESSOR, |       provide: NG_VALUE_ACCESSOR, | ||||||
|       useExisting: forwardRef(() => CustomFieldsSelectComponent), |       useExisting: forwardRef(() => CustomFieldsValuesComponent), | ||||||
|       multi: true, |       multi: true, | ||||||
|     }, |     }, | ||||||
|   ], |   ], | ||||||
|   selector: 'pngx-input-custom-fields-select', |   selector: 'pngx-input-custom-fields-values', | ||||||
|   templateUrl: './custom-fields-select.component.html', |   templateUrl: './custom-fields-values.component.html', | ||||||
|   styleUrls: ['./custom-fields-select.component.scss'], |   styleUrl: './custom-fields-values.component.scss', | ||||||
|   imports: [ |   imports: [ | ||||||
|     TextComponent, |     TextComponent, | ||||||
|     DateComponent, |     DateComponent, | ||||||
| @@ -46,7 +46,7 @@ import { UrlComponent } from '../url/url.component' | |||||||
|     NgxBootstrapIconsModule, |     NgxBootstrapIconsModule, | ||||||
|   ], |   ], | ||||||
| }) | }) | ||||||
| export class CustomFieldsSelectComponent extends AbstractInputComponent<Object> { | export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> { | ||||||
|   public CustomFieldDataType = CustomFieldDataType |   public CustomFieldDataType = CustomFieldDataType | ||||||
| 
 | 
 | ||||||
|   constructor(customFieldsService: CustomFieldsService) { |   constructor(customFieldsService: CustomFieldsService) { | ||||||
| @@ -56,9 +56,11 @@ export class CustomFieldsSelectComponent extends AbstractInputComponent<Object> | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   fields: CustomField[] |   private fields: CustomField[] | ||||||
| 
 | 
 | ||||||
|   _selectedFields: number[] |   private _selectedFields: number[] | ||||||
|  | 
 | ||||||
|  |   @Input() | ||||||
|   set selectedFields(newFields: number[]) { |   set selectedFields(newFields: number[]) { | ||||||
|     this._selectedFields = newFields |     this._selectedFields = newFields | ||||||
|     // map the selected fields to an object with field_id as key and value as value
 |     // map the selected fields to an object with field_id as key and value as value
 | ||||||
| @@ -68,20 +70,11 @@ export class CustomFieldsSelectComponent extends AbstractInputComponent<Object> | |||||||
|     }, {}) |     }, {}) | ||||||
|     this.onChange(this.value) |     this.onChange(this.value) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   get selectedFields(): number[] { |   get selectedFields(): number[] { | ||||||
|     return this._selectedFields |     return this._selectedFields | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   writeValue(newValue: Object): void { |  | ||||||
|     // value will be a json object with field_id as key and value as value
 |  | ||||||
|     this._selectedFields = newValue |  | ||||||
|       ? this.fields |  | ||||||
|           .filter((field) => field.id in newValue) |  | ||||||
|           .map((field) => field.id) |  | ||||||
|       : [] |  | ||||||
|     super.writeValue(newValue) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   public getCustomField(id: number): CustomField { |   public getCustomField(id: number): CustomField { | ||||||
|     return this.fields.find((field) => field.id === id) |     return this.fields.find((field) => field.id === id) | ||||||
|   } |   } | ||||||
| @@ -56,7 +56,9 @@ export interface WorkflowAction extends ObjectWithId { | |||||||
|  |  | ||||||
|   assign_change_groups?: number[] // [Group.id] |   assign_change_groups?: number[] // [Group.id] | ||||||
|  |  | ||||||
|   assign_custom_fields_w_values?: number[] // { [CustomField.id]: value } |   assign_custom_fields?: number[] // [CustomField.id] | ||||||
|  |  | ||||||
|  |   assign_custom_fields_values?: object | ||||||
|  |  | ||||||
|   remove_tags?: number[] // Tag.id |   remove_tags?: number[] // Tag.id | ||||||
|  |  | ||||||
|   | |||||||
| @@ -114,11 +114,8 @@ class DocumentMetadataOverrides: | |||||||
|             ).values_list("id", flat=True), |             ).values_list("id", flat=True), | ||||||
|         ) |         ) | ||||||
|         overrides.custom_fields = { |         overrides.custom_fields = { | ||||||
|             custom_field.id: value |             custom_field.id: custom_field.value | ||||||
|             for custom_field, value in doc.custom_fields.all().values_list( |             for custom_field in doc.custom_fields.all() | ||||||
|                 "id", |  | ||||||
|                 "value", |  | ||||||
|             ) |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         groups_with_perms = get_groups_with_perms( |         groups_with_perms = get_groups_with_perms( | ||||||
|   | |||||||
| @@ -1,42 +0,0 @@ | |||||||
| # Generated by Django 5.1.6 on 2025-03-01 04:49 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
| from django.db import models |  | ||||||
|  |  | ||||||
| import documents.models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def convert_assign_custom_fields(apps, schema_editor): |  | ||||||
|     # Convert the old assign_custom_fields ManyToManyField to the new assign_custom_fields_w_values JSONField |  | ||||||
|     WorkflowAction = apps.get_model("documents", "WorkflowAction") |  | ||||||
|     for workflow_action in WorkflowAction.objects.all(): |  | ||||||
|         if workflow_action.assign_custom_fields.exists(): |  | ||||||
|             workflow_action.assign_custom_fields_w_values = { |  | ||||||
|                 custom_field.id: None |  | ||||||
|                 for custom_field in workflow_action.assign_custom_fields.all() |  | ||||||
|             } |  | ||||||
|             workflow_action.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="workflowaction", |  | ||||||
|             name="assign_custom_fields_w_values", |  | ||||||
|             field=models.JSONField( |  | ||||||
|                 blank=True, |  | ||||||
|                 help_text="assign these custom fields, with optional values", |  | ||||||
|                 null=True, |  | ||||||
|                 verbose_name=documents.models.CustomField, |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython(convert_assign_custom_fields, migrations.RunPython.noop), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name="workflowaction", |  | ||||||
|             name="assign_custom_fields", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | # Generated by Django 5.1.6 on 2025-03-01 18:10 | ||||||
|  |  | ||||||
|  | from django.db import migrations | ||||||
|  | from django.db import models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |     dependencies = [ | ||||||
|  |         ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="workflowaction", | ||||||
|  |             name="assign_custom_fields_values", | ||||||
|  |             field=models.JSONField( | ||||||
|  |                 blank=True, | ||||||
|  |                 help_text="Optional values to assign to the custom fields.", | ||||||
|  |                 null=True, | ||||||
|  |                 verbose_name="custom field values", | ||||||
|  |                 default={}, | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -1384,13 +1384,21 @@ class WorkflowAction(models.Model): | |||||||
|         verbose_name=_("grant change permissions to these groups"), |         verbose_name=_("grant change permissions to these groups"), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     assign_custom_fields_w_values = models.JSONField( |     assign_custom_fields = models.ManyToManyField( | ||||||
|         CustomField, |         CustomField, | ||||||
|         blank=True, |         blank=True, | ||||||
|  |         related_name="+", | ||||||
|  |         verbose_name=_("assign these custom fields"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     assign_custom_fields_values = models.JSONField( | ||||||
|  |         _("custom field values"), | ||||||
|         null=True, |         null=True, | ||||||
|  |         blank=True, | ||||||
|         help_text=_( |         help_text=_( | ||||||
|             "assign these custom fields, with optional values", |             "Optional values to assign to the custom fields.", | ||||||
|         ), |         ), | ||||||
|  |         default={}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     remove_tags = models.ManyToManyField( |     remove_tags = models.ManyToManyField( | ||||||
|   | |||||||
| @@ -2017,7 +2017,8 @@ class WorkflowActionSerializer(serializers.ModelSerializer): | |||||||
|             "assign_view_groups", |             "assign_view_groups", | ||||||
|             "assign_change_users", |             "assign_change_users", | ||||||
|             "assign_change_groups", |             "assign_change_groups", | ||||||
|             "assign_custom_fields_w_values", |             "assign_custom_fields", | ||||||
|  |             "assign_custom_fields_values", | ||||||
|             "remove_all_tags", |             "remove_all_tags", | ||||||
|             "remove_tags", |             "remove_tags", | ||||||
|             "remove_all_correspondents", |             "remove_all_correspondents", | ||||||
| @@ -2135,6 +2136,7 @@ class WorkflowSerializer(serializers.ModelSerializer): | |||||||
|                 assign_view_groups = action.pop("assign_view_groups", None) |                 assign_view_groups = action.pop("assign_view_groups", None) | ||||||
|                 assign_change_users = action.pop("assign_change_users", None) |                 assign_change_users = action.pop("assign_change_users", None) | ||||||
|                 assign_change_groups = action.pop("assign_change_groups", None) |                 assign_change_groups = action.pop("assign_change_groups", None) | ||||||
|  |                 assign_custom_fields = action.pop("assign_custom_fields", None) | ||||||
|                 remove_tags = action.pop("remove_tags", None) |                 remove_tags = action.pop("remove_tags", None) | ||||||
|                 remove_correspondents = action.pop("remove_correspondents", None) |                 remove_correspondents = action.pop("remove_correspondents", None) | ||||||
|                 remove_document_types = action.pop("remove_document_types", None) |                 remove_document_types = action.pop("remove_document_types", None) | ||||||
| @@ -2184,6 +2186,8 @@ class WorkflowSerializer(serializers.ModelSerializer): | |||||||
|                     action_instance.assign_change_users.set(assign_change_users) |                     action_instance.assign_change_users.set(assign_change_users) | ||||||
|                 if assign_change_groups is not None: |                 if assign_change_groups is not None: | ||||||
|                     action_instance.assign_change_groups.set(assign_change_groups) |                     action_instance.assign_change_groups.set(assign_change_groups) | ||||||
|  |                 if assign_custom_fields is not None: | ||||||
|  |                     action_instance.assign_custom_fields.set(assign_custom_fields) | ||||||
|                 if remove_tags is not None: |                 if remove_tags is not None: | ||||||
|                     action_instance.remove_tags.set(remove_tags) |                     action_instance.remove_tags.set(remove_tags) | ||||||
|                 if remove_correspondents is not None: |                 if remove_correspondents is not None: | ||||||
|   | |||||||
| @@ -769,29 +769,34 @@ def run_workflows( | |||||||
|                     ), |                     ), | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|         if action.assign_custom_fields_w_values: |         if action.assign_custom_fields.exists(): | ||||||
|             if not use_overrides: |             if not use_overrides: | ||||||
|                 for field_id in action.assign_custom_fields_w_values: |                 for field in action.assign_custom_fields.all(): | ||||||
|                     if not CustomFieldInstance.objects.filter( |                     if not CustomFieldInstance.objects.filter( | ||||||
|                         field_id=field_id, |                         field=field, | ||||||
|                         document=document, |                         document=document, | ||||||
|                     ).exists(): |                     ).exists(): | ||||||
|                         # can be triggered on existing docs, so only add the field if it doesn't already exist |                         # can be triggered on existing docs, so only add the field if it doesn't already exist | ||||||
|                         field = CustomField.objects.get(pk=field_id) |  | ||||||
|                         value_field_name = CustomFieldInstance.get_value_field_name( |                         value_field_name = CustomFieldInstance.get_value_field_name( | ||||||
|                             data_type=field.data_type, |                             data_type=field.data_type, | ||||||
|                         ) |                         ) | ||||||
|                         args = { |                         args = { | ||||||
|                             "field": field, |                             "field": field, | ||||||
|                             "document": document, |                             "document": document, | ||||||
|                             value_field_name: action.assign_custom_fields_w_values[ |                             value_field_name: action.assign_custom_fields_values.get( | ||||||
|                                 field_id |                                 field.pk, | ||||||
|                             ], |                                 None, | ||||||
|  |                             ), | ||||||
|                         } |                         } | ||||||
|                         CustomFieldInstance.objects.create(**args) |                         CustomFieldInstance.objects.create(**args) | ||||||
|             else: |             else: | ||||||
|  |                 if overrides.custom_fields is None: | ||||||
|  |                     overrides.custom_fields = {} | ||||||
|                 overrides.custom_fields.update( |                 overrides.custom_fields.update( | ||||||
|                     action.assign_custom_fields_w_values, |                     { | ||||||
|  |                         field.pk: action.assign_custom_fields_values.get(field.pk, None) | ||||||
|  |                         for field in action.assign_custom_fields.all() | ||||||
|  |                     }, | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|     def removal_action(): |     def removal_action(): | ||||||
|   | |||||||
| @@ -1471,7 +1471,10 @@ class PostDocumentView(GenericAPIView): | |||||||
|             created=created, |             created=created, | ||||||
|             asn=archive_serial_number, |             asn=archive_serial_number, | ||||||
|             owner_id=request.user.id, |             owner_id=request.user.id, | ||||||
|             custom_fields={cf_id: None for cf_id in custom_field_ids},  # for now |             # TODO: set values | ||||||
|  |             custom_fields={cf_id: None for cf_id in custom_field_ids} | ||||||
|  |             if custom_field_ids | ||||||
|  |             else None, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         async_task = consume_file.delay( |         async_task = consume_file.delay( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon