mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Basic backend migration, frontend UI. Mostly works
[ci skip]
This commit is contained in:
		| @@ -188,7 +188,7 @@ | ||||
|             <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 storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-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-select i18n-title title="Assign custom fields" formControlName="assign_custom_fields_w_values"></pngx-input-custom-fields-select> | ||||
|           </div> | ||||
|           <div class="col"> | ||||
|             <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 { first } from 'rxjs' | ||||
| import { Correspondent } from 'src/app/data/correspondent' | ||||
| import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' | ||||
| import { CustomField } from 'src/app/data/custom-field' | ||||
| import { DocumentType } from 'src/app/data/document-type' | ||||
| import { MailRule } from 'src/app/data/mail-rule' | ||||
| import { | ||||
| @@ -38,7 +38,6 @@ import { | ||||
|   WorkflowTriggerType, | ||||
| } from 'src/app/data/workflow-trigger' | ||||
| 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 { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| @@ -47,6 +46,7 @@ import { WorkflowService } from 'src/app/services/rest/workflow.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component' | ||||
| import { CheckComponent } from '../../input/check/check.component' | ||||
| import { CustomFieldsSelectComponent } from '../../input/custom-fields-select/custom-fields-select.component' | ||||
| import { EntriesComponent } from '../../input/entries/entries.component' | ||||
| import { NumberComponent } from '../../input/number/number.component' | ||||
| import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component' | ||||
| @@ -148,6 +148,7 @@ const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter( | ||||
|     SwitchComponent, | ||||
|     NumberComponent, | ||||
|     TextComponent, | ||||
|     CustomFieldsSelectComponent, | ||||
|     SelectComponent, | ||||
|     TextAreaComponent, | ||||
|     TagsComponent, | ||||
| @@ -174,7 +175,6 @@ export class WorkflowEditDialogComponent | ||||
|   documentTypes: DocumentType[] | ||||
|   storagePaths: StoragePath[] | ||||
|   mailRules: MailRule[] | ||||
|   customFields: CustomField[] | ||||
|   dateCustomFields: CustomField[] | ||||
|  | ||||
|   expandedItem: number = null | ||||
| @@ -189,8 +189,7 @@ export class WorkflowEditDialogComponent | ||||
|     storagePathService: StoragePathService, | ||||
|     mailRuleService: MailRuleService, | ||||
|     userService: UserService, | ||||
|     settingsService: SettingsService, | ||||
|     customFieldsService: CustomFieldsService | ||||
|     settingsService: SettingsService | ||||
|   ) { | ||||
|     super(service, activeModal, userService, settingsService) | ||||
|  | ||||
| @@ -213,16 +212,6 @@ export class WorkflowEditDialogComponent | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .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() { | ||||
| @@ -263,6 +252,8 @@ export class WorkflowEditDialogComponent | ||||
|   } | ||||
|  | ||||
|   private checkRemovalActionFields(formWorkflow: Workflow) { | ||||
|     console.log('checkRemovalActionFields', formWorkflow) | ||||
|  | ||||
|     formWorkflow.actions | ||||
|       .filter((action) => action.type === WorkflowActionType.Removal) | ||||
|       .forEach((action, i) => { | ||||
| @@ -438,7 +429,9 @@ export class WorkflowEditDialogComponent | ||||
|         assign_view_groups: new FormControl(action.assign_view_groups), | ||||
|         assign_change_users: new FormControl(action.assign_change_users), | ||||
|         assign_change_groups: new FormControl(action.assign_change_groups), | ||||
|         assign_custom_fields: new FormControl(action.assign_custom_fields), | ||||
|         assign_custom_fields_w_values: new FormControl( | ||||
|           action.assign_custom_fields_w_values | ||||
|         ), | ||||
|         remove_tags: new FormControl(action.remove_tags), | ||||
|         remove_all_tags: new FormControl(action.remove_all_tags), | ||||
|         remove_document_types: new FormControl(action.remove_document_types), | ||||
| @@ -564,7 +557,7 @@ export class WorkflowEditDialogComponent | ||||
|       assign_view_groups: [], | ||||
|       assign_change_users: [], | ||||
|       assign_change_groups: [], | ||||
|       assign_custom_fields: [], | ||||
|       assign_custom_fields_w_values: [], | ||||
|       remove_tags: [], | ||||
|       remove_all_tags: false, | ||||
|       remove_document_types: [], | ||||
|   | ||||
| @@ -0,0 +1,113 @@ | ||||
| <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> | ||||
| @@ -0,0 +1,41 @@ | ||||
| // 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; | ||||
| } | ||||
| @@ -0,0 +1,135 @@ | ||||
| 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,92 @@ | ||||
| import { Component, forwardRef } from '@angular/core' | ||||
| import { | ||||
|   FormsModule, | ||||
|   NG_VALUE_ACCESSOR, | ||||
|   ReactiveFormsModule, | ||||
| } from '@angular/forms' | ||||
| import { RouterModule } from '@angular/router' | ||||
| import { NgSelectModule } from '@ng-select/ng-select' | ||||
| import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | ||||
| import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' | ||||
| import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' | ||||
| import { AbstractInputComponent } from '../abstract-input' | ||||
| import { CheckComponent } from '../check/check.component' | ||||
| import { DateComponent } from '../date/date.component' | ||||
| import { DocumentLinkComponent } from '../document-link/document-link.component' | ||||
| 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 { UrlComponent } from '../url/url.component' | ||||
|  | ||||
| @Component({ | ||||
|   providers: [ | ||||
|     { | ||||
|       provide: NG_VALUE_ACCESSOR, | ||||
|       useExisting: forwardRef(() => CustomFieldsSelectComponent), | ||||
|       multi: true, | ||||
|     }, | ||||
|   ], | ||||
|   selector: 'pngx-input-custom-fields-select', | ||||
|   templateUrl: './custom-fields-select.component.html', | ||||
|   styleUrls: ['./custom-fields-select.component.scss'], | ||||
|   imports: [ | ||||
|     TextComponent, | ||||
|     DateComponent, | ||||
|     NumberComponent, | ||||
|     DocumentLinkComponent, | ||||
|     UrlComponent, | ||||
|     SelectComponent, | ||||
|     MonetaryComponent, | ||||
|     CheckComponent, | ||||
|     NgSelectModule, | ||||
|     FormsModule, | ||||
|     ReactiveFormsModule, | ||||
|     RouterModule, | ||||
|     NgxBootstrapIconsModule, | ||||
|   ], | ||||
| }) | ||||
| export class CustomFieldsSelectComponent extends AbstractInputComponent<Object> { | ||||
|   public CustomFieldDataType = CustomFieldDataType | ||||
|  | ||||
|   constructor(customFieldsService: CustomFieldsService) { | ||||
|     super() | ||||
|     customFieldsService.listAll().subscribe((items) => { | ||||
|       this.fields = items.results | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   fields: CustomField[] | ||||
|  | ||||
|   _selectedFields: number[] | ||||
|   set selectedFields(newFields: number[]) { | ||||
|     this._selectedFields = newFields | ||||
|     // map the selected fields to an object with field_id as key and value as value | ||||
|     this.value = newFields.reduce((acc, fieldId) => { | ||||
|       acc[fieldId] = this.value?.[fieldId] || null | ||||
|       return acc | ||||
|     }, {}) | ||||
|     this.onChange(this.value) | ||||
|   } | ||||
|   get selectedFields(): number[] { | ||||
|     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 { | ||||
|     return this.fields.find((field) => field.id === id) | ||||
|   } | ||||
|  | ||||
|   public removeField(fieldId: number): void { | ||||
|     this.selectedFields = this.selectedFields.filter((id) => id !== fieldId) | ||||
|   } | ||||
| } | ||||
| @@ -56,7 +56,7 @@ export interface WorkflowAction extends ObjectWithId { | ||||
|  | ||||
|   assign_change_groups?: number[] // [Group.id] | ||||
|  | ||||
|   assign_custom_fields?: number[] // [CustomField.id] | ||||
|   assign_custom_fields_w_values?: number[] // { [CustomField.id]: value } | ||||
|  | ||||
|   remove_tags?: number[] // Tag.id | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,42 @@ | ||||
| # 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", | ||||
|         ), | ||||
|     ] | ||||
| @@ -1384,11 +1384,13 @@ class WorkflowAction(models.Model): | ||||
|         verbose_name=_("grant change permissions to these groups"), | ||||
|     ) | ||||
|  | ||||
|     assign_custom_fields = models.ManyToManyField( | ||||
|     assign_custom_fields_w_values = models.JSONField( | ||||
|         CustomField, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("assign these custom fields"), | ||||
|         null=True, | ||||
|         help_text=_( | ||||
|             "assign these custom fields, with optional values", | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     remove_tags = models.ManyToManyField( | ||||
|   | ||||
| @@ -2017,7 +2017,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer): | ||||
|             "assign_view_groups", | ||||
|             "assign_change_users", | ||||
|             "assign_change_groups", | ||||
|             "assign_custom_fields", | ||||
|             "assign_custom_fields_w_values", | ||||
|             "remove_all_tags", | ||||
|             "remove_tags", | ||||
|             "remove_all_correspondents", | ||||
| @@ -2135,7 +2135,6 @@ class WorkflowSerializer(serializers.ModelSerializer): | ||||
|                 assign_view_groups = action.pop("assign_view_groups", None) | ||||
|                 assign_change_users = action.pop("assign_change_users", 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_correspondents = action.pop("remove_correspondents", None) | ||||
|                 remove_document_types = action.pop("remove_document_types", None) | ||||
| @@ -2185,8 +2184,6 @@ class WorkflowSerializer(serializers.ModelSerializer): | ||||
|                     action_instance.assign_change_users.set(assign_change_users) | ||||
|                 if assign_change_groups is not None: | ||||
|                     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: | ||||
|                     action_instance.remove_tags.set(remove_tags) | ||||
|                 if remove_correspondents is not None: | ||||
|   | ||||
| @@ -576,6 +576,8 @@ def cleanup_custom_field_deletion(sender, instance: CustomField, **kwargs): | ||||
|             f"Removing custom field {instance} from sort field of {views_with_sort_updated} views", | ||||
|         ) | ||||
|  | ||||
|     # Remove from workflow actions | ||||
|  | ||||
|  | ||||
| def add_to_index(sender, document, **kwargs): | ||||
|     from documents import index | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon