diff --git a/frontend/src/app/core/common/enum/permission.enum.ts b/frontend/src/app/core/common/enum/permission.enum.ts index d2a63e292..2a1aa80b0 100644 --- a/frontend/src/app/core/common/enum/permission.enum.ts +++ b/frontend/src/app/core/common/enum/permission.enum.ts @@ -217,6 +217,10 @@ export enum AppPermission { EditUsageLimit = "EditUsageLimit", DeleteUsageLimit = "DeleteUsageLimit", + //PlanWorkflow + EditPlanWorkflow = "EditPlanWorkflow", + DeletePlanWorkflow = "DeletePlanWorkflow", + // UI Pages ViewDescriptionTemplateTypePage = "ViewDescriptionTemplateTypePage", @@ -245,5 +249,6 @@ export enum AppPermission { ViewUsageLimitPage = "ViewUsageLimitPage", ViewPlanStatusPage = "ViewPlanStatusPage", ViewDescriptionStatusPage = "ViewDescriptionStatusPage", + ViewPlanWorkflowPage = "ViewPlanWorkflowPage" } diff --git a/frontend/src/app/core/core-service.module.ts b/frontend/src/app/core/core-service.module.ts index 57d1dd5df..70d1f162c 100644 --- a/frontend/src/app/core/core-service.module.ts +++ b/frontend/src/app/core/core-service.module.ts @@ -50,6 +50,7 @@ import { RouterUtilsService } from './services/router/router-utils.service'; import { UsageLimitService } from './services/usage-limit/usage.service'; import { PlanStatusService } from './services/plan/plan-status.service'; import { DescriptionStatusService } from './services/description-status/description-status.service'; +import { PlanWorkflowService } from './services/plan/plan-workflow.service'; // // // This is shared module that provides all the services. Its imported only once on the AppModule. @@ -119,7 +120,8 @@ export class CoreServiceModule { RouterUtilsService, UsageLimitService, PlanStatusService, - DescriptionStatusService + DescriptionStatusService, + PlanWorkflowService ], }; } diff --git a/frontend/src/app/core/model/plan-workflow/plan-workflow-persist.ts b/frontend/src/app/core/model/plan-workflow/plan-workflow-persist.ts index c2cfe328e..f357fc671 100644 --- a/frontend/src/app/core/model/plan-workflow/plan-workflow-persist.ts +++ b/frontend/src/app/core/model/plan-workflow/plan-workflow-persist.ts @@ -13,6 +13,6 @@ export interface PlanWorkflowDefinitionPersist { } export interface PlanWorkflowDefinitionTransitionPersist { - fromStatus: Guid; - toStatus: Guid; + fromStatusId: Guid; + toStatusId: Guid; } \ No newline at end of file diff --git a/frontend/src/app/core/query/plan-workflow.lookup.ts b/frontend/src/app/core/query/plan-workflow.lookup.ts index 32d579281..d504dc12b 100644 --- a/frontend/src/app/core/query/plan-workflow.lookup.ts +++ b/frontend/src/app/core/query/plan-workflow.lookup.ts @@ -3,19 +3,12 @@ import { Guid } from "@common/types/guid"; import { IsActive } from "../common/enum/is-active.enum"; export class PlanWorkflowLookup extends Lookup implements PlanWorkflowFilter{ - ids: Guid[]; - excludedIds: Guid[]; - like: string; - isActive: IsActive[]; - + tenantId: Guid; constructor() { super(); } } export interface PlanWorkflowFilter { - ids: Guid[]; - excludedIds: Guid[]; - like: string; - isActive: IsActive[]; + tenantId: Guid; } \ No newline at end of file diff --git a/frontend/src/app/core/services/matomo/analytics-service.ts b/frontend/src/app/core/services/matomo/analytics-service.ts index f793f3179..ba00c0dec 100644 --- a/frontend/src/app/core/services/matomo/analytics-service.ts +++ b/frontend/src/app/core/services/matomo/analytics-service.ts @@ -22,6 +22,7 @@ export class AnalyticsService { public static DepositEditor: string = 'Admin: TenantConfigurations'; public static FileTransformerEditor: string = 'Admin: TenantConfigurations'; public static LogoEditor: string = 'Admin: TenantConfigurations'; + public static PlanWorkflowEditor: string = 'Admin: TenantConfigurations'; public static ContactContent: string = 'Contact Content'; public static RecentEditedActivity: string = 'Recent DMP Activity'; public static DescriptionEditor: string = 'Description Editor'; diff --git a/frontend/src/app/core/services/plan/plan-status.service.ts b/frontend/src/app/core/services/plan/plan-status.service.ts index 51c66dafd..0a85d6561 100644 --- a/frontend/src/app/core/services/plan/plan-status.service.ts +++ b/frontend/src/app/core/services/plan/plan-status.service.ts @@ -8,6 +8,10 @@ import { QueryResult } from "@common/model/query-result"; import { catchError, Observable, throwError } from "rxjs"; import { PlanStatusPersist } from "@app/core/model/plan-status/plan-status-persist"; import { Guid } from "@common/types/guid"; +import { PlanLookup } from "@app/core/query/plan.lookup"; +import { FilterService } from "@common/modules/text-filter/filter-service"; +import { IsActive } from "@notification-service/core/enum/is-active.enum"; +import { nameof } from "ts-simple-nameof"; @Injectable() export class PlanStatusService { @@ -16,6 +20,7 @@ export class PlanStatusService { constructor( private http: BaseHttpV2Service, private configurationService: ConfigurationService, + private filterService: FilterService ) { } @@ -50,4 +55,30 @@ export class PlanStatusService { .delete(url).pipe( catchError((error: any) => throwError(() => error))); } + + buildLookup(params: { + like?: string, + excludedIds?: Guid[], + ids?: Guid[], + lookupFields?: string[], + size?: number, + order?: string[] + }): PlanStatusLookup { + const {like, excludedIds, ids, lookupFields, size = 100, order} = params; + const lookup = new PlanLookup(); + lookup.isActive = [IsActive.Active]; + + lookup.order = { items: order ?? [nameof(x => x.name)] }; + lookup.page = { size, offset: 0 }; + + if (excludedIds && excludedIds.length > 0) { lookup.excludedIds = excludedIds; } + if (ids && ids.length > 0) { lookup.ids = ids; } + if (like) { lookup.like = this.filterService.transformLike(like); } + if(lookupFields){ + lookup.project = { + fields: lookupFields + } + }; + return lookup; + } } \ No newline at end of file diff --git a/frontend/src/app/core/services/plan/plan-workflow.service.ts b/frontend/src/app/core/services/plan/plan-workflow.service.ts new file mode 100644 index 000000000..d7ae2fb41 --- /dev/null +++ b/frontend/src/app/core/services/plan/plan-workflow.service.ts @@ -0,0 +1,55 @@ +import { HttpHeaders } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { BaseHttpV2Service } from "../http/base-http-v2.service"; +import { ConfigurationService } from "../configuration/configuration.service"; +import { PlanWorkflowPersist } from "@app/core/model/plan-workflow/plan-workflow-persist"; +import { PlanWorkflow } from "@app/core/model/plan-workflow/plan-workflow"; +import { catchError, map, Observable, of, throwError } from "rxjs"; +import { PlanWorkflowLookup } from "@app/core/query/plan-workflow.lookup"; +import { QueryResult } from "@common/model/query-result"; +import { Guid } from "@common/types/guid"; + +@Injectable() +export class PlanWorkflowService { + private headers = new HttpHeaders(); + + constructor( + private http: BaseHttpV2Service, + private configurationService: ConfigurationService, + ) { + } + + private get apiBase(): string { return `${this.configurationService.server}plan-workflow`; } + + getCurrent(reqFields: string[] = []): Observable { + const url = `${this.apiBase}/current-tenant`; + const options = { params: { f: reqFields } }; + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(() => error))); + } + + query(q: PlanWorkflowLookup): Observable> { + const url = `${this.apiBase}/query`; + return this.http.post>(url, q).pipe( + catchError((error: any) => throwError(() => error)) + ); + } + + + persist(item: PlanWorkflowPersist): Observable { + const url = `${this.apiBase}/persist`; + + return this.http + .post(url, item).pipe( + catchError((error: any) => throwError(() => error))); + } + + delete(id: Guid): Observable { + const url = `${this.apiBase}/${id}`; + + return this.http + .delete(url).pipe( + catchError((error: any) => throwError(() => error))); + } +} \ No newline at end of file diff --git a/frontend/src/app/ui/admin/plan-status/editor/plan-status-editor/plan-status-editor.model.ts b/frontend/src/app/ui/admin/plan-status/editor/plan-status-editor/plan-status-editor.model.ts index 5ded80ec6..2caf3dd66 100644 --- a/frontend/src/app/ui/admin/plan-status/editor/plan-status-editor/plan-status-editor.model.ts +++ b/frontend/src/app/ui/admin/plan-status/editor/plan-status-editor/plan-status-editor.model.ts @@ -95,6 +95,7 @@ export class PlanStatusEditorModel extends BaseEditorModel implements PlanStatus export interface PlanStatusForm { id: FormControl; + hash: FormControl; name: FormControl; description: FormControl; internalStatus: FormControl; diff --git a/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor.model.ts b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor.model.ts new file mode 100644 index 000000000..35b889073 --- /dev/null +++ b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor.model.ts @@ -0,0 +1,125 @@ +import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from "@angular/forms"; +import { PlanWorkflow } from "@app/core/model/plan-workflow/plan-workflow"; +import { PlanWorkflowDefinitionPersist, PlanWorkflowPersist } from "@app/core/model/plan-workflow/plan-workflow-persist"; +import { BaseEditorModel } from "@common/base/base-form-editor-model"; +import { BackendErrorValidator } from "@common/forms/validation/custom-validator"; +import { Validation, ValidationContext } from "@common/forms/validation/validation-context"; +import { Guid } from "@common/types/guid"; + +export class PlanWorkflowEditorModel extends BaseEditorModel implements PlanWorkflowPersist { + name: string = 'default'; + description: string; + definition: PlanWorkflowDefinitionPersist; + + public fromModel(item: PlanWorkflow): PlanWorkflowEditorModel { + if(item){ + super.fromModel(item); + this.description = item.description; + this.definition = { + startingStatusId: item.definition?.startingStatus?.id ?? null, + statusTransitions: [] + }; + item.definition?.statusTransitions?.forEach((st) => + this.definition.statusTransitions.push({ + fromStatusId: st.fromStatus?.id, + toStatusId: st.toStatus?.id + }) + ) + } + + return this; + } + + buildForm(params: {context?: ValidationContext, disabled?: boolean}): FormGroup { + const {context, disabled = false} = params; + const mainContext = context ?? this.createValidationContext(); + const formGroup = new FormGroup({ + id: new FormControl({value: this.id, disabled }, mainContext.getValidation('id').validators), + hash: new FormControl({value: this.hash, disabled }, mainContext.getValidation('hash').validators), + name: new FormControl({value: this.name, disabled}, mainContext.getValidation('name').validators), + description: new FormControl({value: this.description, disabled}, mainContext.getValidation('description').validators), + definition: new FormGroup({ + startingStatusId: new FormControl({value: this.definition?.startingStatusId, disabled}, mainContext.getValidation('startingStatusId').validators), + statusTransitions: new FormArray>([], mainContext.getValidation('statusTransitions').validators) + }, mainContext.getValidation('definition').validators) + }) + + this.definition?.statusTransitions?.forEach((st, index) => { + const itemContext = context ?? this.createValidationContext(`StatusTransition[${index}]`) + formGroup.controls.definition.controls.statusTransitions.push(new FormGroup({ + fromStatusId: new FormControl({value: st.fromStatusId,disabled}, itemContext.getValidation('fromStatusId').validators), + toStatusId: new FormControl({value: st.toStatusId,disabled}, itemContext.getValidation('fromStatusId').validators) + }, this.differentStatusId())) + }) + + return formGroup; + } + + buildStatusTransitionForm(index: number): FormGroup { + const itemContext = this.createValidationContext(`StatusTransition[${index}]`) + return new FormGroup({ + fromStatusId: new FormControl(null, itemContext.getValidation('fromStatusId').validators), + toStatusId: new FormControl(null, itemContext.getValidation('toStatusId').validators) + }, this.differentStatusId()) + } + + reApplyDefinitionValidators(formGroup: FormGroup) { + if(!formGroup?.controls?.statusTransitions?.length) { return; } + formGroup.controls.statusTransitions.controls.forEach((st, index) => { + const context = this.createValidationContext(`StatusTransition[${index}]`); + st.clearValidators(); + st.addValidators(this.differentStatusId()); + st.controls.fromStatusId.clearValidators(); + st.controls.fromStatusId.addValidators(context.getValidation('fromStatusId').validators); + st.controls.toStatusId.clearValidators(); + st.controls.toStatusId.addValidators(context.getValidation('toStatusId').validators); + }) + } + + createValidationContext(rootPath?: string): ValidationContext { + const baseContext: ValidationContext = new ValidationContext(); + const baseValidationArray: Validation[] = new Array(); + baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, `${rootPath}id`)] }); + baseValidationArray.push({ key: 'name', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, `${rootPath}name`)] }); + baseValidationArray.push({ key: 'description', validators: [BackendErrorValidator(this.validationErrorModel, `${rootPath}description`)] }); + baseValidationArray.push({ key: 'hash', validators: [] }); + baseValidationArray.push({ key: 'definition', validators: [BackendErrorValidator(this.validationErrorModel, `${rootPath}definition`)] }); + baseValidationArray.push({ key: 'statusTransitions', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, `${rootPath}statusTransitions`)] }); + baseValidationArray.push({ key: 'startingStatusId', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, `${rootPath}startingStatusId`)] }); + baseValidationArray.push({ key: 'fromStatusId', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, `${rootPath}fromStatusId`)] }); + baseValidationArray.push({ key: 'toStatusId', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, `${rootPath}toStatusId`)] }); + + baseContext.validation = baseValidationArray; + return baseContext; + } + + differentStatusId(): ValidatorFn { + return (formGroup: FormGroup): { [key: string]: any } => { + const fromStatusId = formGroup?.controls?.fromStatusId?.value; + const toStatusId = formGroup?.controls?.toStatusId?.value; + if(!fromStatusId || !toStatusId){ + return null; + } + return fromStatusId?.toString().toLowerCase() == toStatusId?.toString()?.toLowerCase() ? + {'differentStatusId': true} : null + } + } +} + +export interface PlanWorkflowForm { + id: FormControl; + hash: FormControl; + name: FormControl; + description: FormControl; + definition: FormGroup; +} + +export interface PlanWorkflowDefinitionForm { + startingStatusId: FormControl; + statusTransitions: FormArray> +} + +export interface PlanWorkflowDefinitionTransitionForm { + fromStatusId: FormControl; + toStatusId: FormControl; +} \ No newline at end of file diff --git a/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor.resolver.ts b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor.resolver.ts new file mode 100644 index 000000000..60dd1d652 --- /dev/null +++ b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor.resolver.ts @@ -0,0 +1,40 @@ +import { Injectable } from "@angular/core"; +import { PlanWorkflow, PlanWorkflowDefinition, PlanWorkflowDefinitionTransition } from "@app/core/model/plan-workflow/plan-workflow"; +import { PlanWorkflowService } from "@app/core/services/plan/plan-workflow.service"; +import { BaseEditorResolver } from "@common/base/base-editor.resolver"; +import { takeUntil } from "rxjs"; +import { nameof } from "ts-simple-nameof"; + +@Injectable() +export class PlanWorkflowEditorResolver extends BaseEditorResolver { + + constructor(private planWorkflowService: PlanWorkflowService) { + super(); + } + + public static lookupFields(): string[] { + return [ + ...BaseEditorResolver.lookupFields(), + nameof(x => x.name), + nameof(x => x.description), + nameof(x => x.definition), + + [nameof(x => x.definition), nameof(x => x.startingStatus.id)].join('.'), + [nameof(x => x.definition), nameof(x => x.startingStatus.name)].join('.'), + [nameof(x => x.definition), nameof(x => x.statusTransitions)].join('.'), + [nameof(x => x.definition), nameof(x => x.statusTransitions), nameof(x => x.fromStatus.id)].join('.'), + [nameof(x => x.definition), nameof(x => x.statusTransitions), nameof(x => x.fromStatus.name)].join('.'), + [nameof(x => x.definition), nameof(x => x.statusTransitions), nameof(x => x.toStatus.id)].join('.'), + [nameof(x => x.definition), nameof(x => x.statusTransitions), nameof(x => x.toStatus.name)].join('.'), + ] + } + + resolve() { + + const fields = [ + ...PlanWorkflowEditorResolver.lookupFields() + ]; + + return this.planWorkflowService.getCurrent(fields).pipe(takeUntil(this._destroyed)); + } +} diff --git a/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.html b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.html new file mode 100644 index 000000000..b5f68ee93 --- /dev/null +++ b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.html @@ -0,0 +1,86 @@ +
+
+
+ + {{'PLAN-WORKFLOW-EDITOR.FIELDS.STARTING-STATUS' | translate}}* + + {{definitionForm?.controls?.startingStatusId.getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ +
+
+ + {{'PLAN-WORKFLOW-EDITOR.ERRORS.STATUS-TRANSITION-REQUIRED' | translate}} + + @for(transitionForm of definitionForm?.controls?.statusTransitions?.controls; track transitionForm; let index = $index){ +
+
+
+ {{'PLAN-WORKFLOW-EDITOR.FIELDS.STATUS-TRANSITION' | translate}} {{index + 1}} +
+
+ +
+
+
+
+ + {{'PLAN-WORKFLOW-EDITOR.FIELDS.FROM-STATUS' | translate}}* + + {{transitionForm?.controls?.fromStatusId.getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ + {{'PLAN-WORKFLOW-EDITOR.FIELDS.TO-STATUS' | translate}}* + + {{transitionForm?.controls?.toStatusId.getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+ {{'PLAN-WORKFLOW-EDITOR.ERRORS.DIFFERENT-STATUS' | translate}} + } +
+
+
+
+ @if(canDelete){ +
+ +
+ } + @if(canSave){ +
+ +
+ } + +
+
+
+
diff --git a/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.scss b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.scss new file mode 100644 index 000000000..f70521df1 --- /dev/null +++ b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.scss @@ -0,0 +1,24 @@ +.plan-workflow { + .action-btn { + border-radius: 30px; + background-color: var(--secondary-color); + border: 1px solid transparent; + padding-left: 2em; + padding-right: 2em; + box-shadow: 0px 3px 6px #1E202029; + + transition-property: background-color, color; + transition-duration: 200ms; + transition-delay: 50ms; + transition-timing-function: ease-in-out; + &:disabled{ + background-color: #CBCBCB; + color: #FFF; + border: 0px; + } + } +} + +::ng-deep label { + margin: 0; +} diff --git a/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.ts b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.ts new file mode 100644 index 000000000..f14279dee --- /dev/null +++ b/frontend/src/app/ui/admin/tenant-configuration/editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component.ts @@ -0,0 +1,211 @@ +import { Component, OnInit } from '@angular/core'; +import { BasePendingChangesComponent } from '@common/base/base-pending-changes.component'; +import { map, Observable, takeUntil } from 'rxjs'; +import { PlanWorkflowDefinitionForm, PlanWorkflowEditorModel, PlanWorkflowForm } from '../plan-workflow-editor.model'; +import { FormGroup } from '@angular/forms'; +import { PlanWorkflowService } from '@app/core/services/plan/plan-workflow.service'; +import { AnalyticsService } from '@app/core/services/matomo/analytics-service'; +import { PlanWorkflow } from '@app/core/model/plan-workflow/plan-workflow'; +import { PlanWorkflowEditorResolver } from '../plan-workflow-editor.resolver'; +import { HttpErrorResponse } from '@angular/common/http'; +import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; +import { FormService } from '@common/forms/form-service'; +import { TranslateService } from '@ngx-translate/core'; +import { PlanWorkflowPersist } from '@app/core/model/plan-workflow/plan-workflow-persist'; +import { MatDialog } from '@angular/material/dialog'; +import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { PlanStatusEnum } from '@app/core/common/enum/plan-status'; +import { PlanStatus } from '@app/core/model/plan-status/plan-status'; +import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration'; +import { PlanStatusService } from '@app/core/services/plan/plan-status.service'; +import { nameof } from 'ts-simple-nameof'; +import { Guid } from '@common/types/guid'; + +@Component({ + selector: 'app-plan-workflow-editor', + templateUrl: './plan-workflow-editor.component.html', + styleUrl: './plan-workflow-editor.component.scss' +}) +export class PlanWorkflowEditorComponent extends BasePendingChangesComponent implements OnInit{ + formGroup: FormGroup; + editorModel: PlanWorkflowEditorModel; + + constructor( + protected dialog: MatDialog, + protected language: TranslateService, + protected formService: FormService, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected authService: AuthService, + protected enumUtils: EnumUtils, + private logger: LoggingService, + private planWorkflowService: PlanWorkflowService, + private analyticsService: AnalyticsService, + private planStatusService: PlanStatusService + ) { + super(); + } + + ngOnInit(): void { + this.analyticsService.trackPageView(AnalyticsService.PlanWorkflowEditor); + this.getItem((entity) => { + this.prepareForm(entity); + if (this.formGroup && this.editorModel.belongsToCurrentTenant == false) { + this.formGroup.disable(); + } + }); + } + + protected getItem(successFunction: (item: PlanWorkflow) => void) { + this.planWorkflowService.getCurrent(PlanWorkflowEditorResolver.lookupFields()) + .pipe(takeUntil(this._destroyed)) + .subscribe({ + next: (data) => successFunction(data), + error: (error) => this.onCallbackError(error) + }); + } + + protected prepareForm(data: PlanWorkflow) { + try { + this.editorModel = data ? new PlanWorkflowEditorModel().fromModel(data) : new PlanWorkflowEditorModel(); + this.formGroup = this.editorModel.buildForm({disabled: !this.authService.hasPermission(AppPermission.EditPlanWorkflow)}); + + } catch (error) { + this.logger.error('Could not parse PlanWorkflow item: ' + data + error); + this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error); + } + } + + protected refreshData(): void { + this.getItem((entity) => { + this.prepareForm(entity); + if (this.formGroup && this.editorModel.belongsToCurrentTenant == false) { + this.formGroup.disable(); + } + }); + } + + + protected get canDelete(): boolean { + return this.editorModel?.id && this.authService.hasPermission(this.authService.permissionEnum.DeletePlanWorkflow); + } + + protected get canSave(): boolean { + return this.formGroup.touched && this.authService.hasPermission(this.authService.permissionEnum.EditPlanWorkflow); + } + + protected get definitionForm(): FormGroup { + return this.formGroup?.controls?.definition; + } + + protected onCallbackError(errorResponse: HttpErrorResponse) { + this.httpErrorHandlingService.handleBackedRequestError(errorResponse) + + const error: HttpError = this.httpErrorHandlingService.getError(errorResponse); + if (error.statusCode === 400) { + this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error); + this.formService.validateAllFormFields(this.formGroup); + } + } + + protected onCallbackSuccess(): void { + this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success); + this.refreshData(); + } + + formSubmit(): void { + this.editorModel.validationErrorModel.clear(); + this.formService.removeAllBackEndErrors(this.formGroup); + + this.formService.touchAllFormFields(this.formGroup); + this.formService.validateAllFormFields(this.formGroup); + + if (!this.formGroup?.valid) { + return; + } + + this.persistEntity(); + } + + persistEntity(): void { + const formData = JSON.parse(JSON.stringify(this.formGroup.value)) as PlanWorkflowPersist; + + + + this.planWorkflowService.persist(formData) + .pipe(takeUntil(this._destroyed)).subscribe({ + complete: () => this.onCallbackSuccess(), + error: (error) => this.onCallbackError(error) + }); + } + + delete() { + const value = this.formGroup.value; + if (value.id) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + maxWidth: '300px', + data: { + message: this.language.instant('TENANT-CONFIGURATION-EDITOR.RESET-TO-DEFAULT-DIALOG.RESET-TO-DEFAULT'), + confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'), + cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL') + } + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { + if (result) { + this.planWorkflowService.delete(value.id).pipe(takeUntil(this._destroyed)) + .subscribe({ + complete: () => { + this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-RESET'), SnackBarNotificationLevel.Success); + this.prepareForm(null); + }, + error: (error) => this.onCallbackError(error) + }); + } + }); + } + } + + addStatusTransition(){ + const index = this.formGroup.controls.definition.controls.statusTransitions.length; + this.formGroup.controls.definition.controls.statusTransitions.push(this.editorModel.buildStatusTransitionForm(index)); + this.formGroup.markAsTouched(); + } + + removeStatusTransition(index: number){ + this.formGroup.controls.definition.controls.statusTransitions.removeAt(index); + this.editorModel.reApplyDefinitionValidators(this.formGroup.controls.definition); + this.formGroup.markAsTouched(); + } + + canDeactivate(): boolean | Observable { + return this.formGroup ? !this.formGroup.dirty : true; + } + + private planStatusLookupFields = [ + nameof(x => x.id), + nameof(x => x.name), + ] + + planStatusAutoCompleteConfiguration: SingleAutoCompleteConfiguration = { + initialItems: (data?: any) => this.planStatusService.query( + this.planStatusService.buildLookup({ + size: 20, lookupFields: this.planStatusLookupFields + })).pipe(map(x => x.items)), + filterFn: (searchQuery: string, data?: any) => this.planStatusService.query( + this.planStatusService.buildLookup({ + size: 20, + like: searchQuery, + lookupFields: this.planStatusLookupFields + }) + ).pipe(map(x => x.items)), + getSelectedItem: (id: Guid) => this.planStatusService.getSingle(id, this.planStatusLookupFields), + displayFn: (item: PlanStatus) => item.name, + titleFn: (item: PlanStatus) => item.name, + valueAssign: (item: PlanStatus) => {this.formGroup.markAsTouched(); return item.id}, + }; +} diff --git a/frontend/src/app/ui/admin/tenant-configuration/editor/tenant-configuration-editor.component.html b/frontend/src/app/ui/admin/tenant-configuration/editor/tenant-configuration-editor.component.html index a51afec06..a6f15f72e 100644 --- a/frontend/src/app/ui/admin/tenant-configuration/editor/tenant-configuration-editor.component.html +++ b/frontend/src/app/ui/admin/tenant-configuration/editor/tenant-configuration-editor.component.html @@ -73,7 +73,15 @@ - + + + {{'TENANT-CONFIGURATION-EDITOR.PLAN-WORKFLOW.TITLE' | translate}} + {{'TENANT-CONFIGURATION-EDITOR.PLAN-WORKFLOW.HINT' | translate}} + + + + + diff --git a/frontend/src/app/ui/admin/tenant-configuration/tenant-configuration.module.ts b/frontend/src/app/ui/admin/tenant-configuration/tenant-configuration.module.ts index c3027a6cb..a3251ca04 100644 --- a/frontend/src/app/ui/admin/tenant-configuration/tenant-configuration.module.ts +++ b/frontend/src/app/ui/admin/tenant-configuration/tenant-configuration.module.ts @@ -20,6 +20,7 @@ import { FileTransformerEditorComponent } from './editor/file-transformer/file-t import { LogoEditorComponent } from './editor/logo/logo-editor.component'; import { NgxColorsModule } from 'ngx-colors'; import { NotifierListModule } from '@notification-service/ui/admin/tenant-configuration/notifier-list/notifier-list-editor.module'; +import { PlanWorkflowEditorComponent } from './editor/plan-workflow/plan-workflow-editor/plan-workflow-editor.component'; @NgModule({ imports: [ @@ -45,7 +46,8 @@ import { NotifierListModule } from '@notification-service/ui/admin/tenant-config DefaultUserLocaleEditorComponent, DepositEditorComponent, FileTransformerEditorComponent, - LogoEditorComponent + LogoEditorComponent, + PlanWorkflowEditorComponent ] }) export class TenantConfigurationModule { } diff --git a/frontend/src/assets/i18n/baq.json b/frontend/src/assets/i18n/baq.json index fd90b0d0c..bafd1baab 100644 --- a/frontend/src/assets/i18n/baq.json +++ b/frontend/src/assets/i18n/baq.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index f4ebad3de..6b9578b21 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index e93c42154..feec2f456 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -450,6 +450,10 @@ "TITLE": "File Transformer Plugins", "HINT": "Change file transformer plugins" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "FIELDS": { "TIMEZONE": "Timezone", "CULTURE": "Format", @@ -479,6 +483,7 @@ "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/es.json b/frontend/src/assets/i18n/es.json index 683e83eba..9d18d3072 100644 --- a/frontend/src/assets/i18n/es.json +++ b/frontend/src/assets/i18n/es.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/gr.json b/frontend/src/assets/i18n/gr.json index dda856ae6..21aeee48f 100644 --- a/frontend/src/assets/i18n/gr.json +++ b/frontend/src/assets/i18n/gr.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/hr.json b/frontend/src/assets/i18n/hr.json index 5502fe0a2..0c7b4fc3e 100644 --- a/frontend/src/assets/i18n/hr.json +++ b/frontend/src/assets/i18n/hr.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/pl.json b/frontend/src/assets/i18n/pl.json index dac686c72..da42016d9 100644 --- a/frontend/src/assets/i18n/pl.json +++ b/frontend/src/assets/i18n/pl.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/pt.json b/frontend/src/assets/i18n/pt.json index 1d9b5a74c..396bfdeb1 100644 --- a/frontend/src/assets/i18n/pt.json +++ b/frontend/src/assets/i18n/pt.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/sk.json b/frontend/src/assets/i18n/sk.json index a4121a541..f88cc639e 100644 --- a/frontend/src/assets/i18n/sk.json +++ b/frontend/src/assets/i18n/sk.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/sr.json b/frontend/src/assets/i18n/sr.json index cbe8adf06..3d5df559d 100644 --- a/frontend/src/assets/i18n/sr.json +++ b/frontend/src/assets/i18n/sr.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version" diff --git a/frontend/src/assets/i18n/tr.json b/frontend/src/assets/i18n/tr.json index 506bc837b..30eacf6be 100644 --- a/frontend/src/assets/i18n/tr.json +++ b/frontend/src/assets/i18n/tr.json @@ -476,11 +476,16 @@ "RESET-TO-DEFAULT-DIALOG": { "RESET-TO-DEFAULT": "Reset To Default" }, + "PLAN-WORKFLOW": { + "TITLE": "Plan Workflow", + "HINT": "Configure the workflow for this tenant's plans" + }, "ACTIONS": { "SAVE": "Save", "UPLOAD": "Upload", "DOWNLOAD": "Download", "ADD-SOURCE": "Add Source", + "REMOVE-SOURCE": "Remove Source", "RESET-TO-DEFAULT": "Reset To Default" } }, @@ -2522,6 +2527,23 @@ "CONFIRM": "Confirm" } }, + "PLAN-WORKFLOW-EDITOR": { + "ACTIONS": { + "SELECT-PLAN-STATUS": "Select plan status", + "ADD-STATUS-TRANSITION": "Add status transition", + "REMOVE-STATUS-TRANSITION": "Remove status transition" + }, + "FIELDS": { + "STARTING-STATUS": "Starting Status", + "STATUS-TRANSITION": "Status Transition", + "FROM-STATUS": "From Status", + "TO-STATUS": "To Status" + }, + "ERRORS": { + "DIFFERENT-STATUS": "Transition statuses must be different", + "STATUS-TRANSITION-REQUIRED": "At least one Status transition pair must be defined" + } + }, "copy": "Copy", "clone": "Clone", "new-version": "New Version"