mirror of https://github.com/Squidex/squidex.git
37 changed files with 725 additions and 169 deletions
@ -0,0 +1,59 @@ |
|||||
|
<div class="step"> |
||||
|
<div class="row no-gutters"> |
||||
|
<div class="col-auto color pr-2"> |
||||
|
<sqx-color-picker mode="Circle" |
||||
|
[ngModel]="step.color" |
||||
|
(ngModelChange)="changeColor($event)"> |
||||
|
</sqx-color-picker> |
||||
|
</div> |
||||
|
<div class="col"> |
||||
|
<sqx-editable-title |
||||
|
[name]="step.name" |
||||
|
(nameChanged)="changeName($event)" |
||||
|
[disabled]="step.isLocked"> |
||||
|
</sqx-editable-title> |
||||
|
</div> |
||||
|
<div class="col-auto"> |
||||
|
<button type="button" class="btn btn-text-danger" (click)="remove.emit()" *ngIf="!step.isLocked"> |
||||
|
<i class="icon-bin2"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row transition no-gutters"> |
||||
|
<div class="col-auto"> |
||||
|
<i class="icon-arrow-right text-decent"></i> |
||||
|
</div> |
||||
|
<div class="col-3 pl-2"> |
||||
|
<div class="color-circle" [style.background]="'red'"></div> In Progress |
||||
|
</div> |
||||
|
<div class="col pl-2"> |
||||
|
<input class="form-control" placeholder="Expression" /> |
||||
|
</div> |
||||
|
<div class="col-auto pl-2"> |
||||
|
<button type="button" class="btn btn-text-danger" (click)="remove.emit()"> |
||||
|
<i class="icon-bin2"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row transition no-gutters" *ngIf="openSteps.length > 0"> |
||||
|
<div class="col-auto"> |
||||
|
<i class="icon-arrow-right text-decent"></i> |
||||
|
</div> |
||||
|
<div class="col-3 pl-2"> |
||||
|
<sqx-dropdown [items]="openSteps"> |
||||
|
<ng-template let-target="$implicit"> |
||||
|
<span class="autocomplete-user"> |
||||
|
<div class="color-circle" [style.background]="target.color"></div> {{target.name}} |
||||
|
</span> |
||||
|
</ng-template> |
||||
|
</sqx-dropdown> |
||||
|
</div> |
||||
|
<div class="col pl-2"> |
||||
|
<button class="btn btn-success"> |
||||
|
Add Transition |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,28 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.color { |
||||
|
line-height: 2.8rem; |
||||
|
} |
||||
|
|
||||
|
.color-circle { |
||||
|
@include circle(12px); |
||||
|
border: 1px solid $color-border-dark; |
||||
|
background: $color-border; |
||||
|
display: inline-block; |
||||
|
} |
||||
|
|
||||
|
.transition { |
||||
|
& { |
||||
|
padding-left: 1rem; |
||||
|
margin-top: .25rem; |
||||
|
margin-bottom: .5rem; |
||||
|
line-height: 2rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.step { |
||||
|
& { |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; |
||||
|
|
||||
|
import { |
||||
|
WorkflowDto, |
||||
|
WorkflowStep, |
||||
|
WorkflowStepValues, |
||||
|
WorkflowTransition, |
||||
|
WorkflowTransitionView |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-workflow-step', |
||||
|
styleUrls: ['./workflow-step.component.scss'], |
||||
|
templateUrl: './workflow-step.component.html' |
||||
|
}) |
||||
|
export class WorkflowStepComponent implements OnChanges { |
||||
|
@Input() |
||||
|
public workflow: WorkflowDto; |
||||
|
|
||||
|
@Input() |
||||
|
public step: WorkflowStep; |
||||
|
|
||||
|
@Output() |
||||
|
public transitionRemove = new EventEmitter<WorkflowTransition>(); |
||||
|
|
||||
|
@Output() |
||||
|
public update = new EventEmitter<WorkflowStepValues>(); |
||||
|
|
||||
|
@Output() |
||||
|
public rename = new EventEmitter<string>(); |
||||
|
|
||||
|
@Output() |
||||
|
public remove = new EventEmitter(); |
||||
|
|
||||
|
public openSteps: WorkflowStep[]; |
||||
|
|
||||
|
public transitions: WorkflowTransitionView[]; |
||||
|
|
||||
|
public ngOnChanges(changes: SimpleChanges) { |
||||
|
if (changes['workflow'] || changes['step']) { |
||||
|
this.openSteps = this.workflow.getOpenSteps(this.step); |
||||
|
|
||||
|
this.transitions = this.workflow.getTransitions(this.step); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public changeName(name: string) { |
||||
|
this.rename.emit(name); |
||||
|
} |
||||
|
|
||||
|
public changeColor(color: string) { |
||||
|
this.update.emit({ color }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,35 @@ |
|||||
|
<sqx-panel desiredWidth="50rem" isBlank="true" [isLazyLoaded]="false"> |
||||
|
<ng-container title> |
||||
|
Workflows |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container menu> |
||||
|
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh roles (CTRL + SHIFT + R)"> |
||||
|
<i class="icon-reset"></i> Refresh |
||||
|
</button> |
||||
|
|
||||
|
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> |
||||
|
|
||||
|
<ng-container> |
||||
|
<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 content> |
||||
|
<sqx-workflow-step *ngFor="let step of workflow.steps" |
||||
|
[workflow]="workflow" |
||||
|
[step]="step" |
||||
|
(update)="updateStep(step, $event)" |
||||
|
(rename)="renameStep(step, $event)" |
||||
|
(remove)="removeStep(step)"> |
||||
|
</sqx-workflow-step> |
||||
|
|
||||
|
<button class="btn btn-success" (click)="addStep()"> |
||||
|
Add Step |
||||
|
</button> |
||||
|
</ng-container> |
||||
|
</sqx-panel> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -0,0 +1,48 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
import { |
||||
|
WorkflowDto, |
||||
|
WorkflowStep, |
||||
|
WorkflowStepValues |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-workflows-page', |
||||
|
styleUrls: ['./workflows-page.component.scss'], |
||||
|
templateUrl: './workflows-page.component.html' |
||||
|
}) |
||||
|
export class WorkflowsPageComponent { |
||||
|
public workflow = new WorkflowDto().setStep('Published', { color: 'green' }); |
||||
|
|
||||
|
public reload() { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
public save() { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
public addStep() { |
||||
|
this.workflow = this.workflow.setStep(`Step${this.workflow.steps.length + 1}`, {}); |
||||
|
} |
||||
|
|
||||
|
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);
|
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,26 @@ |
|||||
|
<span> |
||||
|
<div class="selection"> |
||||
|
<input type="text" class="form-control" [disabled]="snapshot.isDisabled" (click)="open()" readonly (keydown)="onKeyDown($event)" #input |
||||
|
autocomplete="off" |
||||
|
autocorrect="off" |
||||
|
autocapitalize="off"> |
||||
|
|
||||
|
<div class="control-dropdown-item" *ngIf="snapshot.selectedItem"> |
||||
|
<ng-container *ngIf="!selectionTemplate">{{snapshot.selectedItem}}</ng-container> |
||||
|
|
||||
|
<ng-template *ngIf="selectionTemplate" [sqxTemplateWrapper]="selectionTemplate" [item]="snapshot.selectedItem"></ng-template> |
||||
|
</div> |
||||
|
|
||||
|
<i class="icon-caret-down"></i> |
||||
|
</div> |
||||
|
|
||||
|
<div class="items-container"> |
||||
|
<div class="control-dropdown" #container *sqxModalView="dropdown" [sqxModalTarget]="input" position="bottomLeft"> |
||||
|
<div *ngFor="let item of items; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === snapshot.selectedIndex" (mousedown)="selectIndexAndClose(i)" [sqxScrollActive]="i === snapshot.selectedIndex" [container]="container"> |
||||
|
<ng-container *ngIf="!itemTemplate">{{item}}</ng-container> |
||||
|
|
||||
|
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</span> |
||||
@ -0,0 +1,38 @@ |
|||||
|
@import '_mixins'; |
||||
|
@import '_vars'; |
||||
|
|
||||
|
$color-input-disabled: #eef1f4; |
||||
|
|
||||
|
.form-control { |
||||
|
& { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
&[readonly] { |
||||
|
background: $color-input-background; |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
background: $color-input-disabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.selection { |
||||
|
& { |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.control-dropdown-item { |
||||
|
@include absolute(0, 1rem, 0, 0); |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.icon-caret-down { |
||||
|
@include absolute(30%, .4rem, auto, auto); |
||||
|
cursor: pointer; |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
|
||||
|
import { Keys, ModalModel, StatefulControlComponent } from '@app/framework/internal'; |
||||
|
|
||||
|
export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { |
||||
|
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true |
||||
|
}; |
||||
|
|
||||
|
interface State { |
||||
|
selectedItem: any; |
||||
|
selectedIndex: number; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-dropdown', |
||||
|
styleUrls: ['./dropdown.component.scss'], |
||||
|
templateUrl: './dropdown.component.html', |
||||
|
providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class DropdownComponent extends StatefulControlComponent<State, any[]> implements AfterContentInit, ControlValueAccessor { |
||||
|
@Input() |
||||
|
public items: any[] = []; |
||||
|
|
||||
|
@ContentChildren(TemplateRef) |
||||
|
public templates: QueryList<any>; |
||||
|
|
||||
|
public dropdown = new ModalModel(); |
||||
|
|
||||
|
public selectionTemplate: TemplateRef<any>; |
||||
|
|
||||
|
public itemTemplate: TemplateRef<any>; |
||||
|
|
||||
|
constructor(changeDetector: ChangeDetectorRef) { |
||||
|
super(changeDetector, { |
||||
|
selectedItem: undefined, |
||||
|
selectedIndex: -1 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngAfterContentInit() { |
||||
|
if (this.templates.length === 1) { |
||||
|
this.itemTemplate = this.selectionTemplate = this.templates.first; |
||||
|
} else { |
||||
|
this.templates.forEach(template => { |
||||
|
if (template.name === 'selection') { |
||||
|
this.selectionTemplate = template; |
||||
|
} else { |
||||
|
this.itemTemplate = template; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public writeValue(obj: any) { |
||||
|
this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0); |
||||
|
} |
||||
|
|
||||
|
public onKeyDown(event: KeyboardEvent) { |
||||
|
switch (event.keyCode) { |
||||
|
case Keys.UP: |
||||
|
this.up(); |
||||
|
return false; |
||||
|
case Keys.DOWN: |
||||
|
this.down(); |
||||
|
return false; |
||||
|
case Keys.ESCAPE: |
||||
|
case Keys.ENTER: |
||||
|
if (this.dropdown.isOpen) { |
||||
|
this.close(); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public open() { |
||||
|
this.dropdown.show(); |
||||
|
|
||||
|
this.callTouched(); |
||||
|
} |
||||
|
|
||||
|
public selectIndexAndClose(selectedIndex: number) { |
||||
|
this.selectIndex(selectedIndex); |
||||
|
|
||||
|
this.close(); |
||||
|
} |
||||
|
|
||||
|
private close() { |
||||
|
this.dropdown.hide(); |
||||
|
} |
||||
|
|
||||
|
public selectIndex(selectedIndex: number) { |
||||
|
if (selectedIndex < 0) { |
||||
|
selectedIndex = 0; |
||||
|
} |
||||
|
|
||||
|
const items = this.items || []; |
||||
|
|
||||
|
if (selectedIndex >= items.length) { |
||||
|
selectedIndex = items.length - 1; |
||||
|
} |
||||
|
|
||||
|
const value = items[selectedIndex]; |
||||
|
|
||||
|
if (value !== this.snapshot.selectedItem) { |
||||
|
selectedIndex = selectedIndex; |
||||
|
|
||||
|
this.next(s => ({ ...s, selectedIndex, selectedItem: value })); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private up() { |
||||
|
this.selectIndex(this.snapshot.selectedIndex - 1); |
||||
|
} |
||||
|
|
||||
|
private down() { |
||||
|
this.selectIndex(this.snapshot.selectedIndex + 1); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
<div class="title"> |
||||
|
<form *ngIf="isRenaming; else noRenaming" class="form-inline" [formGroup]="renameForm" (ngSubmit)="rename()"> |
||||
|
<div class="form-group mr-1"> |
||||
|
<sqx-control-errors for="name"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control form-underlined" formControlName="name" maxlength="20" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" /> |
||||
|
</div> |
||||
|
|
||||
|
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.valid || !renameForm.dirty">Save</button> |
||||
|
|
||||
|
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="toggleRename()"> |
||||
|
<i class="icon-close"></i> |
||||
|
</button> |
||||
|
</form> |
||||
|
|
||||
|
<ng-template #noRenaming> |
||||
|
<h3 class="title-name" (dblclick)="toggleRename()"> |
||||
|
{{name}} |
||||
|
</h3> |
||||
|
|
||||
|
<i class="title-edit icon-pencil" *ngIf="!disabled" (click)="toggleRename()"></i> |
||||
|
</ng-template> |
||||
|
</div> |
||||
@ -0,0 +1,33 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.title { |
||||
|
&-edit { |
||||
|
color: $color-border-dark; |
||||
|
display: none; |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
padding: .6rem .25rem; |
||||
|
border: 0; |
||||
|
background: transparent; |
||||
|
vertical-align: baseline; |
||||
|
} |
||||
|
|
||||
|
&-name { |
||||
|
padding: .375rem 0; |
||||
|
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
||||
|
font-size: 1.2rem; |
||||
|
font-weight: normal; |
||||
|
line-height: 1.5rem; |
||||
|
border-top: 1px solid transparent; |
||||
|
border-bottom: 1px solid transparent; |
||||
|
display: inline-block; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
.title-edit { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core'; |
||||
|
import { FormBuilder, Validators } from '@angular/forms'; |
||||
|
|
||||
|
const ESCAPE_KEY = 27; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-editable-title', |
||||
|
styleUrls: ['./editable-title.component.scss'], |
||||
|
templateUrl: './editable-title.component.html' |
||||
|
}) |
||||
|
export class EditableTitleComponent { |
||||
|
@Input() |
||||
|
public disabled = false; |
||||
|
|
||||
|
@Input() |
||||
|
public name: string; |
||||
|
|
||||
|
@Output() |
||||
|
public nameChanged = new EventEmitter<string>(); |
||||
|
|
||||
|
public isRenaming = false; |
||||
|
|
||||
|
public renameForm = this.formBuilder.group({ |
||||
|
name: ['', |
||||
|
[ |
||||
|
Validators.required |
||||
|
] |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public onKeyDown(keyCode: number) { |
||||
|
if (keyCode === ESCAPE_KEY) { |
||||
|
this.toggleRename(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public toggleRename() { |
||||
|
if (this.disabled) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.renameForm.setValue({ name: this.name }); |
||||
|
|
||||
|
this.isRenaming = !this.isRenaming; |
||||
|
} |
||||
|
|
||||
|
public rename() { |
||||
|
if (this.disabled) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this.renameForm.valid) { |
||||
|
const value = this.renameForm.value; |
||||
|
|
||||
|
this.nameChanged.emit(value.name); |
||||
|
|
||||
|
this.toggleRename(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
export module Keys { |
||||
|
export const COMMA = 188; |
||||
|
export const DELETE = 8; |
||||
|
export const ENTER = 13; |
||||
|
export const ESCAPE = 27; |
||||
|
export const DOWN = 40; |
||||
|
export const UP = 38; |
||||
|
} |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue