Browse Source

Basic stuff.

pull/382/head
Sebastian Stehle 7 years ago
parent
commit
d848393770
  1. 2
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  2. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs
  3. 8
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs
  4. 1
      src/Squidex/app/features/settings/declarations.ts
  5. 2
      src/Squidex/app/features/settings/module.ts
  6. 4
      src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
  7. 70
      src/Squidex/app/features/settings/pages/workflows/workflow.component.html
  8. 25
      src/Squidex/app/features/settings/pages/workflows/workflow.component.scss
  9. 120
      src/Squidex/app/features/settings/pages/workflows/workflow.component.ts
  10. 56
      src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html
  11. 78
      src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts
  12. 2
      src/Squidex/app/features/settings/settings-area.component.html
  13. 1
      src/Squidex/app/shared/internal.ts
  14. 117
      src/Squidex/app/shared/services/workflows.service.spec.ts
  15. 51
      src/Squidex/app/shared/services/workflows.service.ts
  16. 2
      src/Squidex/app/shared/state/clients.forms.ts
  17. 24
      src/Squidex/app/shared/state/workflows.forms.ts
  18. 52
      src/Squidex/app/shared/state/workflows.state.spec.ts
  19. 56
      src/Squidex/app/shared/state/workflows.state.ts

2
src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
public sealed class Workflow : Named public sealed class Workflow : Named
{ {
private const string DefaultName = "Name"; private const string DefaultName = "Unnamed";
private static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>(); private static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Default = CreateDefault();

7
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs

@ -15,6 +15,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
public sealed class UpdateWorkflowDto public sealed class UpdateWorkflowDto
{ {
/// <summary>
/// The name of the workflow.
/// </summary>
public string Name { get; set; }
/// <summary> /// <summary>
/// The workflow steps. /// The workflow steps.
/// </summary> /// </summary>
@ -37,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
y => new WorkflowTransition(y.Value.Expression, y.Value.Role)), y => new WorkflowTransition(y.Value.Expression, y.Value.Role)),
x.Value.Color, x.Value.Color,
x.Value.NoUpdate)), x.Value.NoUpdate)),
Initial); Initial, Name);
return new UpdateWorkflow { Workflow = workflow }; return new UpdateWorkflow { Workflow = workflow };
} }

8
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs

@ -23,6 +23,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// The name of the workflow.
/// </summary>
public string Name { get; set; }
/// <summary> /// <summary>
/// The workflow steps. /// The workflow steps.
/// </summary> /// </summary>
@ -38,7 +43,6 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
var result = new WorkflowDto var result = new WorkflowDto
{ {
Id = id,
Steps = workflow.Steps.ToDictionary( Steps = workflow.Steps.ToDictionary(
x => x.Key, x => x.Key,
x => SimpleMapper.Map(x.Value, new WorkflowStepDto x => SimpleMapper.Map(x.Value, new WorkflowStepDto
@ -47,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
y => y.Key, y => y.Key,
y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role }) y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role })
})), })),
Initial = workflow.Initial Id = id, Name = workflow.Name, Initial = workflow.Initial
}; };
return result.CreateLinks(controller, app, id); return result.CreateLinks(controller, app, id);

1
src/Squidex/app/features/settings/declarations.ts

@ -20,6 +20,7 @@ export * from './pages/roles/role.component';
export * from './pages/roles/roles-page.component'; export * from './pages/roles/roles-page.component';
export * from './pages/workflows/workflow-step.component'; export * from './pages/workflows/workflow-step.component';
export * from './pages/workflows/workflow-transition.component'; export * from './pages/workflows/workflow-transition.component';
export * from './pages/workflows/workflow.component';
export * from './pages/workflows/workflows-page.component'; export * from './pages/workflows/workflows-page.component';
export * from './settings-area.component'; export * from './settings-area.component';

2
src/Squidex/app/features/settings/module.ts

@ -31,6 +31,7 @@ import {
RoleComponent, RoleComponent,
RolesPageComponent, RolesPageComponent,
SettingsAreaComponent, SettingsAreaComponent,
WorkflowComponent,
WorkflowsPageComponent, WorkflowsPageComponent,
WorkflowStepComponent, WorkflowStepComponent,
WorkflowTransitionComponent WorkflowTransitionComponent
@ -213,6 +214,7 @@ const routes: Routes = [
RoleComponent, RoleComponent,
RolesPageComponent, RolesPageComponent,
SettingsAreaComponent, SettingsAreaComponent,
WorkflowComponent,
WorkflowsPageComponent, WorkflowsPageComponent,
WorkflowTransitionComponent, WorkflowTransitionComponent,
WorkflowStepComponent WorkflowStepComponent

4
src/Squidex/app/features/settings/pages/clients/clients-page.component.ts

@ -9,8 +9,8 @@ import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { import {
AddClientForm,
AppsState, AppsState,
AttachClientForm,
ClientDto, ClientDto,
ClientsState, ClientsState,
RolesState RolesState
@ -22,7 +22,7 @@ import {
templateUrl: './clients-page.component.html' templateUrl: './clients-page.component.html'
}) })
export class ClientsPageComponent implements OnInit { export class ClientsPageComponent implements OnInit {
public addClientForm = new AttachClientForm(this.formBuilder); public addClientForm = new AddClientForm(this.formBuilder);
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,

70
src/Squidex/app/features/settings/pages/workflows/workflow.component.html

@ -0,0 +1,70 @@
<div class="table-items-row table-items-row-expandable workflow">
<div class="table-items-row-summary">
<div class="row no-gutters">
<div class="col">
{{workflow.displayName}}
</div>
<div class="col-options">
<div class="float-right">
<button type="button" class="btn btn-secondary table-items-edit-button mr-1" [class.active]="isEditing" (click)="toggleEditing()">
<i class="icon-settings"></i>
</button>
<button type="button" class="btn btn-text-danger"
[disabled]="!workflow.canDelete"
(sqxConfirmClick)="remove()"
confirmTitle="Remove workflow"
confirmText="Do you really want to remove the workflow?">
<i class="icon-bin2"></i>
</button>
</div>
</div>
</div>
</div>
<div class="table-items-row-details" *ngIf="isEditing">
<div class="table-items-row-details-tabs clearfix">
<div class="float-right">
<button type="reset" class="btn btn-text" (click)="toggleEditing()">Cancel</button>
<button type="submit" class="btn btn-primary" *ngIf="isEditable" (click)="save()">Save</button>
</div>
</div>
<div class="table-items-row-details-tab">
<sqx-form-error [error]="error"></sqx-form-error>
<div class="form-group row">
<label class="col-form-label" for="{{workflow.id}}_name">Name</label>
<div class="col-6">
<input class="form-control" id="{{workflow.id}}_name"
[ngModelOptions]="onBlur"
[ngModel]="workflow.name"
(ngModelChange)="rename($event)" />
<sqx-form-hint>
Optional name for the workflow.
</sqx-form-hint>
</div>
</div>
<sqx-workflow-step *ngFor="let step of workflow.steps; trackBy: trackByStep"
[workflow]="workflow"
[disabled]="!workflow.canUpdate"
[roles]="roles"
[step]="step"
(makeInitial)="setInitial(step)"
(rename)="renameStep(step, $event)"
(remove)="removeStep(step)"
(transitionAdd)="addTransiton(step, $event)"
(transitionRemove)="removeTransition(step, $event)"
(transitionUpdate)="updateTransition($event)"
(update)="updateStep(step, $event)">
</sqx-workflow-step>
<button class="btn btn-success" (click)="addStep()" *ngIf="workflow.canUpdate">
Add Step
</button>
</div>
</div>
</div>

25
src/Squidex/app/features/settings/pages/workflows/workflow.component.scss

@ -0,0 +1,25 @@
@import '_vars';
@import '_mixins';
.table-items-row-details {
&::before {
right: 4.55rem;
}
}
.col-form-label {
min-width: 4rem;
max-width: 4rem;
text-align: left;
}
.form-group {
margin-bottom: 2rem;
margin-left: 2rem;
max-width: 60rem;
}
.btn-success {
margin-bottom: 1rem;
margin-left: 2rem;
}

120
src/Squidex/app/features/settings/pages/workflows/workflow.component.ts

@ -0,0 +1,120 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input, OnChanges } from '@angular/core';
import {
ErrorDto,
MathHelper,
RoleDto,
WorkflowDto,
WorkflowsState,
WorkflowStep,
WorkflowStepValues,
WorkflowTransition,
WorkflowTransitionValues
} from '@app/shared';
@Component({
selector: 'sqx-workflow',
styleUrls: ['./workflow.component.scss'],
templateUrl: './workflow.component.html'
})
export class WorkflowComponent implements OnChanges {
@Input()
public workflow: WorkflowDto;
@Input()
public roles: RoleDto[];
public error: ErrorDto | null;
public onBlur = { updateOn: 'blur' };
public isEditing = false;
public isEditable = false;
constructor(
private readonly workflowsState: WorkflowsState
) {
}
public ngOnChanges() {
this.isEditable = this.workflow.canUpdate;
}
public toggleEditing() {
this.isEditing = !this.isEditing;
}
public remove() {
this.workflowsState.delete(this.workflow);
}
public save() {
if (!this.isEditable) {
return;
}
this.workflowsState.update(this.workflow)
.subscribe(() => {
this.error = null;
}, error => {
this.error = error;
});
}
public addStep() {
let index = this.workflow.steps.length;
for (let i = index; i < index + 100; i++) {
const name = `Step${i}`;
if (!this.workflow.getStep(name)) {
this.workflow = this.workflow.setStep(name, { color: MathHelper.randomColor() });
return;
}
}
}
public rename(name: string) {
this.workflow = this.workflow.rename(name);
}
public setInitial(step: WorkflowStep) {
this.workflow = this.workflow.setInitial(step.name);
}
public addTransiton(from: WorkflowStep, to: WorkflowStep) {
this.workflow = this.workflow.setTransition(from.name, to.name, {});
}
public removeTransition(from: WorkflowStep, transition: WorkflowTransition) {
this.workflow = this.workflow.removeTransition(from.name, transition.to);
}
public updateTransition(update: { transition: WorkflowTransition, values: WorkflowTransitionValues }) {
this.workflow = this.workflow.setTransition(update.transition.from, update.transition.to, update.values);
}
public updateStep(step: WorkflowStep, values: WorkflowStepValues) {
this.workflow = this.workflow.setStep(step.name, values);
}
public renameStep(step: WorkflowStep, newName: string) {
this.workflow = this.workflow.renameStep(step.name, newName);
}
public removeStep(step: WorkflowStep) {
this.workflow = this.workflow.removeStep(step.name);
}
public trackByStep(step: WorkflowStep) {
return step.name;
}
}

56
src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html

@ -1,47 +1,47 @@
<sqx-title message="{app} | Workflows | Settings" parameter1="app" [value1]="appsState.appName"></sqx-title> <sqx-title message="{app} | Workflows | Settings" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="50rem" isBlank="true" [showSidebar]="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="60rem" [showSidebar]="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
Workflow Workflow
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh roles (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh workflows (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> Refresh
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<ng-container *ngIf="workflow && workflow.canUpdate">
<button type="button" class="btn btn-primary" (click)="save()" title="Save (CTRL + S)">
Save
</button>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut>
</ng-container>
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<ng-container *ngIf="workflow"> <ng-container *ngIf="workflowsState.workflows | async; let workflows">
<ng-container *ngIf="rolesState.roles | async; let roles"> <ng-container *ngIf="rolesState.roles | async; let roles">
<sqx-workflow-step *ngFor="let step of workflow.steps; trackBy: trackByStep" <div class="table-items-row table-items-row-empty" *ngIf="workflows.length === 0">
[workflow]="workflow" No workflows created yet.
[disabled]="!workflow.canUpdate" </div>
[roles]="roles"
[step]="step" <sqx-workflow *ngFor="let workflow of workflows; trackBy: trackByWorkflow"
(makeInitial)="setInitial(step)" [workflow]="workflow" [roles]="roles">
(rename)="renameStep(step, $event)" </sqx-workflow>
(remove)="removeStep(step)"
(transitionAdd)="addTransiton(step, $event)" <div class="table-items-footer" *ngIf="workflowsState.canCreate | async">
(transitionRemove)="removeTransition(step, $event)" <form [formGroup]="addWorkflowForm.form" (ngSubmit)="addWorkflow()">
(transitionUpdate)="updateTransition($event)" <div class="row no-gutters">
(update)="updateStep(step, $event)"> <div class="col">
</sqx-workflow-step> <sqx-control-errors for="name" [submitted]="addWorkflowForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter workflow name" autocomplete="off" />
</div>
<div class="col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="addWorkflowForm.hasNoName | async">Add Workflow</button>
</div>
<div class="col-auto pl-1">
<button type="reset" class="btn btn-secondary" (click)="cancelAddWorkflow()">Cancel</button>
</div>
</div>
</form>
</div>
</ng-container> </ng-container>
<button class="btn btn-success" (click)="addStep()" *ngIf="workflow.canUpdate">
Add Step
</button>
</ng-container> </ng-container>
</ng-container> </ng-container>

78
src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts

@ -6,17 +6,14 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { import {
AddWorkflowForm,
AppsState, AppsState,
MathHelper,
RolesState, RolesState,
WorkflowDto, WorkflowDto,
WorkflowsState, WorkflowsState
WorkflowStep,
WorkflowStepValues,
WorkflowTransition,
WorkflowTransitionValues
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -25,78 +22,45 @@ import {
templateUrl: './workflows-page.component.html' templateUrl: './workflows-page.component.html'
}) })
export class WorkflowsPageComponent implements OnInit { export class WorkflowsPageComponent implements OnInit {
public workflow: WorkflowDto; public addWorkflowForm = new AddWorkflowForm(this.formBuilder);
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly rolesState: RolesState, public readonly rolesState: RolesState,
public readonly workflowsState: WorkflowsState public readonly workflowsState: WorkflowsState,
private readonly formBuilder: FormBuilder
) { ) {
} }
public ngOnInit() { public ngOnInit() {
this.workflowsState.load() this.workflowsState.load();
.subscribe(workflow => {
this.workflow = workflow;
});
this.rolesState.load(); this.rolesState.load();
} }
public reload() { public reload() {
this.workflowsState.load(true) this.workflowsState.load(true);
.subscribe(workflow => {
this.workflow = workflow;
});
} }
public save() { public addWorkflow() {
this.workflowsState.save(this.workflow); const value = this.addWorkflowForm.submit();
}
public addStep() {
let index = this.workflow.steps.length;
for (let i = index; i < index + 100; i++) { if (value) {
const name = `Step${i}`; this.workflowsState.add(value.name)
.subscribe(() => {
if (!this.workflow.getStep(name)) { this.addWorkflowForm.submitCompleted();
this.workflow = this.workflow.setStep(name, { color: MathHelper.randomColor() }); }, error => {
return; this.addWorkflowForm.submitFailed(error);
} });
} }
} }
public setInitial(step: WorkflowStep) { public cancelAddWorkflow() {
this.workflow = this.workflow.setInitial(step.name); this.addWorkflowForm.submitCompleted();
}
public addTransiton(from: WorkflowStep, to: WorkflowStep) {
this.workflow = this.workflow.setTransition(from.name, to.name, {});
}
public removeTransition(from: WorkflowStep, transition: WorkflowTransition) {
this.workflow = this.workflow.removeTransition(from.name, transition.to);
}
public updateTransition(update: { transition: WorkflowTransition, values: WorkflowTransitionValues }) {
this.workflow = this.workflow.setTransition(update.transition.from, update.transition.to, update.values);
}
public updateStep(step: WorkflowStep, values: WorkflowStepValues) {
this.workflow = this.workflow.setStep(step.name, values);
}
public renameStep(step: WorkflowStep, newName: string) {
this.workflow = this.workflow.renameStep(step.name, newName);
}
public removeStep(step: WorkflowStep) {
this.workflow = this.workflow.removeStep(step.name);
} }
public trackByStep(index: number, step: WorkflowStep) { public trackByWorkflow(index: number, workflow: WorkflowDto) {
return step.name; return workflow.id;
} }
} }

2
src/Squidex/app/features/settings/settings-area.component.html

@ -45,7 +45,7 @@
</li> </li>
<li class="nav-item" *ngIf="selectedApp.canReadWorkflows"> <li class="nav-item" *ngIf="selectedApp.canReadWorkflows">
<a class="nav-link" routerLink="workflows" routerLinkActive="active"> <a class="nav-link" routerLink="workflows" routerLinkActive="active">
Workflow Workflows
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>

1
src/Squidex/app/shared/internal.ts

@ -63,6 +63,7 @@ export * from './state/rules.state';
export * from './state/schemas.forms'; export * from './state/schemas.forms';
export * from './state/schemas.state'; export * from './state/schemas.state';
export * from './state/ui.state'; export * from './state/ui.state';
export * from './state/workflows.forms';
export * from './state/workflows.state'; export * from './state/workflows.state';
export * from './utils/messages'; export * from './utils/messages';

117
src/Squidex/app/shared/services/workflows.service.spec.ts

@ -49,7 +49,7 @@ describe('WorkflowsService', () => {
workflows = result; workflows = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflows');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
@ -64,7 +64,7 @@ describe('WorkflowsService', () => {
expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') }); expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') });
})); }));
it('should make a put request to create a workflow', it('should make a post request to create a workflow',
inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => {
let workflows: WorkflowsDto; let workflows: WorkflowsDto;
@ -73,7 +73,7 @@ describe('WorkflowsService', () => {
workflows = result; workflows = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow/123'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflows');
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
@ -92,7 +92,7 @@ describe('WorkflowsService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
update: { method: 'PUT', href: '/api/apps/my-app/workflow/123' } update: { method: 'PUT', href: '/api/apps/my-app/workflows/123' }
} }
}; };
@ -102,7 +102,7 @@ describe('WorkflowsService', () => {
workflows = result; workflows = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow/123'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflows/123');
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
@ -121,7 +121,7 @@ describe('WorkflowsService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
delete: { method: 'DELETE', href: '/api/apps/my-app/workflow/123' } delete: { method: 'DELETE', href: '/api/apps/my-app/workflows/123' }
} }
}; };
@ -131,9 +131,9 @@ describe('WorkflowsService', () => {
workflows = result; workflows = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflows/123');
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(workflowsResponse('1', '2'), { req.flush(workflowsResponse('1', '2'), {
@ -156,32 +156,28 @@ describe('WorkflowsService', () => {
function workflowResponse(name: string) { function workflowResponse(name: string) {
return { return {
workflow: { name: `name_${name}`, id: `id_${name}`, initial: `${name}1`,
steps: { steps: {
[`${name}1`]: { [`${name}1`]: {
transitions: { transitions: {
[`${name}2`]: { [`${name}2`]: {
expression: 'Expression1', role: 'Role1' expression: 'Expression1', role: 'Role1'
} }
},
color: `${name}1`, noUpdate: true
}, },
[`${name}2`]: { color: `${name}1`, noUpdate: true
transitions: {
[`${name}1`]: {
expression: 'Expression2', role: 'Role2'
}
},
color: `${name}2`, noUpdate: true
}
}, },
initial: `${name}1`, [`${name}2`]: {
_links: { transitions: {
update: { method: 'PUT', href: '/api/workflows' } [`${name}1`]: {
expression: 'Expression2', role: 'Role2'
}
},
color: `${name}2`, noUpdate: true
} }
}, },
_links: {}, _links: {
canCreate: true update: { method: 'PUT', href: `/workflows/${name}` }
}
}; };
} }
}); });
@ -198,9 +194,9 @@ export function createWorkflows(...names: string[]): WorkflowsPayload {
export function createWorkflow(name: string): WorkflowDto { export function createWorkflow(name: string): WorkflowDto {
return new WorkflowDto({ return new WorkflowDto({
update: { method: 'PUT', href: '/workflows' } update: { method: 'PUT', href: `/workflows/${name}` }
}, },
`${name}1`, `id_${name}`, `name_${name}`, `${name}1`,
[ [
{ name: `${name}1`, color: `${name}1`, noUpdate: true, isLocked: false }, { name: `${name}1`, color: `${name}1`, noUpdate: true, isLocked: false },
{ name: `${name}2`, color: `${name}2`, noUpdate: true, isLocked: false } { name: `${name}2`, color: `${name}2`, noUpdate: true, isLocked: false }
@ -213,17 +209,18 @@ export function createWorkflow(name: string): WorkflowDto {
describe('Workflow', () => { describe('Workflow', () => {
it('should create empty workflow', () => { it('should create empty workflow', () => {
const workflow = new WorkflowDto(); const workflow = new WorkflowDto({}, 'id');
expect(workflow.initial); expect(workflow.initial);
}); });
it('should add step to workflow', () => { it('should add step to workflow', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1', { color: '#00ff00' }); .setStep('1', { color: '#00ff00' });
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'1': { transitions: {}, color: '#00ff00' } '1': { transitions: {}, color: '#00ff00' }
}, },
@ -233,11 +230,12 @@ describe('Workflow', () => {
it('should override settings if step already exists', () => { it('should override settings if step already exists', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1', { color: '#00ff00', noUpdate: true }) .setStep('1', { color: '#00ff00', noUpdate: true })
.setStep('1', { color: 'red' }); .setStep('1', { color: 'red' });
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'1': { transitions: {}, color: 'red', noUpdate: true } '1': { transitions: {}, color: 'red', noUpdate: true }
}, },
@ -247,7 +245,7 @@ describe('Workflow', () => {
it('should return same workflow if step to update is locked', () => { it('should return same workflow if step to update is locked', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1', { color: '#00ff00', isLocked: true }); .setStep('1', { color: '#00ff00', isLocked: true });
const updated = workflow.setStep('1', { color: 'red' }); const updated = workflow.setStep('1', { color: 'red' });
@ -257,7 +255,7 @@ describe('Workflow', () => {
it('should sort steps case invariant', () => { it('should sort steps case invariant', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('Z') .setStep('Z')
.setStep('a'); .setStep('a');
@ -269,7 +267,7 @@ describe('Workflow', () => {
it('should return same workflow if step to remove is locked', () => { it('should return same workflow if step to remove is locked', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1', { color: '#00ff00', isLocked: true }); .setStep('1', { color: '#00ff00', isLocked: true });
const updated = workflow.removeStep('1'); const updated = workflow.removeStep('1');
@ -279,7 +277,7 @@ describe('Workflow', () => {
it('should return same workflow if step to remove not found', () => { it('should return same workflow if step to remove not found', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1'); .setStep('1');
const updated = workflow.removeStep('3'); const updated = workflow.removeStep('3');
@ -289,7 +287,7 @@ describe('Workflow', () => {
it('should remove step', () => { it('should remove step', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1', { color: '#00ff00' }) .setStep('1', { color: '#00ff00' })
.setStep('2', { color: '#ff0000' }) .setStep('2', { color: '#ff0000' })
.setStep('3', { color: '#0000ff' }) .setStep('3', { color: '#0000ff' })
@ -299,6 +297,7 @@ describe('Workflow', () => {
.removeStep('1'); .removeStep('1');
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'2': { '2': {
transitions: { transitions: {
@ -314,13 +313,14 @@ describe('Workflow', () => {
it('should make first non-locked step the initial step if initial removed', () => { it('should make first non-locked step the initial step if initial removed', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2', { isLocked: true }) .setStep('2', { isLocked: true })
.setStep('3') .setStep('3')
.removeStep('1'); .removeStep('1');
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'2': { transitions: {}, isLocked: true }, '2': { transitions: {}, isLocked: true },
'3': { transitions: {} } '3': { transitions: {} }
@ -331,16 +331,16 @@ describe('Workflow', () => {
it('should unset initial step if initial removed', () => { it('should unset initial step if initial removed', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.removeStep('1'); .removeStep('1');
expect(workflow.serialize()).toEqual({ steps: {}, initial: undefined }); expect(workflow.serialize()).toEqual({ name: null, steps: {}, initial: null });
}); });
it('should rename step', () => { it('should rename step', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1', { color: '#00ff00' }) .setStep('1', { color: '#00ff00' })
.setStep('2', { color: '#ff0000' }) .setStep('2', { color: '#ff0000' })
.setStep('3', { color: '#0000ff' }) .setStep('3', { color: '#0000ff' })
@ -350,6 +350,7 @@ describe('Workflow', () => {
.renameStep('1', 'a'); .renameStep('1', 'a');
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'a': { 'a': {
transitions: { transitions: {
@ -372,13 +373,14 @@ describe('Workflow', () => {
it('should add transitions to workflow', () => { it('should add transitions to workflow', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setTransition('1', '2', { expression: '1 === 2' }) .setTransition('1', '2', { expression: '1 === 2' })
.setTransition('2', '1', { expression: '2 === 1' }); .setTransition('2', '1', { expression: '2 === 1' });
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'1': { '1': {
transitions: { transitions: {
@ -397,7 +399,7 @@ describe('Workflow', () => {
it('should remove transition from workflow', () => { it('should remove transition from workflow', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setTransition('1', '2', { expression: '1 === 2' }) .setTransition('1', '2', { expression: '1 === 2' })
@ -405,6 +407,7 @@ describe('Workflow', () => {
.removeTransition('1', '2'); .removeTransition('1', '2');
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'1': { transitions: {}}, '1': { transitions: {}},
'2': { '2': {
@ -419,13 +422,14 @@ describe('Workflow', () => {
it('should override settings if transition already exists', () => { it('should override settings if transition already exists', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setTransition('2', '1', { expression: '2 === 1', role: 'Role' }) .setTransition('2', '1', { expression: '2 === 1', role: 'Role' })
.setTransition('2', '1', { expression: '2 !== 1' }); .setTransition('2', '1', { expression: '2 !== 1' });
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'1': { transitions: {} }, '1': { transitions: {} },
'2': { '2': {
@ -440,7 +444,7 @@ describe('Workflow', () => {
it('should return same workflow if transition to update not found by from step', () => { it('should return same workflow if transition to update not found by from step', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setTransition('1', '2'); .setTransition('1', '2');
@ -452,7 +456,7 @@ describe('Workflow', () => {
it('should return same workflow if transition to update not found by to step', () => { it('should return same workflow if transition to update not found by to step', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setTransition('1', '2'); .setTransition('1', '2');
@ -464,7 +468,7 @@ describe('Workflow', () => {
it('should return same workflow if transition to remove not', () => { it('should return same workflow if transition to remove not', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setTransition('1', '2'); .setTransition('1', '2');
@ -476,7 +480,7 @@ describe('Workflow', () => {
it('should return same workflow if step to make initial is locked', () => { it('should return same workflow if step to make initial is locked', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2', { color: '#00ff00', isLocked: true }); .setStep('2', { color: '#00ff00', isLocked: true });
@ -487,12 +491,13 @@ describe('Workflow', () => {
it('should set initial step', () => { it('should set initial step', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto({}, 'id')
.setStep('1') .setStep('1')
.setStep('2') .setStep('2')
.setInitial('2'); .setInitial('2');
expect(workflow.serialize()).toEqual({ expect(workflow.serialize()).toEqual({
name: null,
steps: { steps: {
'1': { transitions: {} }, '1': { transitions: {} },
'2': { transitions: {} } '2': { transitions: {} }
@ -501,4 +506,12 @@ describe('Workflow', () => {
}); });
}); });
it('should rename workflow', () => {
const workflow =
new WorkflowDto({}, 'id')
.rename('name');
expect(workflow.serialize()).toEqual({ name: 'name', steps: {}, initial: null });
});
}); });

51
src/Squidex/app/shared/services/workflows.service.ts

@ -20,6 +20,8 @@ import {
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
StringHelper,
Types,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
@ -35,9 +37,12 @@ export class WorkflowDto {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
public readonly canUpdate: boolean; public readonly canUpdate: boolean;
public readonly canDelete: boolean;
public readonly displayName: string;
public static DEFAULT = public static DEFAULT =
new WorkflowDto() new WorkflowDto({}, 'id', 'name')
.setStep('Draft', { color: '#8091a5' }) .setStep('Draft', { color: '#8091a5' })
.setStep('Archived', { color: '#eb3142', noUpdate: true }) .setStep('Archived', { color: '#eb3142', noUpdate: true })
.setStep('Published', { color: '#4bb958', isLocked: true }) .setStep('Published', { color: '#4bb958', isLocked: true })
@ -47,8 +52,11 @@ export class WorkflowDto {
.setTransition('Published', 'Draft') .setTransition('Published', 'Draft')
.setTransition('Published', 'Archived'); .setTransition('Published', 'Archived');
constructor(links: ResourceLinks = {}, constructor(
public readonly initial?: string, links: ResourceLinks = {},
public readonly id: string,
public readonly name: string | null = null,
public readonly initial: string | null = null,
public readonly steps: WorkflowStep[] = [], public readonly steps: WorkflowStep[] = [],
private readonly transitions: WorkflowTransition[] = [] private readonly transitions: WorkflowTransition[] = []
) { ) {
@ -59,6 +67,9 @@ export class WorkflowDto {
this._links = links; this._links = links;
this.canUpdate = hasAnyLink(links, 'update'); this.canUpdate = hasAnyLink(links, 'update');
this.canDelete = hasAnyLink(links, 'delete');
this.displayName = StringHelper.firstNonEmpty(name, 'Unnamed Workflow');
} }
public getOpenSteps(step: WorkflowStep) { public getOpenSteps(step: WorkflowStep) {
@ -94,7 +105,7 @@ export class WorkflowDto {
initial = steps[0].name; initial = steps[0].name;
} }
return new WorkflowDto(this._links, initial, steps, this.transitions); return this.createNew({ initial, steps });
} }
public setInitial(initial: string) { public setInitial(initial: string) {
@ -104,7 +115,7 @@ export class WorkflowDto {
return this; return this;
} }
return new WorkflowDto(this._links, initial, this.steps, this.transitions); return this.createNew({ initial });
} }
public removeStep(name: string) { public removeStep(name: string) {
@ -124,10 +135,14 @@ export class WorkflowDto {
if (initial === name) { if (initial === name) {
const first = steps.find(x => !x.isLocked); const first = steps.find(x => !x.isLocked);
initial = first ? first.name : undefined; initial = first ? first.name : null;
} }
return new WorkflowDto(this._links, initial, steps, transitions); return this.createNew({ initial, steps, transitions });
}
public rename(name: string) {
return this.createNew({ name });
} }
public renameStep(name: string, newName: string) { public renameStep(name: string, newName: string) {
@ -163,7 +178,7 @@ export class WorkflowDto {
initial = newName; initial = newName;
} }
return new WorkflowDto(this._links, initial, steps, transitions); return this.createNew({ initial, steps, transitions });
} }
public removeTransition(from: string, to: string) { public removeTransition(from: string, to: string) {
@ -173,7 +188,7 @@ export class WorkflowDto {
return this; return this;
} }
return new WorkflowDto(this._links, this.initial, this.steps, transitions); return this.createNew({ transitions });
} }
public setTransition(from: string, to: string, values: Partial<WorkflowTransitionValues> = {}) { public setTransition(from: string, to: string, values: Partial<WorkflowTransitionValues> = {}) {
@ -199,11 +214,11 @@ export class WorkflowDto {
const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }]; const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }];
return new WorkflowDto(this._links, this.initial, this.steps, transitions); return this.createNew({ transitions });
} }
public serialize(): any { public serialize(): any {
const result = { steps: {}, initial: this.initial }; const result = { steps: {}, initial: this.initial, name: this.name };
for (let step of this.steps) { for (let step of this.steps) {
const { name, ...values } = step; const { name, ...values } = step;
@ -221,6 +236,14 @@ export class WorkflowDto {
return result; return result;
} }
private createNew(values: { steps?: WorkflowStep[], transitions?: WorkflowTransition[], initial?: string | null, name?: string | null }) {
return new WorkflowDto(this._links, this.id,
Types.isUndefined(values.name) ? this.name : values.name,
Types.isUndefined(values.initial) ? this.initial : values.initial,
values.steps || this.steps,
values.transitions || this.transitions);
}
} }
export type WorkflowStepValues = { color?: string; isLocked?: boolean; noUpdate?: boolean; }; export type WorkflowStepValues = { color?: string; isLocked?: boolean; noUpdate?: boolean; };
@ -245,7 +268,7 @@ export class WorkflowsService {
} }
public getWorkflows(appName: string): Observable<WorkflowsDto> { public getWorkflows(appName: string): Observable<WorkflowsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflows`);
return HTTP.getVersioned(this.http, url).pipe( return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
@ -255,7 +278,7 @@ export class WorkflowsService {
} }
public postWorkflow(appName: string, dto: CreateWorkflowDto, version: Version): Observable<WorkflowsDto> { public postWorkflow(appName: string, dto: CreateWorkflowDto, version: Version): Observable<WorkflowsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflows`);
return HTTP.postVersioned(this.http, url, dto, version).pipe( return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
@ -329,5 +352,5 @@ function parseWorkflow(workflow: any) {
} }
} }
return new WorkflowDto(workflow._links, workflow.initial, steps, transitions); return new WorkflowDto(workflow._links, workflow.id, workflow.name, workflow.initial, steps, transitions);
} }

2
src/Squidex/app/shared/state/clients.forms.ts

@ -25,7 +25,7 @@ export class RenameClientForm extends Form<FormGroup, { name: string }> {
} }
} }
export class AttachClientForm extends Form<FormGroup, { name: string}> { export class AddClientForm extends Form<FormGroup, { name: string}> {
public hasNoName = hasNoValue$(this.form.controls['name']); public hasNoName = hasNoValue$(this.form.controls['name']);
constructor(formBuilder: FormBuilder) { constructor(formBuilder: FormBuilder) {

24
src/Squidex/app/shared/state/workflows.forms.ts

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, hasNoValue$ } from '@app/framework';
export class AddWorkflowForm extends Form<FormGroup, { name: string}> {
public hasNoName = hasNoValue$(this.form.controls['name']);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
Validators.required
]
]
}));
}
}

52
src/Squidex/app/shared/state/workflows.state.spec.ts

@ -11,12 +11,12 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
versioned, versioned,
WorkflowPayload, WorkflowsPayload,
WorkflowsService, WorkflowsService,
WorkflowsState WorkflowsState
} from '@app/shared/internal'; } from '@app/shared/internal';
import { createWorkflow } from '../services/workflows.service.spec'; import { createWorkflows } from '../services/workflows.service.spec';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -28,7 +28,7 @@ describe('WorkflowsState', () => {
version version
} = TestValues; } = TestValues;
const oldWorkflow = createWorkflow('test'); const oldWorkflows = createWorkflows('1', '2');
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let workflowsService: IMock<WorkflowsService>; let workflowsService: IMock<WorkflowsService>;
@ -48,11 +48,11 @@ describe('WorkflowsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load workflow', () => { it('should load workflow', () => {
workflowsService.setup(x => x.getWorkflows(app)) workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable(); .returns(() => of(versioned(version, oldWorkflows))).verifiable();
workflowsState.load().subscribe(); workflowsState.load().subscribe();
expect(workflowsState.snapshot.workflow).toEqual(oldWorkflow.workflow); expect(workflowsState.snapshot.workflows.values).toEqual(oldWorkflows.items);
expect(workflowsState.snapshot.isLoaded).toBeTruthy(); expect(workflowsState.snapshot.isLoaded).toBeTruthy();
expect(workflowsState.snapshot.version).toEqual(version); expect(workflowsState.snapshot.version).toEqual(version);
@ -61,7 +61,7 @@ describe('WorkflowsState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
workflowsService.setup(x => x.getWorkflows(app)) workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable(); .returns(() => of(versioned(version, oldWorkflows))).verifiable();
workflowsState.load(true).subscribe(); workflowsState.load(true).subscribe();
@ -74,28 +74,50 @@ describe('WorkflowsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
workflowsService.setup(x => x.getWorkflows(app)) workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable(); .returns(() => of(versioned(version, oldWorkflows))).verifiable();
workflowsState.load().subscribe(); workflowsState.load().subscribe();
}); });
it('should update workflows when saved', () => { it('should update workflows when workflow added', () => {
const updated = createWorkflow('updated'); const updated = createWorkflows('1', '2', '3');
const request = oldWorkflow.workflow.serialize(); workflowsService.setup(x => x.postWorkflow(app, { name: 'my-workflow' }, version))
workflowsService.setup(x => x.putWorkflow(app, oldWorkflow.workflow, request, version))
.returns(() => of(versioned(newVersion, updated))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
workflowsState.save(oldWorkflow.workflow).subscribe(); workflowsState.add('my-workflow' ).subscribe();
expectNewWorkflows(updated); expectNewWorkflows(updated);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
}); });
function expectNewWorkflows(updated: WorkflowPayload) { it('should update workflows when workflow updated', () => {
expect(workflowsState.snapshot.workflow).toEqual(updated.workflow); const updated = createWorkflows('1', '2', '3');
const request = oldWorkflows.items[0].serialize();
workflowsService.setup(x => x.putWorkflow(app, oldWorkflows.items[0], request, version))
.returns(() => of(versioned(newVersion, updated))).verifiable();
workflowsState.update(oldWorkflows.items[0]).subscribe();
expectNewWorkflows(updated);
});
it('should update workflows when workflow deleted', () => {
const updated = createWorkflows('1', '2', '3');
workflowsService.setup(x => x.deleteWorkflow(app, oldWorkflows.items[0], version))
.returns(() => of(versioned(newVersion, updated))).verifiable();
workflowsState.delete(oldWorkflows.items[0]).subscribe();
expectNewWorkflows(updated);
});
function expectNewWorkflows(updated: WorkflowsPayload) {
expect(workflowsState.snapshot.workflows.values).toEqual(updated.items);
expect(workflowsState.snapshot.version).toEqual(newVersion); expect(workflowsState.snapshot.version).toEqual(newVersion);
} }
}); });

56
src/Squidex/app/shared/state/workflows.state.ts

@ -13,7 +13,7 @@ import { tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
shareMapSubscribed, ImmutableArray,
shareSubscribed, shareSubscribed,
State, State,
Version Version
@ -23,38 +23,44 @@ import { AppsState } from './apps.state';
import { import {
WorkflowDto, WorkflowDto,
WorkflowPayload, WorkflowsPayload,
WorkflowsService WorkflowsService
} from './../services/workflows.service'; } from './../services/workflows.service';
interface Snapshot { interface Snapshot {
// The current workflow. // The current workflow.
workflow?: WorkflowDto; workflows: ImmutableArray<WorkflowDto>;
// The app version. // The app version.
version: Version; version: Version;
// Indicates if the workflows are loaded. // Indicates if the workflows are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// Indicates if the user can create new workflow.
canCreate?: boolean;
} }
@Injectable() @Injectable()
export class WorkflowsState extends State<Snapshot> { export class WorkflowsState extends State<Snapshot> {
public workflow = public workflows =
this.project(x => x.workflow); this.project(x => x.workflows);
public isLoaded = public isLoaded =
this.project(x => !!x.isLoaded); this.project(x => !!x.isLoaded);
public canCreate =
this.project(x => !!x.canCreate);
constructor( constructor(
private readonly workflowsService: WorkflowsService, private readonly workflowsService: WorkflowsService,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ version: Version.EMPTY }); super({ workflows: ImmutableArray.empty(), version: Version.EMPTY });
} }
public load(isReload = false): Observable<WorkflowDto> { public load(isReload = false): Observable<any> {
if (!isReload) { if (!isReload) {
this.resetState(); this.resetState();
} }
@ -62,29 +68,47 @@ export class WorkflowsState extends State<Snapshot> {
return this.workflowsService.getWorkflows(this.appName).pipe( return this.workflowsService.getWorkflows(this.appName).pipe(
tap(({ version, payload }) => { tap(({ version, payload }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Workflow reloaded.'); this.dialogs.notifyInfo('Workflows reloaded.');
} }
this.replaceWorkflow(payload, version); this.replaceWorkflows(payload, version);
}), }),
shareMapSubscribed(this.dialogs, x => x.payload.workflow)); shareSubscribed(this.dialogs));
} }
public save(workflow: WorkflowDto): Observable<any> { public add(name: string): Observable<any> {
return this.workflowsService.putWorkflow(this.appName, workflow, workflow.serialize(), this.version).pipe( return this.workflowsService.postWorkflow(this.appName, { name }, this.version).pipe(
tap(({ version, payload }) => { tap(({ version, payload }) => {
this.replaceWorkflow(payload, version); this.replaceWorkflows(payload, version);
}),
shareSubscribed(this.dialogs));
}
public update(workflow: WorkflowDto): Observable<any> {
return this.workflowsService.putWorkflow(this.appName, workflow, workflow.serialize(), this.version).pipe(
tap(({ version, payload }) => {
this.dialogs.notifyInfo('Workflow has been saved.'); this.dialogs.notifyInfo('Workflow has been saved.');
this.replaceWorkflows(payload, version);
}),
shareSubscribed(this.dialogs));
}
public delete(workflow: WorkflowDto): Observable<any> {
return this.workflowsService.deleteWorkflow(this.appName, workflow, this.version).pipe(
tap(({ version, payload }) => {
this.replaceWorkflows(payload, version);
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
private replaceWorkflow(payload: WorkflowPayload, version: Version) { private replaceWorkflows(payload: WorkflowsPayload, version: Version) {
const { workflow } = payload; const { canCreate, items } = payload;
const workflows = ImmutableArray.of(items);
this.next(s => { this.next(s => {
return { ...s, workflow, isLoaded: true, version }; return { ...s, workflows, isLoaded: true, version, canCreate };
}); });
} }

Loading…
Cancel
Save