mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-30 18:27:45 -05:00
Feature: Workflows (#5121)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<pngx-page-header title="Consumption Templates" i18n-title>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }">
|
||||
<pngx-page-header title="Workflows" i18n-title>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editWorkflow()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Template</ng-container>
|
||||
<ng-container i18n>Add Workflow</ng-container>
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
@@ -13,25 +13,27 @@
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Sort order</div>
|
||||
<div class="col" i18n>Document Sources</div>
|
||||
<div class="col" i18n>Status</div>
|
||||
<div class="col" i18n>Triggers</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@for (template of templates; track template) {
|
||||
@for (workflow of workflows; track workflow.id) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><code>{{template.order}}</code></div>
|
||||
<div class="col d-flex align-items-center">{{getSourceList(template)}}</div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><code>{{workflow.order}}</code></div>
|
||||
<div class="col d-flex align-items-center"><code> @if(workflow.enabled) { <ng-container i18n>Enabled</ng-container> } @else { <span i18n class="text-muted">Disabled</span> }</code></div>
|
||||
<div class="col d-flex align-items-center">{{getTypesList(workflow)}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editWorkflow(workflow)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteWorkflow(workflow)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
@@ -41,7 +43,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (templates.length === 0) {
|
||||
<li class="list-group-item" i18n>No templates defined.</li>
|
||||
@if (workflows.length === 0) {
|
||||
<li class="list-group-item" i18n>No workflows defined.</li>
|
||||
}
|
||||
</ul>
|
@@ -9,55 +9,76 @@ import {
|
||||
NgbModalModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import {
|
||||
DocumentSource,
|
||||
ConsumptionTemplate,
|
||||
} from 'src/app/data/consumption-template'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { ConsumptionTemplatesComponent } from './consumption-templates.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
|
||||
import { WorkflowsComponent } from './workflows.component'
|
||||
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import {
|
||||
DocumentSource,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import { WorkflowActionType } from 'src/app/data/workflow-action'
|
||||
|
||||
const templates: ConsumptionTemplate[] = [
|
||||
const workflows: Workflow[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Template 1',
|
||||
order: 0,
|
||||
sources: [
|
||||
DocumentSource.ConsumeFolder,
|
||||
DocumentSource.ApiUpload,
|
||||
DocumentSource.MailFetch,
|
||||
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',
|
||||
},
|
||||
],
|
||||
filter_filename: 'foo',
|
||||
filter_path: 'bar',
|
||||
assign_tags: [1, 2, 3],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Template 2',
|
||||
order: 1,
|
||||
sources: [DocumentSource.MailFetch],
|
||||
filter_filename: null,
|
||||
filter_path: 'foo/bar',
|
||||
assign_owner: 1,
|
||||
name: 'Workflow 2',
|
||||
id: 2,
|
||||
order: 2,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowTriggerType.DocumentAdded,
|
||||
filter_filename: 'foo',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'bar',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
describe('ConsumptionTemplatesComponent', () => {
|
||||
let component: ConsumptionTemplatesComponent
|
||||
let fixture: ComponentFixture<ConsumptionTemplatesComponent>
|
||||
let consumptionTemplateService: ConsumptionTemplateService
|
||||
describe('WorkflowsComponent', () => {
|
||||
let component: WorkflowsComponent
|
||||
let fixture: ComponentFixture<WorkflowsComponent>
|
||||
let workflowService: WorkflowService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConsumptionTemplatesComponent,
|
||||
WorkflowsComponent,
|
||||
IfPermissionsDirective,
|
||||
PageHeaderComponent,
|
||||
ConfirmDialogComponent,
|
||||
@@ -81,18 +102,18 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
],
|
||||
})
|
||||
|
||||
consumptionTemplateService = TestBed.inject(ConsumptionTemplateService)
|
||||
jest.spyOn(consumptionTemplateService, 'listAll').mockReturnValue(
|
||||
workflowService = TestBed.inject(WorkflowService)
|
||||
jest.spyOn(workflowService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: templates.length,
|
||||
all: templates.map((o) => o.id),
|
||||
results: templates,
|
||||
count: workflows.length,
|
||||
all: workflows.map((o) => o.id),
|
||||
results: workflows,
|
||||
})
|
||||
)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
|
||||
fixture = TestBed.createComponent(ConsumptionTemplatesComponent)
|
||||
fixture = TestBed.createComponent(WorkflowsComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
@@ -108,8 +129,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
createButton.triggerEventHandler('click')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
const editDialog =
|
||||
modal.componentInstance as ConsumptionTemplateEditDialogComponent
|
||||
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
|
||||
|
||||
// fail first
|
||||
editDialog.failed.emit({ error: 'error creating item' })
|
||||
@@ -117,7 +137,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
expect(reloadSpy).not.toHaveBeenCalled()
|
||||
|
||||
// succeed
|
||||
editDialog.succeeded.emit(templates[0])
|
||||
editDialog.succeeded.emit(workflows[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalled()
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
@@ -133,9 +153,8 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
editButton.triggerEventHandler('click')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
const editDialog =
|
||||
modal.componentInstance as ConsumptionTemplateEditDialogComponent
|
||||
expect(editDialog.object).toEqual(templates[0])
|
||||
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
|
||||
expect(editDialog.object).toEqual(workflows[0])
|
||||
|
||||
// fail first
|
||||
editDialog.failed.emit({ error: 'error editing item' })
|
||||
@@ -143,7 +162,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
expect(reloadSpy).not.toHaveBeenCalled()
|
||||
|
||||
// succeed
|
||||
editDialog.succeeded.emit(templates[0])
|
||||
editDialog.succeeded.emit(workflows[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalled()
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
@@ -152,7 +171,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const deleteSpy = jest.spyOn(consumptionTemplateService, 'delete')
|
||||
const deleteSpy = jest.spyOn(workflowService, 'delete')
|
||||
const reloadSpy = jest.spyOn(component, 'reload')
|
||||
|
||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[3]
|
@@ -1,33 +1,33 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { ConsumptionTemplate } from 'src/app/data/consumption-template'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import {
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
DOCUMENT_SOURCE_OPTIONS,
|
||||
} from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
|
||||
WorkflowEditDialogComponent,
|
||||
WORKFLOW_TYPE_OPTIONS,
|
||||
} from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-consumption-templates',
|
||||
templateUrl: './consumption-templates.component.html',
|
||||
styleUrls: ['./consumption-templates.component.scss'],
|
||||
selector: 'pngx-workflows',
|
||||
templateUrl: './workflows.component.html',
|
||||
styleUrls: ['./workflows.component.scss'],
|
||||
})
|
||||
export class ConsumptionTemplatesComponent
|
||||
export class WorkflowsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
public templates: ConsumptionTemplate[] = []
|
||||
public workflows: Workflow[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
private consumptionTemplateService: ConsumptionTemplateService,
|
||||
private workflowService: WorkflowService,
|
||||
public permissionsService: PermissionsService,
|
||||
private modalService: NgbModal,
|
||||
private toastService: ToastService
|
||||
@@ -40,68 +40,74 @@ export class ConsumptionTemplatesComponent
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.consumptionTemplateService
|
||||
this.workflowService
|
||||
.listAll()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((r) => {
|
||||
this.templates = r.results
|
||||
this.workflows = r.results
|
||||
})
|
||||
}
|
||||
|
||||
getSourceList(template: ConsumptionTemplate): string {
|
||||
return template.sources
|
||||
.map((id) => DOCUMENT_SOURCE_OPTIONS.find((s) => s.id === id).name)
|
||||
getTypesList(template: Workflow): string {
|
||||
return template.triggers
|
||||
.map(
|
||||
(trigger) =>
|
||||
WORKFLOW_TYPE_OPTIONS.find((t) => t.id === trigger.type).name
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
editTemplate(rule: ConsumptionTemplate) {
|
||||
const modal = this.modalService.open(
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
{
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
}
|
||||
)
|
||||
modal.componentInstance.dialogMode = rule
|
||||
editWorkflow(workflow: Workflow) {
|
||||
const modal = this.modalService.open(WorkflowEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = workflow
|
||||
? EditDialogMode.EDIT
|
||||
: EditDialogMode.CREATE
|
||||
modal.componentInstance.object = rule
|
||||
if (workflow) {
|
||||
// quick "deep" clone so original doesnt get modified
|
||||
const clone = Object.assign({}, workflow)
|
||||
clone.actions = [...workflow.actions]
|
||||
clone.triggers = [...workflow.triggers]
|
||||
modal.componentInstance.object = clone
|
||||
}
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((newTemplate) => {
|
||||
.subscribe((newWorkflow) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved template "${newTemplate.name}".`
|
||||
$localize`Saved workflow "${newWorkflow.name}".`
|
||||
)
|
||||
this.consumptionTemplateService.clearCache()
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((e) => {
|
||||
this.toastService.showError($localize`Error saving template.`, e)
|
||||
this.toastService.showError($localize`Error saving workflow.`, e)
|
||||
})
|
||||
}
|
||||
|
||||
deleteTemplate(rule: ConsumptionTemplate) {
|
||||
deleteWorkflow(workflow: Workflow) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete template`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this template.`
|
||||
modal.componentInstance.title = $localize`Confirm delete workflow`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this workflow.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.consumptionTemplateService.delete(rule).subscribe({
|
||||
this.workflowService.delete(workflow).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted template`)
|
||||
this.consumptionTemplateService.clearCache()
|
||||
this.toastService.showInfo($localize`Deleted workflow`)
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting template.`, e)
|
||||
this.toastService.showError($localize`Error deleting workflow.`, e)
|
||||
},
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user