mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: Workflows (#5121)
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5 class="border-bottom pb-2" i18n>Filters</h5>
|
||||
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
|
||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="border-bottom pb-2" i18n>Assignments</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#consumption-templates'>documentation</a>." [error]="error?.assign_title"></pngx-input-text>
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||
<div>
|
||||
<label class="form-label" i18n>Assign view permissions</label>
|
||||
<div class="mb-2">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label" i18n>Assign edit permissions</label>
|
||||
<div>
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (error?.non_field_errors) {
|
||||
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
@@ -1,125 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormGroup, FormControl } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import {
|
||||
DocumentSource,
|
||||
ConsumptionTemplate,
|
||||
} from 'src/app/data/consumption-template'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent } from '../edit-dialog.component'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { MailRule } from 'src/app/data/mail-rule'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
|
||||
export const DOCUMENT_SOURCE_OPTIONS = [
|
||||
{
|
||||
id: DocumentSource.ConsumeFolder,
|
||||
name: $localize`Consume Folder`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.ApiUpload,
|
||||
name: $localize`API Upload`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.MailFetch,
|
||||
name: $localize`Mail Fetch`,
|
||||
},
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-consumption-template-edit-dialog',
|
||||
templateUrl: './consumption-template-edit-dialog.component.html',
|
||||
styleUrls: ['./consumption-template-edit-dialog.component.scss'],
|
||||
})
|
||||
export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<ConsumptionTemplate> {
|
||||
templates: ConsumptionTemplate[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
mailRules: MailRule[]
|
||||
customFields: CustomField[]
|
||||
|
||||
constructor(
|
||||
service: ConsumptionTemplateService,
|
||||
activeModal: NgbActiveModal,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService,
|
||||
storagePathService: StoragePathService,
|
||||
mailRuleService: MailRuleService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService,
|
||||
customFieldsService: CustomFieldsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
|
||||
correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
mailRuleService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.mailRules = result.results))
|
||||
|
||||
customFieldsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new consumption template`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit consumption template`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(null),
|
||||
account: new FormControl(null),
|
||||
filter_filename: new FormControl(null),
|
||||
filter_path: new FormControl(null),
|
||||
filter_mailrule: new FormControl(null),
|
||||
order: new FormControl(null),
|
||||
sources: new FormControl([]),
|
||||
assign_title: new FormControl(null),
|
||||
assign_tags: new FormControl([]),
|
||||
assign_owner: new FormControl(null),
|
||||
assign_document_type: new FormControl(null),
|
||||
assign_correspondent: new FormControl(null),
|
||||
assign_storage_path: new FormControl(null),
|
||||
assign_view_users: new FormControl([]),
|
||||
assign_view_groups: new FormControl([]),
|
||||
assign_change_users: new FormControl([]),
|
||||
assign_change_groups: new FormControl([]),
|
||||
assign_custom_fields: new FormControl([]),
|
||||
})
|
||||
}
|
||||
|
||||
get sourceOptions() {
|
||||
return DOCUMENT_SOURCE_OPTIONS
|
||||
}
|
||||
}
|
@@ -0,0 +1,207 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-switch i18n-title title="Enabled" formControlName="enabled" [error]="error?.enabled"></pngx-input-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton i18n>Triggers</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="d-flex">
|
||||
<p class="p-2" i18n>Trigger Workflow On:</p>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Trigger</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordion [closeOthers]="true">
|
||||
@for (trigger of object?.triggers; track trigger; let i = $index){
|
||||
<div ngbAccordionItem>
|
||||
<div ngbAccordionHeader>
|
||||
<button ngbAccordionButton>{{i + 1}}. {{getTriggerTypeOptionName(triggerFields.controls[i].value.type)}}
|
||||
@if(trigger.id > -1) {
|
||||
<span class="badge bg-dark text-light ms-2">ID: {{trigger.id}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template [ngTemplateOutlet]="triggerForm" [ngTemplateOutletContext]="{ formGroup: triggerFields.controls[i], trigger: trigger }"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button class="btn-lg" ngbAccordionButton i18n>Actions</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="d-flex">
|
||||
<p class="p-2" i18n>Apply Actions:</p>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Action</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
||||
@for (action of object?.actions; track action; let i = $index){
|
||||
<div ngbAccordionItem cdkDrag [formGroup]="actionFields.controls[i]">
|
||||
<div ngbAccordionHeader>
|
||||
<button ngbAccordionButton><ng-container i18n>Action</ng-container> {{i + 1}}
|
||||
@if(action.id > -1) {
|
||||
<span class="badge bg-dark text-light ms-2">ID: {{action.id}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
|
||||
<input type="hidden" formControlName="id" />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.assign_title"></pngx-input-text>
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||
<div>
|
||||
<label class="form-label" i18n>Assign view permissions</label>
|
||||
<div class="mb-2">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label" i18n>Assign edit permissions</label>
|
||||
<div>
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (error?.non_field_errors) {
|
||||
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template #triggerForm let-formGroup="formGroup" let-trigger="trigger">
|
||||
<div [formGroup]="formGroup">
|
||||
<input type="hidden" formControlName="id" />
|
||||
<pngx-input-select i18n-title title="Trigger type" [horizontal]="true" [items]="triggerTypeOptions" formControlName="type"></pngx-input-select>
|
||||
<p class="small" i18n>Trigger for documents that match <em>all</em> filters specified below.</p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||
}
|
||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||
<pngx-input-select i18n-title title="Content matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Content matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
}
|
||||
@if (patternRequired) {
|
||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||
<div class="col-md-6">
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Has tags" formControlName="filter_has_tags"></pngx-input-tags>
|
||||
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@@ -0,0 +1,5 @@
|
||||
.btn.text-danger {
|
||||
&:hover, &:focus {
|
||||
color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;
|
||||
}
|
||||
}
|
@@ -18,24 +18,69 @@ import { PermissionsUserComponent } from '../../input/permissions/permissions-us
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TagsComponent } from '../../input/tags/tags.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { SwitchComponent } from '../../input/switch/switch.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from './consumption-template-edit-dialog.component'
|
||||
import {
|
||||
DOCUMENT_SOURCE_OPTIONS,
|
||||
WORKFLOW_ACTION_OPTIONS,
|
||||
WORKFLOW_TYPE_OPTIONS,
|
||||
WorkflowEditDialogComponent,
|
||||
} from './workflow-edit-dialog.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import {
|
||||
WorkflowTriggerType,
|
||||
DocumentSource,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop'
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/app/data/workflow-action'
|
||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||
|
||||
const workflow: Workflow = {
|
||||
name: 'Workflow 1',
|
||||
id: 1,
|
||||
order: 1,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [DocumentSource.ConsumeFolder],
|
||||
filter_filename: '*',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'foo',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_owner: 2,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
let component: ConsumptionTemplateEditDialogComponent
|
||||
let component: WorkflowEditDialogComponent
|
||||
let settingsService: SettingsService
|
||||
let fixture: ComponentFixture<ConsumptionTemplateEditDialogComponent>
|
||||
let fixture: ComponentFixture<WorkflowEditDialogComponent>
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
WorkflowEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
TagsComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
@@ -113,7 +158,7 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ConsumptionTemplateEditDialogComponent)
|
||||
fixture = TestBed.createComponent(WorkflowEditDialogComponent)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 99, username: 'user99' }
|
||||
component = fixture.componentInstance
|
||||
@@ -121,15 +166,70 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
it('should support create and edit modes, support adding triggers and actions on new workflow', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
expect(component.object).toBeUndefined()
|
||||
component.addAction()
|
||||
expect(component.object).not.toBeUndefined()
|
||||
expect(component.object.actions).toHaveLength(1)
|
||||
component.object = undefined
|
||||
component.addTrigger()
|
||||
expect(component.object).not.toBeUndefined()
|
||||
expect(component.object.triggers).toHaveLength(1)
|
||||
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return source options, type options, type name', () => {
|
||||
// coverage
|
||||
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
|
||||
expect(component.triggerTypeOptions).toEqual(WORKFLOW_TYPE_OPTIONS)
|
||||
expect(
|
||||
component.getTriggerTypeOptionName(WorkflowTriggerType.DocumentAdded)
|
||||
).toEqual('Document Added')
|
||||
expect(component.getTriggerTypeOptionName(null)).toEqual('')
|
||||
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
|
||||
expect(component.actionTypeOptions).toEqual(WORKFLOW_ACTION_OPTIONS)
|
||||
expect(
|
||||
component.getActionTypeOptionName(WorkflowActionType.Assignment)
|
||||
).toEqual('Assignment')
|
||||
expect(component.getActionTypeOptionName(null)).toEqual('')
|
||||
})
|
||||
|
||||
it('should support add and remove triggers and actions', () => {
|
||||
component.object = workflow
|
||||
component.addTrigger()
|
||||
expect(component.object.triggers.length).toEqual(2)
|
||||
component.addAction()
|
||||
expect(component.object.actions.length).toEqual(3)
|
||||
component.removeTrigger(1)
|
||||
expect(component.object.triggers.length).toEqual(1)
|
||||
component.removeAction(1)
|
||||
expect(component.object.actions.length).toEqual(2)
|
||||
})
|
||||
|
||||
it('should update order and remove ids from actions on drag n drop', () => {
|
||||
const action1 = workflow.actions[0]
|
||||
const action2 = workflow.actions[1]
|
||||
component.object = workflow
|
||||
component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
|
||||
WorkflowAction[]
|
||||
>)
|
||||
expect(component.object.actions).toEqual([action2, action1])
|
||||
expect(action1.id).toBeNull()
|
||||
expect(action2.id).toBeNull()
|
||||
})
|
||||
|
||||
it('should not include auto matching in algorithms', () => {
|
||||
expect(component.getMatchingAlgorithms()).not.toContain(
|
||||
MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
|
||||
)
|
||||
})
|
||||
})
|
@@ -0,0 +1,300 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormGroup, FormControl, FormArray } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent } from '../edit-dialog.component'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { MailRule } from 'src/app/data/mail-rule'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import {
|
||||
DocumentSource,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/app/data/workflow-action'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import {
|
||||
MATCHING_ALGORITHMS,
|
||||
MATCH_AUTO,
|
||||
MATCH_NONE,
|
||||
} from 'src/app/data/matching-model'
|
||||
|
||||
export const DOCUMENT_SOURCE_OPTIONS = [
|
||||
{
|
||||
id: DocumentSource.ConsumeFolder,
|
||||
name: $localize`Consume Folder`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.ApiUpload,
|
||||
name: $localize`API Upload`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.MailFetch,
|
||||
name: $localize`Mail Fetch`,
|
||||
},
|
||||
]
|
||||
|
||||
export const WORKFLOW_TYPE_OPTIONS = [
|
||||
{
|
||||
id: WorkflowTriggerType.Consumption,
|
||||
name: $localize`Consumption Started`,
|
||||
},
|
||||
{
|
||||
id: WorkflowTriggerType.DocumentAdded,
|
||||
name: $localize`Document Added`,
|
||||
},
|
||||
{
|
||||
id: WorkflowTriggerType.DocumentUpdated,
|
||||
name: $localize`Document Updated`,
|
||||
},
|
||||
]
|
||||
|
||||
export const WORKFLOW_ACTION_OPTIONS = [
|
||||
{
|
||||
id: WorkflowActionType.Assignment,
|
||||
name: $localize`Assignment`,
|
||||
},
|
||||
]
|
||||
|
||||
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
|
||||
(a) => a.id !== MATCH_AUTO
|
||||
)
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-workflow-edit-dialog',
|
||||
templateUrl: './workflow-edit-dialog.component.html',
|
||||
styleUrls: ['./workflow-edit-dialog.component.scss'],
|
||||
})
|
||||
export class WorkflowEditDialogComponent
|
||||
extends EditDialogComponent<Workflow>
|
||||
implements OnInit
|
||||
{
|
||||
public WorkflowTriggerType = WorkflowTriggerType
|
||||
|
||||
templates: Workflow[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
mailRules: MailRule[]
|
||||
customFields: CustomField[]
|
||||
|
||||
expandedItem: number = null
|
||||
|
||||
constructor(
|
||||
service: WorkflowService,
|
||||
activeModal: NgbActiveModal,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService,
|
||||
storagePathService: StoragePathService,
|
||||
mailRuleService: MailRuleService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService,
|
||||
customFieldsService: CustomFieldsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
|
||||
correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
mailRuleService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.mailRules = result.results))
|
||||
|
||||
customFieldsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new workflow`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit workflow`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(null),
|
||||
order: new FormControl(null),
|
||||
enabled: new FormControl(true),
|
||||
triggers: new FormArray([]),
|
||||
actions: new FormArray([]),
|
||||
})
|
||||
}
|
||||
|
||||
getMatchingAlgorithms() {
|
||||
// No auto matching
|
||||
return TRIGGER_MATCHING_ALGORITHMS
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.updateTriggerActionFields()
|
||||
}
|
||||
|
||||
get triggerFields(): FormArray {
|
||||
return this.objectForm.get('triggers') as FormArray
|
||||
}
|
||||
|
||||
get actionFields(): FormArray {
|
||||
return this.objectForm.get('actions') as FormArray
|
||||
}
|
||||
|
||||
private updateTriggerActionFields(emitEvent: boolean = false) {
|
||||
this.triggerFields.clear({ emitEvent: false })
|
||||
this.object?.triggers.forEach((trigger) => {
|
||||
this.triggerFields.push(
|
||||
new FormGroup({
|
||||
id: new FormControl(trigger.id),
|
||||
type: new FormControl(trigger.type),
|
||||
sources: new FormControl(trigger.sources),
|
||||
filter_filename: new FormControl(trigger.filter_filename),
|
||||
filter_path: new FormControl(trigger.filter_path),
|
||||
filter_mailrule: new FormControl(trigger.filter_mailrule),
|
||||
matching_algorithm: new FormControl(MATCH_NONE),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
filter_has_tags: new FormControl(trigger.filter_has_tags),
|
||||
filter_has_correspondent: new FormControl(
|
||||
trigger.filter_has_correspondent
|
||||
),
|
||||
filter_has_document_type: new FormControl(
|
||||
trigger.filter_has_document_type
|
||||
),
|
||||
}),
|
||||
{ emitEvent }
|
||||
)
|
||||
})
|
||||
|
||||
this.actionFields.clear({ emitEvent: false })
|
||||
this.object?.actions.forEach((action) => {
|
||||
this.actionFields.push(
|
||||
new FormGroup({
|
||||
id: new FormControl(action.id),
|
||||
type: new FormControl(action.type),
|
||||
assign_title: new FormControl(action.assign_title),
|
||||
assign_tags: new FormControl(action.assign_tags),
|
||||
assign_owner: new FormControl(action.assign_owner),
|
||||
assign_document_type: new FormControl(action.assign_document_type),
|
||||
assign_correspondent: new FormControl(action.assign_correspondent),
|
||||
assign_storage_path: new FormControl(action.assign_storage_path),
|
||||
assign_view_users: new FormControl(action.assign_view_users),
|
||||
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),
|
||||
}),
|
||||
{ emitEvent }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
get sourceOptions() {
|
||||
return DOCUMENT_SOURCE_OPTIONS
|
||||
}
|
||||
|
||||
get triggerTypeOptions() {
|
||||
return WORKFLOW_TYPE_OPTIONS
|
||||
}
|
||||
|
||||
getTriggerTypeOptionName(type: WorkflowTriggerType): string {
|
||||
return this.triggerTypeOptions.find((t) => t.id === type)?.name ?? ''
|
||||
}
|
||||
|
||||
addTrigger() {
|
||||
if (!this.object) {
|
||||
this.object = Object.assign({}, this.objectForm.value)
|
||||
}
|
||||
this.object.triggers.push({
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [],
|
||||
filter_filename: null,
|
||||
filter_path: null,
|
||||
filter_mailrule: null,
|
||||
filter_has_tags: [],
|
||||
filter_has_correspondent: null,
|
||||
filter_has_document_type: null,
|
||||
})
|
||||
|
||||
this.updateTriggerActionFields()
|
||||
}
|
||||
|
||||
get actionTypeOptions() {
|
||||
return WORKFLOW_ACTION_OPTIONS
|
||||
}
|
||||
|
||||
getActionTypeOptionName(type: WorkflowActionType): string {
|
||||
return this.actionTypeOptions.find((t) => t.id === type)?.name ?? ''
|
||||
}
|
||||
|
||||
addAction() {
|
||||
if (!this.object) {
|
||||
this.object = Object.assign({}, this.objectForm.value)
|
||||
}
|
||||
this.object.actions.push({
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: null,
|
||||
assign_tags: [],
|
||||
assign_document_type: null,
|
||||
assign_correspondent: null,
|
||||
assign_storage_path: null,
|
||||
assign_owner: null,
|
||||
assign_view_users: [],
|
||||
assign_view_groups: [],
|
||||
assign_change_users: [],
|
||||
assign_change_groups: [],
|
||||
assign_custom_fields: [],
|
||||
})
|
||||
|
||||
this.updateTriggerActionFields()
|
||||
}
|
||||
|
||||
removeTrigger(index: number) {
|
||||
this.object.triggers.splice(index, 1)
|
||||
this.updateTriggerActionFields()
|
||||
}
|
||||
|
||||
removeAction(index: number) {
|
||||
this.object.actions.splice(index, 1)
|
||||
this.updateTriggerActionFields()
|
||||
}
|
||||
|
||||
onActionDrop(event: CdkDragDrop<WorkflowAction[]>) {
|
||||
moveItemInArray(
|
||||
this.object.actions,
|
||||
event.previousIndex,
|
||||
event.currentIndex
|
||||
)
|
||||
// removing id will effectively re-create the actions in this order
|
||||
this.object.actions.forEach((a) => (a.id = null))
|
||||
this.updateTriggerActionFields()
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
.accordion {
|
||||
--bs-accordion-btn-padding-x: 0.75rem;
|
||||
--bs-accordion-btn-padding-y: 0.375rem;
|
||||
}
|
||||
|
@@ -1,27 +1,27 @@
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
@if (horizontal) {
|
||||
@if (!horizontal) {
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
|
||||
<div class="form-check form-switch">
|
||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
@if (!horizontal) {
|
||||
<label class="form-check-label" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
@if (hint) {
|
||||
<div class="form-text text-muted">{{hint}}</div>
|
||||
}
|
||||
</div>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
|
||||
<div class="form-check form-switch">
|
||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
@if (horizontal) {
|
||||
<label class="form-check-label" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
@if (hint) {
|
||||
<div class="form-text text-muted">{{hint}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user