Browse Source

Schema ids.

pull/382/head
Sebastian 7 years ago
parent
commit
91bc2cddc1
  1. 30
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  2. 12
      src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs
  3. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs
  4. 15
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs
  5. 38
      src/Squidex/app/features/settings/pages/workflows/schema-tag-converter.ts
  6. 14
      src/Squidex/app/features/settings/pages/workflows/workflow.component.html
  7. 2
      src/Squidex/app/features/settings/pages/workflows/workflow.component.scss
  8. 5
      src/Squidex/app/features/settings/pages/workflows/workflow.component.ts
  9. 4
      src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html
  10. 18
      src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts
  11. 160
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  12. 31
      src/Squidex/app/shared/services/workflows.service.spec.ts
  13. 49
      src/Squidex/app/shared/services/workflows.service.ts
  14. 4
      src/Squidex/app/shared/state/workflows.state.spec.ts
  15. 4
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs
  16. 24
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs
  17. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs
  18. 37
      tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Contents
@ -13,26 +14,41 @@ namespace Squidex.Domain.Apps.Core.Contents
{
private const string DefaultName = "Unnamed";
private static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
private static readonly IReadOnlyList<Guid> EmptySchemaIds = new List<Guid>();
public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(EmptySteps, default);
public static readonly Workflow Empty = new Workflow(default, EmptySteps);
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; }
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps;
public IReadOnlyList<Guid> SchemaIds { get; } = EmptySchemaIds;
public Status Initial { get; }
public Workflow(IReadOnlyDictionary<Status, WorkflowStep> steps, Status initial, string name = null)
public Workflow(
Status initial,
IReadOnlyDictionary<Status, WorkflowStep> steps,
IReadOnlyList<Guid> schemaIds = null,
string name = null)
: base(name ?? DefaultName)
{
Steps = steps ?? EmptySteps;
Initial = initial;
if (steps != null)
{
Steps = steps;
}
if (schemaIds != null)
{
SchemaIds = schemaIds;
}
}
public static Workflow CreateDefault(string name = null)
{
return new Workflow(
new Dictionary<Status, WorkflowStep>
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Archived] =
new WorkflowStep(
@ -57,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Contents
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Published)
}, Status.Draft, name);
}, null, name);
}
public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status)

12
src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs

@ -36,6 +36,18 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
}
}
protected override JsonArrayContract CreateArrayContract(Type objectType)
{
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))
{
var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments());
return base.CreateArrayContract(implementationType);
}
return base.CreateArrayContract(objectType);
}
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -26,6 +27,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
public Dictionary<Status, WorkflowStepDto> Steps { get; set; }
/// <summary>
/// The schema ids.
/// </summary>
public List<Guid> SchemaIds { get; set; }
/// <summary>
/// The initial step.
/// </summary>
@ -34,6 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public UpdateWorkflow ToCommand()
{
var workflow = new Workflow(
Initial,
Steps?.ToDictionary(
x => x.Key,
x => new WorkflowStep(
@ -42,7 +49,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
y => new WorkflowTransition(y.Value.Expression, y.Value.Role)),
x.Value.Color,
x.Value.NoUpdate)),
Initial, Name);
SchemaIds,
Name);
return new UpdateWorkflow { Workflow = workflow };
}

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

@ -34,6 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
public Dictionary<Status, WorkflowStepDto> Steps { get; set; }
/// <summary>
/// The schema ids.
/// </summary>
public IReadOnlyList<Guid> SchemaIds { get; set; }
/// <summary>
/// The initial step.
/// </summary>
@ -41,18 +46,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static WorkflowDto FromWorkflow(Guid id, Workflow workflow, ApiController controller, string app)
{
var result = new WorkflowDto
{
Steps = workflow.Steps.ToDictionary(
var result = SimpleMapper.Map(workflow, new WorkflowDto { Id = id });
result.Steps = workflow.Steps.ToDictionary(
x => x.Key,
x => SimpleMapper.Map(x.Value, new WorkflowStepDto
{
Transitions = x.Value.Transitions.ToDictionary(
y => y.Key,
y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role })
})),
Id = id, Name = workflow.Name, Initial = workflow.Initial
};
}));
return result.CreateLinks(controller, app, id);
}

38
src/Squidex/app/features/settings/pages/workflows/schema-tag-converter.ts

@ -0,0 +1,38 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Converter, SchemaDto, TagValue } from '@app/shared';
export class SchemaTagConverter implements Converter {
public readonly suggestions: TagValue[];
constructor(
private readonly schemas: SchemaDto[]
) {
this.suggestions = schemas.map(x => new TagValue(x.id, x.name, x.id));
}
public convertInput(input: string): TagValue<any> | null {
const schema = this.schemas.find(x => x.name === input);
if (schema) {
return new TagValue(schema.id, schema.name, schema.id);
}
return null;
}
public convertValue(value: any): TagValue<any> | null {
const schema = this.schemas.find(x => x.id === value);
if (schema) {
return new TagValue(schema.id, schema.name, schema.id);
}
return null;
}
}

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

@ -36,7 +36,7 @@
<div class="form-group row">
<label class="col-form-label" for="{{workflow.id}}_name">Name</label>
<div class="col-6">
<div class="col">
<input class="form-control" id="{{workflow.id}}_name"
[ngModelOptions]="onBlur"
[ngModel]="workflow.name"
@ -48,6 +48,18 @@
</div>
</div>
<div class="form-group row">
<label class="col-form-label" for="{{workflow.id}}_name">Schemas</label>
<div class="col">
<sqx-tag-editor [converter]="schemasSource" placeholder=", to add schema" [suggestedValues]="schemasSource.suggestions"></sqx-tag-editor>
<sqx-form-hint>
Restrict this workflow to specific schemas or keep it empty for all schemas.
</sqx-form-hint>
</div>
</div>
<sqx-workflow-step *ngFor="let step of workflow.steps; trackBy: trackByStep"
[workflow]="workflow"
[disabled]="!workflow.canUpdate"

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

@ -10,13 +10,11 @@
.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 {

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

@ -19,6 +19,8 @@ import {
WorkflowTransitionValues
} from '@app/shared';
import { SchemaTagConverter } from './schema-tag-converter';
@Component({
selector: 'sqx-workflow',
styleUrls: ['./workflow.component.scss'],
@ -31,6 +33,9 @@ export class WorkflowComponent implements OnChanges {
@Input()
public roles: RoleDto[];
@Input()
public schemasSource: SchemaTagConverter;
public error: ErrorDto | null;
public onBlur = { updateOn: 'blur' };

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

@ -14,14 +14,14 @@
</ng-container>
<ng-container content>
<ng-container *ngIf="workflowsState.workflows | async; let workflows">
<ng-container *ngIf="schemasSource && workflowsState.workflows | async; let workflows">
<ng-container *ngIf="rolesState.roles | async; let roles">
<div class="table-items-row table-items-row-empty" *ngIf="workflows.length === 0">
No workflows created yet.
</div>
<sqx-workflow *ngFor="let workflow of workflows; trackBy: trackByWorkflow"
[workflow]="workflow" [roles]="roles">
[workflow]="workflow" [roles]="roles" [schemasSource]="schemasSource">
</sqx-workflow>
<div class="table-items-footer" *ngIf="workflowsState.canCreate | async">

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

@ -11,31 +11,45 @@ import { FormBuilder } from '@angular/forms';
import {
AddWorkflowForm,
AppsState,
ResourceOwner,
RolesState,
SchemasState,
WorkflowDto,
WorkflowsState
} from '@app/shared';
import { SchemaTagConverter } from './schema-tag-converter';
@Component({
selector: 'sqx-workflows-page',
styleUrls: ['./workflows-page.component.scss'],
templateUrl: './workflows-page.component.html'
})
export class WorkflowsPageComponent implements OnInit {
export class WorkflowsPageComponent extends ResourceOwner implements OnInit {
public addWorkflowForm = new AddWorkflowForm(this.formBuilder);
public schemasSource: SchemaTagConverter;
constructor(
public readonly appsState: AppsState,
public readonly rolesState: RolesState,
public readonly schemasState: SchemasState,
public readonly workflowsState: WorkflowsState,
private readonly formBuilder: FormBuilder
) {
super();
}
public ngOnInit() {
this.workflowsState.load();
this.own(this.schemasState.changes.subscribe(s => {
if (s.isLoaded) {
this.schemasSource = new SchemaTagConverter(s.schemas.values);
}
}));
this.rolesState.load();
this.schemasState.load();
this.workflowsState.load();
}
public reload() {

160
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -13,52 +13,103 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { Keys, StatefulControlComponent, Types } from '@app/framework/internal';
export const CONVERSION_FAILED = {};
export class TagValue<T = any> {
public readonly lowerCaseName: string;
constructor(
public readonly id: any,
public readonly name: string,
public readonly value: T
) {
this.lowerCaseName = name.toLowerCase();
}
public toString() {
return this.name;
}
}
export interface Converter {
convert(input: string): any;
convertInput(input: string): TagValue | null;
isValidInput(input: string): boolean;
isValidValue(value: any): boolean;
convertValue(value: any): TagValue | null;
}
export class IntConverter implements Converter {
public isValidInput(input: string): boolean {
return !!parseInt(input, 10) || input === '0';
private static ZERO = new TagValue(0, '0', 0);
public convertInput(input: string): TagValue<number> | null {
if (input === '0') {
return IntConverter.ZERO;
}
const parsed = parseInt(input, 10);
if (parsed) {
return new TagValue(parsed, input, parsed);
}
return null;
}
public isValidValue(value: any): boolean {
return Types.isNumber(value);
public convertValue(value: any): TagValue<number> | null {
if (Types.isNumber(value)) {
return new TagValue(value, `${value}`, value);
}
public convert(input: string): any {
return parseInt(input, 10) || 0;
return null;
}
}
export class FloatConverter implements Converter {
public isValidInput(input: string): boolean {
return !!parseFloat(input) || input === '0';
private static ZERO = new TagValue(0, '0', 0);
public convertInput(input: string): TagValue<number> | null {
if (input === '0') {
return FloatConverter.ZERO;
}
const parsed = parseFloat(input);
if (parsed) {
return new TagValue(parsed, input, parsed);
}
return null;
}
public isValidValue(value: any): boolean {
return Types.isNumber(value);
public convertValue(value: any): TagValue<number> | null {
if (Types.isNumber(value)) {
return new TagValue(value, `${value}`, value);
}
public convert(input: string): any {
return parseFloat(input) || 0;
return null;
}
}
export class StringConverter implements Converter {
public isValidInput(input: string): boolean {
return input.trim().length > 0;
public convertInput(input: string): TagValue<string> | null {
if (input) {
const trimmed = input.trim();
if (trimmed.length > 0) {
return new TagValue(trimmed, trimmed, trimmed);
}
}
public isValidValue(value: any): boolean {
return Types.isString(value);
return null;
}
public convertValue(value: any): TagValue<string> | null {
if (Types.isString(value)) {
const trimmed = value.trim();
return new TagValue(trimmed, trimmed, trimmed);
}
public convert(input: string): any {
return input.trim();
return null;
}
}
@ -73,10 +124,10 @@ let CACHED_FONT: string;
interface State {
hasFocus: boolean;
suggestedItems: string[];
suggestedItems: TagValue[];
suggestedIndex: number;
items: any[];
items: TagValue[];
}
@Component({
@ -93,6 +144,9 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
@ViewChild('input', { static: false })
public inputElement: ElementRef<HTMLInputElement>;
@Input()
public suggestedValues: TagValue[] = [];
@Input()
public converter: Converter = new StringConverter();
@ -105,9 +159,6 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
@Input()
public allowDuplicates = true;
@Input()
public suggestions: string[] = [];
@Input()
public singleLine = false;
@ -120,6 +171,15 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
@Input()
public inputName = 'tag-editor';
@Input()
public set suggestions(value: string[]) {
if (value) {
this.suggestedValues = value.map(x => new TagValue(x, x, x));
} else {
this.suggestedValues = [];
}
}
@Input()
public set disabled(value: boolean) {
this.setDisabledState(value);
@ -161,8 +221,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}),
distinctUntilChanged(),
map(query => {
if (Types.isArray(this.suggestions) && query && query.length > 0) {
return this.suggestions.filter(s => s.toLowerCase().indexOf(query) >= 0 && this.snapshot.items.indexOf(s) < 0);
if (Types.isArray(this.suggestedValues) && query && query.length > 0) {
return this.suggestedValues.filter(s => s.lowerCaseName.indexOf(query) >= 0 && !this.snapshot.items.find(x => x.id === s.id));
} else {
return [];
}
@ -180,13 +240,25 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
this.resetForm();
this.resetSize();
if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) {
this.next(s => ({ ...s, items: obj }));
} else {
this.next(s => ({ ...s, items: [] }));
const items: any[] = [];
if (this.converter && Types.isArray(obj)) {
for (let value of obj) {
if (Types.is(value, TagValue)) {
items.push(value);
}
const converted = this.converter.convertValue(obj);
if (converted) {
items.push(value);
}
}
}
this.next(s => ({ ...s, items }));
}
public setDisabledState(isDisabled: boolean): void {
super.setDisabledState(isDisabled);
@ -296,16 +368,22 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
return true;
}
public selectValue(value: string, noFocus?: boolean) {
public selectValue(value: TagValue | string, noFocus?: boolean) {
if (!noFocus) {
this.inputElement.nativeElement.focus();
}
if (value && this.converter.isValidInput(value)) {
const converted = this.converter.convert(value);
let tagValue: TagValue | null;
if (Types.isString(value)) {
tagValue = this.converter.convertInput(value);
} else {
tagValue = value;
}
if (this.allowDuplicates || this.snapshot.items.indexOf(converted) < 0) {
this.updateItems([...this.snapshot.items, converted]);
if (tagValue) {
if (this.allowDuplicates || !this.snapshot.items.find(x => x.id === tagValue!.id)) {
this.updateItems([...this.snapshot.items, tagValue]);
}
this.resetForm();
@ -363,7 +441,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public onCopy(event: ClipboardEvent) {
if (!this.hasSelection()) {
if (event.clipboardData) {
event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(','));
event.clipboardData.setData('text/plain', this.snapshot.items.map(x => x.name).join(','));
}
event.preventDefault();
@ -380,7 +458,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
const values = [...this.snapshot.items];
for (let part of value.split(',')) {
const converted = this.converter.convert(part);
const converted = this.converter.convertInput(part);
if (converted) {
values.push(converted);
@ -401,13 +479,13 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
return s && e && (e - s) > 0;
}
private updateItems(items: any[]) {
private updateItems(items: TagValue[]) {
this.next(s => ({ ...s, items }));
if (items.length === 0 && this.undefinedWhenEmpty) {
this.callChange(undefined);
} else {
this.callChange(items);
this.callChange(items.map(x => x.value));
}
this.resetSize();

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

@ -156,7 +156,10 @@ describe('WorkflowsService', () => {
function workflowResponse(name: string) {
return {
name: `name_${name}`, id: `id_${name}`, initial: `${name}1`,
id: `id_${name}`,
name: `name_${name}`,
initial: `${name}1`,
schemaIds: [`schema_${name}`],
steps: {
[`${name}1`]: {
transitions: {
@ -193,10 +196,14 @@ export function createWorkflows(...names: string[]): WorkflowsPayload {
}
export function createWorkflow(name: string): WorkflowDto {
return new WorkflowDto({
return new WorkflowDto(
{
update: { method: 'PUT', href: `/workflows/${name}` }
},
`id_${name}`, `name_${name}`, `${name}1`,
[
`schema_${name}`
],
[
{ name: `${name}1`, color: `${name}1`, noUpdate: true, isLocked: false },
{ name: `${name}2`, color: `${name}2`, noUpdate: true, isLocked: false }
@ -221,6 +228,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'1': { transitions: {}, color: '#00ff00' }
},
@ -236,6 +244,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'1': { transitions: {}, color: 'red', noUpdate: true }
},
@ -298,6 +307,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'2': {
transitions: {
@ -321,6 +331,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'2': { transitions: {}, isLocked: true },
'3': { transitions: {} }
@ -335,7 +346,7 @@ describe('Workflow', () => {
.setStep('1')
.removeStep('1');
expect(workflow.serialize()).toEqual({ name: null, steps: {}, initial: null });
expect(workflow.serialize()).toEqual({ name: null, schemaIds: [], steps: {}, initial: null });
});
it('should rename step', () => {
@ -351,6 +362,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'a': {
transitions: {
@ -381,6 +393,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'1': {
transitions: {
@ -408,6 +421,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'1': { transitions: {}},
'2': {
@ -430,6 +444,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'1': { transitions: {} },
'2': {
@ -498,6 +513,7 @@ describe('Workflow', () => {
expect(workflow.serialize()).toEqual({
name: null,
schemaIds: [],
steps: {
'1': { transitions: {} },
'2': { transitions: {} }
@ -511,7 +527,14 @@ describe('Workflow', () => {
new WorkflowDto({}, 'id')
.rename('name');
expect(workflow.serialize()).toEqual({ name: 'name', steps: {}, initial: null });
expect(workflow.serialize()).toEqual({ name: 'name', schemaIds: [], steps: {}, initial: null });
});
it('should update schemaIds', () => {
const workflow =
new WorkflowDto({}, 'id')
.changeSchemaIds(['1', '2']);
expect(workflow.serialize()).toEqual({ name: null, schemaIds: ['1', '2'], steps: {}, initial: null });
});
});

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

@ -17,11 +17,11 @@ import {
hasAnyLink,
HTTP,
mapVersioned,
Model,
pretifyError,
Resource,
ResourceLinks,
StringHelper,
Types,
Version,
Versioned
} from '@app/framework';
@ -33,7 +33,7 @@ export type WorkflowsPayload = {
readonly canCreate: boolean;
} & Resource;
export class WorkflowDto {
export class WorkflowDto extends Model<WorkflowDto> {
public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
@ -57,12 +57,13 @@ export class WorkflowDto {
public readonly id: string,
public readonly name: string | null = null,
public readonly initial: string | null = null,
public readonly schemaIds: string[] = [],
public readonly steps: WorkflowStep[] = [],
private readonly transitions: WorkflowTransition[] = []
public readonly transitions: WorkflowTransition[] = []
) {
this.steps.sort((a, b) => compareStringsAsc(a.name, b.name));
super();
this.transitions.sort((a, b) => compareStringsAsc(a.to, b.to));
this.onCloned();
this._links = links;
@ -72,6 +73,12 @@ export class WorkflowDto {
this.displayName = StringHelper.firstNonEmpty(name, 'Unnamed Workflow');
}
protected onCloned() {
this.steps.sort((a, b) => compareStringsAsc(a.name, b.name));
this.transitions.sort((a, b) => compareStringsAsc(a.to, b.to));
}
public getOpenSteps(step: WorkflowStep) {
return this.steps.filter(x => x.name !== step.name && !this.transitions.find(y => y.from === step.name && y.to === x.name));
}
@ -105,7 +112,7 @@ export class WorkflowDto {
initial = steps[0].name;
}
return this.createNew({ initial, steps });
return this.with({ initial, steps });
}
public setInitial(initial: string) {
@ -115,7 +122,7 @@ export class WorkflowDto {
return this;
}
return this.createNew({ initial });
return this.with({ initial });
}
public removeStep(name: string) {
@ -138,11 +145,15 @@ export class WorkflowDto {
initial = first ? first.name : null;
}
return this.createNew({ initial, steps, transitions });
return this.with({ initial, steps, transitions });
}
public changeSchemaIds(schemaIds: string[]) {
return this.with({ schemaIds });
}
public rename(name: string) {
return this.createNew({ name });
return this.with({ name });
}
public renameStep(name: string, newName: string) {
@ -178,7 +189,7 @@ export class WorkflowDto {
initial = newName;
}
return this.createNew({ initial, steps, transitions });
return this.with({ initial, steps, transitions });
}
public removeTransition(from: string, to: string) {
@ -188,7 +199,7 @@ export class WorkflowDto {
return this;
}
return this.createNew({ transitions });
return this.with({ transitions });
}
public setTransition(from: string, to: string, values: Partial<WorkflowTransitionValues> = {}) {
@ -214,11 +225,11 @@ export class WorkflowDto {
const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }];
return this.createNew({ transitions });
return this.with({ transitions });
}
public serialize(): any {
const result = { steps: {}, initial: this.initial, name: this.name };
const result = { steps: {}, schemaIds: this.schemaIds, initial: this.initial, name: this.name };
for (let step of this.steps) {
const { name, ...values } = step;
@ -236,14 +247,6 @@ export class WorkflowDto {
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; };
@ -333,6 +336,8 @@ function parseWorkflows(response: any) {
}
function parseWorkflow(workflow: any) {
const { id, name, initial, schemaIds, _links } = workflow;
const steps: WorkflowStep[] = [];
const transitions: WorkflowTransition[] = [];
@ -352,5 +357,5 @@ function parseWorkflow(workflow: any) {
}
}
return new WorkflowDto(workflow._links, workflow.id, workflow.name, workflow.initial, steps, transitions);
return new WorkflowDto(_links, id, name, initial, schemaIds, steps, transitions);
}

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

@ -88,8 +88,6 @@ describe('WorkflowsState', () => {
workflowsState.add('my-workflow' ).subscribe();
expectNewWorkflows(updated);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should update workflows when workflow updated', () => {
@ -103,6 +101,8 @@ describe('WorkflowsState', () => {
workflowsState.update(oldWorkflows.items[0]).subscribe();
expectNewWorkflows(updated);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should update workflows when workflow deleted', () => {

4
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
public class WorkflowTests
{
private readonly Workflow workflow = new Workflow(
new Dictionary<Status, WorkflowStep>
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Draft] =
new WorkflowStep(
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
new WorkflowStep(),
[Status.Published] =
new WorkflowStep()
}, Status.Draft);
});
[Fact]
public void Should_provide_default_workflow_if_none_found()

24
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs

@ -69,11 +69,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdateWorkflow
{
Workflow = new Workflow(
default,
new Dictionary<Status, WorkflowStep>
{
[Status.Published] = new WorkflowStep()
},
default),
}),
WorkflowId = workflowId
};
@ -87,11 +87,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdateWorkflow
{
Workflow = new Workflow(
Status.Published,
new Dictionary<Status, WorkflowStep>
{
[Status.Published] = new WorkflowStep()
},
Status.Published),
}),
WorkflowId = workflowId
};
@ -105,11 +105,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdateWorkflow
{
Workflow = new Workflow(
Status.Draft,
new Dictionary<Status, WorkflowStep>
{
[Status.Draft] = new WorkflowStep()
},
Status.Draft),
}),
WorkflowId = workflowId
};
@ -123,12 +123,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdateWorkflow
{
Workflow = new Workflow(
Status.Draft,
new Dictionary<Status, WorkflowStep>
{
[Status.Published] = null,
[Status.Draft] = new WorkflowStep()
},
Status.Draft),
}),
WorkflowId = workflowId
};
@ -142,6 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdateWorkflow
{
Workflow = new Workflow(
Status.Draft,
new Dictionary<Status, WorkflowStep>
{
[Status.Published] =
@ -151,8 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Status.Archived] = new WorkflowTransition()
}),
[Status.Draft] = new WorkflowStep()
},
Status.Draft),
}),
WorkflowId = workflowId
};
@ -166,6 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdateWorkflow
{
Workflow = new Workflow(
Status.Draft,
new Dictionary<Status, WorkflowStep>
{
[Status.Draft] =
@ -176,8 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
[Status.Draft] = null
})
},
Status.Draft),
}),
WorkflowId = workflowId
};

4
tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs

@ -28,6 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly DynamicContentWorkflow sut;
private readonly Workflow workflow = new Workflow(
Status.Draft,
new Dictionary<Status, WorkflowStep>
{
[Status.Archived] =
@ -53,8 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Published)
},
Status.Draft);
});
public DynamicContentWorkflowTests()
{

37
tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyDictionaryTests.cs → tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs

@ -11,17 +11,17 @@ using Xunit;
namespace Squidex.Infrastructure.Json.Newtonsoft
{
public class ReadOnlyDictionaryTests
public class ReadOnlyCollectionTests
{
public sealed class MyClass
public sealed class MyClass<T>
{
public IReadOnlyDictionary<int, int> Values { get; set; }
public T Values { get; set; }
}
[Fact]
public void Should_serialize_and_deserialize_without_type_name()
public void Should_serialize_and_deserialize_dictionary_without_type_name()
{
var source = new MyClass
var source = new MyClass<IReadOnlyDictionary<int, int>>
{
Values = new Dictionary<int, int>
{
@ -37,7 +37,32 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
var json = JsonConvert.SerializeObject(source, serializerSettings);
var serialized = JsonConvert.DeserializeObject<MyClass>(json);
var serialized = JsonConvert.DeserializeObject<MyClass<IReadOnlyDictionary<int, int>>>(json);
Assert.DoesNotContain("$type", json);
Assert.Equal(2, serialized.Values.Count);
}
[Fact]
public void Should_serialize_and_deserialize_list_without_type_name()
{
var source = new MyClass<IReadOnlyList<int>>
{
Values = new List<int>
{
2,
3
}
};
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new ConverterContractResolver()
};
var json = JsonConvert.SerializeObject(source, serializerSettings);
var serialized = JsonConvert.DeserializeObject<MyClass<IReadOnlyList<int>>>(json);
Assert.DoesNotContain("$type", json);
Assert.Equal(2, serialized.Values.Count);
Loading…
Cancel
Save