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 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-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> | ||||||
|           <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, CustomFieldDataType } from 'src/app/data/custom-field' | import { CustomField } 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,7 +38,6 @@ 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' | ||||||
| @@ -47,6 +46,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 { 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,6 +148,7 @@ const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter( | |||||||
|     SwitchComponent, |     SwitchComponent, | ||||||
|     NumberComponent, |     NumberComponent, | ||||||
|     TextComponent, |     TextComponent, | ||||||
|  |     CustomFieldsSelectComponent, | ||||||
|     SelectComponent, |     SelectComponent, | ||||||
|     TextAreaComponent, |     TextAreaComponent, | ||||||
|     TagsComponent, |     TagsComponent, | ||||||
| @@ -174,7 +175,6 @@ export class WorkflowEditDialogComponent | |||||||
|   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,8 +189,7 @@ 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) | ||||||
|  |  | ||||||
| @@ -213,16 +212,6 @@ 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() { | ||||||
| @@ -263,6 +252,8 @@ 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) => { | ||||||
| @@ -438,7 +429,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: 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_tags: new FormControl(action.remove_tags), | ||||||
|         remove_all_tags: new FormControl(action.remove_all_tags), |         remove_all_tags: new FormControl(action.remove_all_tags), | ||||||
|         remove_document_types: new FormControl(action.remove_document_types), |         remove_document_types: new FormControl(action.remove_document_types), | ||||||
| @@ -564,7 +557,7 @@ export class WorkflowEditDialogComponent | |||||||
|       assign_view_groups: [], |       assign_view_groups: [], | ||||||
|       assign_change_users: [], |       assign_change_users: [], | ||||||
|       assign_change_groups: [], |       assign_change_groups: [], | ||||||
|       assign_custom_fields: [], |       assign_custom_fields_w_values: [], | ||||||
|       remove_tags: [], |       remove_tags: [], | ||||||
|       remove_all_tags: false, |       remove_all_tags: false, | ||||||
|       remove_document_types: [], |       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_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 |   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"), |         verbose_name=_("grant change permissions to these groups"), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     assign_custom_fields = models.ManyToManyField( |     assign_custom_fields_w_values = models.JSONField( | ||||||
|         CustomField, |         CustomField, | ||||||
|         blank=True, |         blank=True, | ||||||
|         related_name="+", |         null=True, | ||||||
|         verbose_name=_("assign these custom fields"), |         help_text=_( | ||||||
|  |             "assign these custom fields, with optional values", | ||||||
|  |         ), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     remove_tags = models.ManyToManyField( |     remove_tags = models.ManyToManyField( | ||||||
|   | |||||||
| @@ -2017,7 +2017,7 @@ 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", |             "assign_custom_fields_w_values", | ||||||
|             "remove_all_tags", |             "remove_all_tags", | ||||||
|             "remove_tags", |             "remove_tags", | ||||||
|             "remove_all_correspondents", |             "remove_all_correspondents", | ||||||
| @@ -2135,7 +2135,6 @@ 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) | ||||||
| @@ -2185,8 +2184,6 @@ 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: | ||||||
|   | |||||||
| @@ -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", |             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): | def add_to_index(sender, document, **kwargs): | ||||||
|     from documents import index |     from documents import index | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon