Merge pull request #4260 from tsiegleauq/workflow-creator

Add motion workflow creator
This commit is contained in:
Sean 2019-02-07 16:52:48 +01:00 committed by GitHub
commit 9664e52237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 977 additions and 19 deletions

View File

@ -9,6 +9,7 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotion } from 'app/site/motions/models/view-motion';
import { HttpService } from 'app/core/core-services/http.service';
/** /**
* Repository Services for Categories * Repository Services for Categories
@ -24,28 +25,54 @@ import { ViewMotion } from 'app/site/motions/models/view-motion';
providedIn: 'root' providedIn: 'root'
}) })
export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Workflow> { export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Workflow> {
/**
* The url to state on rest
*/
private restStateUrl = 'rest/motions/state/';
/** /**
* Creates a WorkflowRepository * Creates a WorkflowRepository
* Converts existing and incoming workflow to ViewWorkflows * Converts existing and incoming workflow to ViewWorkflows
* @param DS *
* @param dataSend * @param DS Accessing the data store
* @param mapperService mapping models
* @param dataSend sending data to the server
* @param httpService HttpService
*/ */
public constructor( public constructor(
protected DS: DataStoreService, protected DS: DataStoreService,
mapperService: CollectionStringMapperService, mapperService: CollectionStringMapperService,
private dataSend: DataSendService private dataSend: DataSendService,
private httpService: HttpService
) { ) {
super(DS, mapperService, Workflow); super(DS, mapperService, Workflow);
} }
/**
* Creates a ViewWorkflow from a given Workflow
*
* @param workflow the Workflow to convert
*/
protected createViewModel(workflow: Workflow): ViewWorkflow { protected createViewModel(workflow: Workflow): ViewWorkflow {
return new ViewWorkflow(workflow); return new ViewWorkflow(workflow);
} }
/**
* Creates a new workflow
*
* @param newWorkflow the workflow to create
* @returns the ID of a new workflow as promise
*/
public async create(newWorkflow: Workflow): Promise<Identifiable> { public async create(newWorkflow: Workflow): Promise<Identifiable> {
return await this.dataSend.createModel(newWorkflow); return await this.dataSend.createModel(newWorkflow);
} }
/**
* Updates the workflow by the given changes
*
* @param workflow Contains the update
* @param viewWorkflow the target workflow
*/
public async update(workflow: Partial<Workflow>, viewWorkflow: ViewWorkflow): Promise<void> { public async update(workflow: Partial<Workflow>, viewWorkflow: ViewWorkflow): Promise<void> {
let updateWorkflow: Workflow; let updateWorkflow: Workflow;
if (viewWorkflow) { if (viewWorkflow) {
@ -57,25 +84,70 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
await this.dataSend.updateModel(updateWorkflow); await this.dataSend.updateModel(updateWorkflow);
} }
/**
* Deletes the given workflow
*
* @param viewWorkflow the workflow to delete
*/
public async delete(viewWorkflow: ViewWorkflow): Promise<void> { public async delete(viewWorkflow: ViewWorkflow): Promise<void> {
const workflow = viewWorkflow.workflow; const workflow = viewWorkflow.workflow;
await this.dataSend.deleteModel(workflow); await this.dataSend.deleteModel(workflow);
} }
/**
* Adds a new state to the given workflow
*
* @param stateName The name of the new Workflow
* @param viewWorkflow The workflow
*/
public async addState(stateName: string, viewWorkflow: ViewWorkflow): Promise<void> {
const newStatePayload = {
name: stateName,
workflow_id: viewWorkflow.id
};
await this.httpService.post(this.restStateUrl, newStatePayload);
}
/**
* Updates workflow state with a new value-object and sends it to the server
*
* @param newValue a key-value pair with the new state value
* @param workflowState the workflow state to update
*/
public async updateState(newValue: object, workflowState: WorkflowState): Promise<void> {
const stateUpdate = Object.assign(workflowState, newValue);
await this.httpService.put(`${this.restStateUrl}${workflowState.id}/`, stateUpdate);
}
/**
* Deletes the selected work
*
* @param workflowState the workflow state to delete
*/
public async deleteState(workflowState: WorkflowState): Promise<void> {
await this.httpService.delete(`${this.restStateUrl}${workflowState.id}/`);
}
/** /**
* Collects all existing states from all workflows * Collects all existing states from all workflows
*
* @returns All currently existing workflow states
*/ */
public getAllWorkflowStates(): WorkflowState[] { public getAllWorkflowStates(): WorkflowState[] {
let states: WorkflowState[] = []; let states: WorkflowState[] = [];
this.getViewModelList().forEach(workflow => { this.getViewModelList().forEach(workflow => {
states = states.concat(workflow.states); if (workflow) {
states = states.concat(workflow.states);
}
}); });
return states; return states;
} }
/** /**
* Returns all workflowStates that cover the list of viewMotions given * Returns all workflowStates that cover the list of viewMotions given
* @param motions *
* @param motions The motions to get the workflows from
* @returns The workflow states to the given motion
*/ */
public getWorkflowStatesForMotions(motions: ViewMotion[]): WorkflowState[] { public getWorkflowStatesForMotions(motions: ViewMotion[]): WorkflowState[] {
let states: WorkflowState[] = []; let states: WorkflowState[] = [];

View File

@ -21,7 +21,7 @@ export class WorkflowState extends Deserializer {
public name: string; public name: string;
public recommendation_label: string; public recommendation_label: string;
public css_class: string; public css_class: string;
public required_permission_to_see: string; public access_level: number;
public allow_support: boolean; public allow_support: boolean;
public allow_create_poll: boolean; public allow_create_poll: boolean;
public allow_submitter_edit: boolean; public allow_submitter_edit: boolean;

View File

@ -9,7 +9,11 @@ export class Workflow extends BaseModel<Workflow> {
public id: number; public id: number;
public name: string; public name: string;
public states: WorkflowState[]; public states: WorkflowState[];
public first_state: number; public first_state_id: number;
public get firstState(): WorkflowState {
return this.getStateById(this.first_state_id);
}
public constructor(input?: any) { public constructor(input?: any) {
super('motions/workflow', 'Workflow', input); super('motions/workflow', 'Workflow', input);
@ -35,13 +39,7 @@ export class Workflow extends BaseModel<Workflow> {
} }
public getStateById(id: number): WorkflowState { public getStateById(id: number): WorkflowState {
let targetState; return this.states.find(state => state.id === id);
this.states.forEach(state => {
if (id === state.id) {
targetState = state;
}
});
return targetState as WorkflowState;
} }
public deserialize(input: any): void { public deserialize(input: any): void {

View File

@ -172,6 +172,10 @@
<mat-icon>speaker_notes</mat-icon> <mat-icon>speaker_notes</mat-icon>
<span translate>Comment fields</span> <span translate>Comment fields</span>
</button> </button>
<button mat-menu-item routerLink="workflow">
<mat-icon>build</mat-icon>
<span translate>Workflows</span>
</button>
<button mat-menu-item routerLink="/tags" *osPerms="'core.can_manage_tags'"> <button mat-menu-item routerLink="/tags" *osPerms="'core.can_manage_tags'">
<mat-icon>local_offer</mat-icon> <mat-icon>local_offer</mat-icon>
<span translate>Tags</span> <span translate>Tags</span>

View File

@ -0,0 +1,189 @@
<os-head-bar [nav]="false" [mainButton]="true" (mainEvent)="onNewStateButton()">
<!-- Title -->
<div class="title-slot">
<h2 *ngIf="workflow">
{{ workflow }}
</h2>
</div>
<!-- Edit button -->
<div class="extra-controls-slot on-transition-fade">
<button mat-icon-button (click)="onEditWorkflowButton()">
<mat-icon>edit</mat-icon>
</button>
</div>
</os-head-bar>
<!-- Detail -->
<div *ngIf="workflow">
<div class="title-line">
<strong>
<span translate>First state</span>:
<span>{{ workflow.firstState }}</span>
</strong>
</div>
<div class="scrollable-matrix">
<table mat-table class="on-transition-fade" [dataSource]="getTableDataSource()">
<ng-container matColumnDef="perm" sticky>
<mat-header-cell class="group-head-table-cell" *matHeaderCellDef translate>Permissions</mat-header-cell>
<mat-cell *matCellDef="let perm">
<div class="permission-name">
{{ perm.name | translate }}
</div>
</mat-cell>
</ng-container>
<div *ngFor="let state of workflow.states; trackBy: trackElement">
<ng-container [matColumnDef]="getColumnDef(state)">
<mat-header-cell *matHeaderCellDef (click)="onClickStateName(state)">
<div class="clickable-cell">
<div class="inner-table">
{{ state.name | translate }}
</div>
</div>
</mat-header-cell>
<mat-cell *matCellDef="let perm">
<div class="inner-table" *ngIf="perm.type === 'check'">
<mat-checkbox
[checked]="state[perm.selector]"
(change)="onToggleStatePerm(state, perm.selector, $event)"
></mat-checkbox>
</div>
<div
class="clickable-cell"
*ngIf="perm.type === 'input'"
(click)="onClickInputPerm(perm, state)"
>
<div class="inner-table">
{{ state[perm.selector] || '-' | translate }}
</div>
</div>
<div class="inner-table" *ngIf="perm.type === 'color'">
<mat-basic-chip
[matMenuTriggerFor]="colorMenu"
[matMenuTriggerData]="{ state: state }"
[disableRipple]="true"
[ngClass]="getStateCssColor(state[perm.selector])"
>
{{ state[perm.selector] }}
</mat-basic-chip>
</div>
<div
class="clickable-cell"
*ngIf="perm.type === 'state'"
[matMenuTriggerFor]="nextStatesMenu"
[matMenuTriggerData]="{ state: state }"
>
<div class="inner-table">
{{ state.next_states_id.length > 0 ? state.getNextStates(workflow.workflow) : '-' }}
</div>
</div>
<div
class="clickable-cell"
*ngIf="perm.type === 'amendment'"
[matMenuTriggerFor]="mergeAmendmentMenu"
[matMenuTriggerData]="{ state: state }"
>
<div class="inner-table">
{{ getMergeAmendmentLabel(state.merge_amendment_into_final) | translate }}
</div>
</div>
<div
class="clickable-cell"
*ngIf="perm.type === 'accessLevel'"
[matMenuTriggerFor]="accessLevelMenu"
[matMenuTriggerData]="{ state: state }"
>
<div class="inner-table">
{{ accessLevels[state.access_level].label }}
</div>
</div>
</mat-cell>
</ng-container>
</div>
<mat-header-row *matHeaderRowDef="headerRowDef"></mat-header-row>
<mat-row *matRowDef="let row; columns: headerRowDef"></mat-row>
</table>
</div>
</div>
<!-- New state dialog -->
<ng-template #workflowDialog>
<h1 mat-dialog-title>
<span translate>{{ dialogData.title }}</span>
</h1>
<div class="os-form-card-mobile" mat-dialog-content>
<p translate>{{ dialogData.description }}</p>
<mat-form-field>
<input matInput osAutofocus [(ngModel)]="dialogData.value" />
</mat-form-field>
</div>
<div mat-dialog-actions>
<button
type="submit"
mat-button
color="primary"
[mat-dialog-close]="{ action: 'update', value: dialogData.value }"
>
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
<button
type="button"
mat-button
color="warn"
*ngIf="dialogData.deletable"
[mat-dialog-close]="{ action: 'delete' }"
>
<span translate>Delete</span>
</button>
</div>
</ng-template>
<!-- select color menu -->
<mat-menu matMenuContent #colorMenu="matMenu">
<ng-template let-state="state" matMenuContent>
<button mat-menu-item *ngFor="let color of labelColors" (click)="onSelectColor(state, color)">
<mat-icon *ngIf="color === state.css_class">check</mat-icon>
<span>{{ color | translate }}</span>
</button>
</ng-template>
</mat-menu>
<!-- select next states menu -->
<mat-menu matMenuContent #nextStatesMenu="matMenu">
<ng-template let-state="state" matMenuContent>
<div *ngFor="let nextState of workflow.states">
<button mat-menu-item *ngIf="nextState.name !== state.name" (click)="onSetNextState(nextState, state)">
<mat-icon *ngIf="state.next_states_id.includes(nextState.id)">check</mat-icon>
<span>{{ nextState.name | translate }}</span>
</button>
</div>
</ng-template>
</mat-menu>
<!-- Select access level menu -->
<mat-menu matMenuContent #accessLevelMenu="matMenu">
<ng-template let-state="state" matMenuContent>
<button mat-menu-item *ngFor="let level of accessLevels" (click)="onSetAccesLevel(level.level, state)">
<mat-icon *ngIf="state.access_level === level.level">check</mat-icon>
<span translate> {{ level.label }}</span>
</button>
</ng-template>
</mat-menu>
<!-- Select merge amendment menu -->
<mat-menu matMenuContent #mergeAmendmentMenu="matMenu">
<ng-template let-state="state" matMenuContent>
<div *ngFor="let amendment of amendmentIntoFinal">
<button mat-menu-item (click)="setMergeAmendment(amendment.merge, state)">
<mat-icon *ngIf="amendment.merge === state.merge_amendment_into_final">check</mat-icon>
<span translate> {{ amendment.label }}</span>
</button>
</div>
</ng-template>
</mat-menu>

View File

@ -0,0 +1,54 @@
.title-line {
margin: 20px 25px;
}
table {
width: 100%;
.mat-header-cell {
min-width: 150px;
position: relative;
}
.mat-cell {
min-width: 150px;
position: relative;
}
// scaled up version of an inner table
.clickable-cell {
position: absolute;
display: flex;
width: 100%;
height: 100%;
}
.clickable-cell:hover {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.025);
}
.inner-table {
align-items: center;
margin: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 10px;
}
.mat-column-perm {
min-width: 150px;
.permission-name {
margin-right: 10px;
}
}
}
.mat-chip:hover {
cursor: pointer;
}
.scrollable-matrix {
overflow: auto;
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkflowDetailComponent } from './workflow-detail.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('WorkflowDetailComponent', () => {
let component: WorkflowDetailComponent;
let fixture: ComponentFixture<WorkflowDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [WorkflowDetailComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,390 @@
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { MatDialog, MatSnackBar, MatTableDataSource, MatCheckboxChange } from '@angular/material';
import { Observable } from 'rxjs';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewWorkflow } from '../../models/view-workflow';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { WorkflowState, MergeAmendment } from 'app/shared/models/motions/workflow-state';
/**
* Declares data for the workflow dialog
*/
interface DialogData {
title: string;
description: string;
value: string;
deletable?: boolean;
}
/**
* Determine answers from the dialog
*/
interface DialogResult {
action: 'update' | 'delete';
value?: string;
}
/**
* Defines state permissions
*/
interface StatePerm {
name: string;
selector: string;
type: string;
reference?: string;
}
/**
* Defines the structure of access levels
*/
interface AccessLevel {
level: number;
label: string;
}
/**
* Defines the structure of access levels
*/
interface AmendmentIntoFinal {
merge: MergeAmendment;
label: string;
}
/**
* Motion workflow management detail view
*/
@Component({
selector: 'os-workflow-detail',
templateUrl: './workflow-detail.component.html',
styleUrls: ['./workflow-detail.component.scss']
})
export class WorkflowDetailComponent extends BaseViewComponent implements OnInit {
/**
* Reference to the workflow dialog
*/
@ViewChild('workflowDialog')
private workflowDialog: TemplateRef<string>;
/**
* Holds the dialog data
*/
public dialogData: DialogData;
/**
* Holds the current workflow
*/
public workflow: ViewWorkflow;
/**
* The header rows that the table should show
* Updated through subscription
*/
public headerRowDef: string[] = [];
/**
* Determine label colors. Where they should come from is currently now know
*/
public labelColors = ['default', 'primary', 'success', 'danger'];
/**
* Holds state permissions
*/
private statePermissionsList = [
{ name: 'Recommendation label', selector: 'recommendation_label', type: 'input' },
{ name: 'Allow support', selector: 'allow_support', type: 'check' },
{ name: 'Allow create poll', selector: 'allow_create_poll', type: 'check' },
{ name: 'Allow submitter edit', selector: 'allow_submitter_edit', type: 'check' },
{ name: 'Set identifier', selector: 'dont_set_identifier', type: 'check' },
{ name: 'Show state extension field', selector: 'show_state_extension_field', type: 'check' },
{
name: 'Show recommendation extension field',
selector: 'show_recommendation_extension_field',
type: 'check'
},
{ name: 'Show amendment in parent motoin', selector: 'merge_amendment_into_final', type: 'amendment' },
{ name: 'Access level', selector: 'access_level', type: 'accessLevel' },
{ name: 'Label color', selector: 'css_class', type: 'color' },
{ name: 'Next states', selector: 'next_states_id', type: 'state' }
] as StatePerm[];
/**
* Determines possible access levels
*/
public accessLevels = [
{ level: 0, label: 'All users' },
{ level: 1, label: 'Submitters, managers and users with permission to manage metadata' },
{ level: 2, label: 'Only managers and users with permission to manage metadata' },
{ level: 3, label: 'Managers only' }
] as AccessLevel[];
/**
* Determines possible "Merge amendments into final"
*/
public amendmentIntoFinal = [
{ merge: MergeAmendment.NO, label: 'No' },
{ merge: MergeAmendment.UNDEFINED, label: '-' },
{ merge: MergeAmendment.YES, label: 'Yes' }
] as AmendmentIntoFinal[];
/**
* Constructor
*
* @param title Set the page title
* @param translate Handle translations
* @param matSnackBar Showing error
* @param dialog Opening dialogs
* @param workflowRepo The repository for workflows
* @param route Read out URL paramters
*/
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private dialog: MatDialog,
private workflowRepo: WorkflowRepositoryService,
private route: ActivatedRoute
) {
super(title, translate, matSnackBar);
}
/**
* Init.
*
* Observe the parameters of the URL and loads the specified workflow
*/
public ngOnInit(): void {
this.route.params.subscribe(params => {
if (params) {
this.workflowRepo.getViewModelObservable(params.id).subscribe(newWorkflow => {
this.workflow = newWorkflow;
this.updateRowDef();
});
}
});
}
/**
* Click handler for the state names.
* Opens a dialog to rename a state.
*
* @param state the selected workflow state
*/
public onClickStateName(state: WorkflowState): void {
this.openEditDialog(state.name, 'Rename state', '', true).subscribe(result => {
if (result) {
if (result.action === 'update') {
this.workflowRepo.updateState({ name: result.value }, state).then(() => {}, this.raiseError);
} else if (result.action === 'delete') {
this.workflowRepo.deleteState(state).then(() => {}, this.raiseError);
}
}
});
}
/**
* Click handler for the button to add new states the the current workflow
* Opens a dialog to enter the workflow name
*/
public onNewStateButton(): void {
this.openEditDialog('', 'Create new state', 'Name').subscribe(result => {
if (result && result.action === 'update') {
this.workflowRepo.addState(result.value, this.workflow).then(() => {}, this.raiseError);
}
});
}
/**
* Click handler for the edit button.
* Opens a dialog to rename the workflow
*/
public onEditWorkflowButton(): void {
this.openEditDialog(this.workflow.name, 'Edit name', 'Please enter a new workflow name:').subscribe(result => {
if (result && result.action === 'update') {
this.workflowRepo.update({ name: result.value }, this.workflow).then(() => {}, this.raiseError);
}
});
}
/**
* Handler for the workflow state "input" fields.
* Opens a dialog to edit a label.
*
* @param perm The permission
* @param state The selected workflow state
*/
public onClickInputPerm(perm: StatePerm, state: WorkflowState): void {
this.openEditDialog(state[perm.selector], 'Edit', perm.name).subscribe(result => {
if (result && result.action === 'update') {
this.workflowRepo.updateState({ [perm.selector]: result.value }, state).then(() => {}, this.raiseError);
}
});
}
/**
* Handler for toggling workflow states
*
* @param state The workflow state to edit
* @param perm The states permission that was changed
* @param event The change event.
*/
public onToggleStatePerm(state: WorkflowState, perm: string, event: MatCheckboxChange): void {
this.workflowRepo.updateState({ [perm]: event.checked }, state).then(() => {}, this.raiseError);
}
/**
* Handler for selecting colors / css classes for workflow states.
* Sets the css class for the specific workflow
*
* @param state The selected workflow state
* @param color The selected color
*/
public onSelectColor(state: WorkflowState, color: string): void {
this.workflowRepo.updateState({ css_class: color }, state).then(() => {}, this.raiseError);
}
/**
* Handler to add or remove next states to a workflow state
*
* @param nextState the potential next workflow state
* @param state the state to add or remove another state to
*/
public onSetNextState(nextState: WorkflowState, state: WorkflowState): void {
const ids = state.next_states_id.map(id => id);
const stateIdIndex = ids.findIndex(id => id === nextState.id);
if (stateIdIndex < 0) {
ids.push(nextState.id);
} else {
ids.splice(stateIdIndex, 1);
}
this.workflowRepo.updateState({ next_states_id: ids }, state).then(() => {}, this.raiseError);
}
/**
* Sets an access level to the given workflow state
*
* @param level The new access level
* @param state the state to change
*/
public onSetAccesLevel(level: number, state: WorkflowState): void {
this.workflowRepo.updateState({ access_level: level }, state).then(() => {}, this.raiseError);
}
/**
* Sets the 'merge_amendment_into_final' value
*
* @param amendment determines the amendment
* @param state the state to change
*/
public setMergeAmendment(amendment: number, state: WorkflowState): void {
this.workflowRepo.updateState({ merge_amendment_into_final: amendment }, state).then(() => {}, this.raiseError);
}
/**
* Function to open the edit dialog. Returns the observable to the result after the dialog
* was closed
*
* @param value holds the valie
* @param title The title of the dialog
* @param description The description of the dialog
* @param deletable determine if a delete button should be offered
*/
private openEditDialog(
value: string,
title?: string,
description?: string,
deletable?: boolean
): Observable<DialogResult> {
this.dialogData = {
title: title || '',
description: description || '',
value: value,
deletable: deletable
};
const dialogRef = this.dialog.open(this.workflowDialog, {
maxHeight: '90vh',
width: '400px',
maxWidth: '90vw'
});
return dialogRef.afterClosed();
}
/**
* Returns a merge amendment label from state
*/
public getMergeAmendmentLabel(mergeAmendment: MergeAmendment): string {
return this.amendmentIntoFinal.find(amendment => amendment.merge === mergeAmendment).label;
}
/**
* Defines the data source for the workflow state table
*
* @returns the MatTableDateSource to iterate over a workflow states contents
*/
public getTableDataSource(): MatTableDataSource<StatePerm> {
const dataSource = new MatTableDataSource<StatePerm>();
dataSource.data = this.statePermissionsList;
return dataSource;
}
/**
* Update the rowDefinition after Reloading or changes
*/
public updateRowDef(): void {
// reset the rowDef list first
this.headerRowDef = ['perm'];
if (this.workflow) {
this.workflow.states.forEach(state => {
this.headerRowDef.push(this.getColumnDef(state));
});
}
}
/**
* Creates a unique column-def from the name and the id of a state
*
* @param state the workflow state
* @returns a unique definition
*/
public getColumnDef(state: WorkflowState): string {
return `${state.name}${state.id}`;
}
/**
* Required to detect changes in *ngFor loops
*
* @param index Corresponding group that was changed
* @returns the tracked workflows id
*/
public trackElement(index: number): number {
return index;
}
/**
* Translate the state's css class into a color
*
* @param colorLabel the default color label of a selected workflow
* @returns a string representing a color
*/
public getStateCssColor(colorLabel: string): string {
switch (colorLabel) {
case 'success':
return 'green';
case 'danger':
return 'red';
case 'default':
return 'grey';
case 'primary':
return 'lightblue';
default:
return '';
}
}
}

View File

@ -0,0 +1,51 @@
<!-- <os-head-bar [mainButton]=true (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect"> -->
<os-head-bar [nav]="false" [mainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)">
<!-- Title -->
<div class="title-slot"><h2 translate>Workflows</h2></div>
</os-head-bar>
<mat-table class="os-headed-listview-table on-transition-fade" [dataSource]="dataSource">
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>
<span translate>Name</span>
</mat-header-cell>
<mat-cell *matCellDef="let workflow" (click)="onClickWorkflow(workflow)">
{{ workflow }}
</mat-cell>
</ng-container>
<!-- Delete Column -->
<ng-container matColumnDef="delete">
<mat-header-cell *matHeaderCellDef> </mat-header-cell>
<mat-cell *matCellDef="let workflow">
<button type="button" mat-icon-button (click)="onDeleteWorkflow(workflow)">
<mat-icon color="warn">delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: getColumnDefinition()"></mat-row>
</mat-table>
<!-- New workflow dialog -->
<ng-template #newWorkflowDialog>
<h1 mat-dialog-title>
<span translate>Create new workflow</span>
</h1>
<div mat-dialog-content>
<p translate>Please enter a name for the new workflow:</p>
<mat-form-field>
<input matInput osAutofocus [(ngModel)]="newWorkflowTitle" />
</mat-form-field>
</div>
<div mat-dialog-actions>
<button type="submit" mat-button color="primary" [mat-dialog-close]="newWorkflowTitle">
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
</div>
</ng-template>

View File

@ -0,0 +1,14 @@
.os-headed-listview-table {
/** name */
.mat-column-name {
width: 100%;
flex: 1 0 200px;
padding-left: 10px;
}
/** delete */
.mat-column-delete {
flex: 0 0 190px;
justify-content: flex-end !important;
}
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkflowListComponent } from './workflow-list.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('WorkflowListComponent', () => {
let component: WorkflowListComponent;
let fixture: ComponentFixture<WorkflowListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [WorkflowListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,114 @@
import { Component, OnInit, TemplateRef } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MatSnackBar, MatDialog } from '@angular/material';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { Router, ActivatedRoute } from '@angular/router';
import { ViewWorkflow } from '../../models/view-workflow';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { Workflow } from 'app/shared/models/motions/workflow';
/**
* List view for workflows
*/
@Component({
selector: 'os-workflow-list',
templateUrl: './workflow-list.component.html',
styleUrls: ['./workflow-list.component.scss']
})
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow> implements OnInit {
/**
* Holds the new workflow title
*/
public newWorkflowTitle: string;
/**
* Determine the coloms in the table
*/
private columns: string[] = ['name', 'delete'];
/**
* Constructor
*
* @param titleService Sets the title
* @param matSnackBar Showing errors
* @param translate handle trandlations
* @param dialog Dialog options
* @param router navigating back and forth
* @param route Information about the current router
* @param workflowRepo Repository for Workflows
* @param promptService Before delete, ask
*/
public constructor(
titleService: Title,
matSnackBar: MatSnackBar,
protected translate: TranslateService,
private dialog: MatDialog,
private router: Router,
private route: ActivatedRoute,
private workflowRepo: WorkflowRepositoryService,
private promptService: PromptService
) {
super(titleService, translate, matSnackBar);
}
/**
* Init. Observe the repository
*/
public ngOnInit(): void {
super.setTitle('Workflows');
this.initTable();
this.workflowRepo.getViewModelListObservable().subscribe(newWorkflows => (this.dataSource.data = newWorkflows));
}
/**
* Click a workflow in the table
*
* @param selected the selected workflow
*/
public onClickWorkflow(selected: ViewWorkflow): void {
this.router.navigate([`${selected.id}`], { relativeTo: this.route });
}
/**
* Main Event handler. Create new Workflow
*
* @param templateRef The reference to the dialog
*/
public onNewButton(templateRef: TemplateRef<string>): void {
this.newWorkflowTitle = '';
const dialogRef = this.dialog.open(templateRef, {
width: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.workflowRepo.create(new Workflow({ name: result })).then(() => {}, this.raiseError);
}
});
}
/**
* Click delete button for workflow
*
* @param selected the selected workflow
*/
public async onDeleteWorkflow(selected: ViewWorkflow): Promise<void> {
const content = this.translate.instant('Delete') + ` ${selected}?`;
if (await this.promptService.open('Are you sure?', content)) {
this.workflowRepo.delete(selected).then(() => {}, this.raiseError);
}
}
/**
* Get the column definition for the current workflow table
*
* @returns The column definition for the table
*/
public getColumnDefinition(): string[] {
return this.columns;
}
}

View File

@ -1,6 +1,7 @@
import { Workflow } from 'app/shared/models/motions/workflow'; import { Workflow } from 'app/shared/models/motions/workflow';
import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { BaseViewModel } from '../../base/base-view-model'; import { BaseViewModel } from '../../base/base-view-model';
import { Deserializable } from 'app/shared/models/base/deserializable';
/** /**
* class for the ViewWorkflow. Currently only a basic stub * class for the ViewWorkflow. Currently only a basic stub
@ -33,8 +34,16 @@ export class ViewWorkflow extends BaseViewModel {
return this.workflow ? this.workflow.states : null; return this.workflow ? this.workflow.states : null;
} }
public get first_state(): number { public get first_state_id(): number {
return this.workflow ? this.workflow.first_state : null; return this.workflow ? this.workflow.first_state_id : null;
}
public get firstState(): WorkflowState {
return this.workflow ? this.workflow.firstState : null;
}
public getStateById(id: number): WorkflowState {
return this.workflow ? this.workflow.getStateById(id) : null;
} }
public getTitle(): string { public getTitle(): string {
@ -50,9 +59,12 @@ export class ViewWorkflow extends BaseViewModel {
/** /**
* Updates the local objects if required * Updates the local objects if required
*
* @param update * @param update
*/ */
public updateValues(update: Workflow): void { public updateValues(update: Deserializable): void {
this._workflow = update; if (update instanceof Workflow) {
this._workflow = update;
}
} }
} }

View File

@ -13,6 +13,8 @@ import { MotionListComponent } from './components/motion-list/motion-list.compon
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component'; import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component';
import { StatuteImportListComponent } from './components/statute-paragraph-list/statute-import-list/statute-import-list.component'; import { StatuteImportListComponent } from './components/statute-paragraph-list/statute-import-list/statute-import-list.component';
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component'; import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
import { WorkflowListComponent } from './components/workflow-list/workflow-list.component';
import { WorkflowDetailComponent } from './components/workflow-detail/workflow-detail.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: MotionListComponent }, { path: '', component: MotionListComponent },
@ -25,6 +27,8 @@ const routes: Routes = [
{ path: 'blocks/:id', component: MotionBlockDetailComponent }, { path: 'blocks/:id', component: MotionBlockDetailComponent },
{ path: 'new', component: MotionDetailComponent }, { path: 'new', component: MotionDetailComponent },
{ path: 'import', component: MotionImportListComponent }, { path: 'import', component: MotionImportListComponent },
{ path: 'workflow', component: WorkflowListComponent },
{ path: 'workflow/:id', component: WorkflowDetailComponent },
{ path: ':id', component: MotionDetailComponent }, { path: ':id', component: MotionDetailComponent },
{ path: ':id/speakers', component: SpeakerListComponent }, { path: ':id/speakers', component: SpeakerListComponent },
{ path: ':id/create-amendment', component: AmendmentCreateWizardComponent } { path: ':id/create-amendment', component: AmendmentCreateWizardComponent }

View File

@ -23,6 +23,8 @@ import { MotionPollComponent } from './components/motion-poll/motion-poll.compon
import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-dialog.component'; import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-dialog.component';
import { MotionExportDialogComponent } from './components/motion-export-dialog/motion-export-dialog.component'; import { MotionExportDialogComponent } from './components/motion-export-dialog/motion-export-dialog.component';
import { StatuteImportListComponent } from './components/statute-paragraph-list/statute-import-list/statute-import-list.component'; import { StatuteImportListComponent } from './components/statute-paragraph-list/statute-import-list/statute-import-list.component';
import { WorkflowListComponent } from './components/workflow-list/workflow-list.component';
import { WorkflowDetailComponent } from './components/workflow-detail/workflow-detail.component';
@NgModule({ @NgModule({
imports: [CommonModule, MotionsRoutingModule, SharedModule], imports: [CommonModule, MotionsRoutingModule, SharedModule],
@ -46,7 +48,9 @@ import { StatuteImportListComponent } from './components/statute-paragraph-list/
MotionPollComponent, MotionPollComponent,
MotionPollDialogComponent, MotionPollDialogComponent,
MotionExportDialogComponent, MotionExportDialogComponent,
StatuteImportListComponent StatuteImportListComponent,
WorkflowListComponent,
WorkflowDetailComponent
], ],
entryComponents: [ entryComponents: [
MotionChangeRecommendationComponent, MotionChangeRecommendationComponent,