AAE-33909 Ensure onProcessFinish event triggers when invoked from onFormLoaded event (#10867)

* AAE-33909 Ensure onProcessFinish event triggers when invoked from onFormLoaded event

* clean code

* fix unit tests

* update outcomes buttons

* update
This commit is contained in:
Bartosz Sekula
2025-05-21 16:01:05 +02:00
committed by GitHub
parent e37ef279e4
commit f2fa458fe5
9 changed files with 104 additions and 34 deletions

View File

@@ -18,6 +18,7 @@
import { Directive, EventEmitter, Input, Output } from '@angular/core'; import { Directive, EventEmitter, Input, Output } from '@angular/core';
import { ThemePalette } from '@angular/material/core'; import { ThemePalette } from '@angular/material/core';
import { FormFieldModel, FormFieldValidator, FormModel, FormOutcomeEvent, FormOutcomeModel } from './widgets'; import { FormFieldModel, FormFieldValidator, FormModel, FormOutcomeEvent, FormOutcomeModel } from './widgets';
import { isOutcomeButtonVisible } from './helpers/buttons-visibility';
@Directive({ @Directive({
standalone: true standalone: true
@@ -103,10 +104,6 @@ export abstract class FormBaseComponent {
*/ */
formStyle: string = ''; formStyle: string = '';
get hasVisibleOutcomes(): boolean {
return this.form?.outcomes?.some((outcome) => this.isOutcomeButtonVisible(outcome, this.form.readOnly));
}
get form(): FormModel { get form(): FormModel {
return this._form; return this._form;
} }
@@ -169,22 +166,7 @@ export abstract class FormBaseComponent {
} }
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean { isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome?.name) { return isOutcomeButtonVisible(outcome, { isFormReadOnly, showCompleteButton: this.showCompleteButton, showSaveButton: this.showSaveButton });
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return this.showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
} }
/** /**

View File

@@ -73,6 +73,7 @@ export class FormFieldComponent implements OnInit, OnDestroy {
if (w.adf === undefined) { if (w.adf === undefined) {
w.adf = {}; w.adf = {};
} }
const originalField = this.getField(); const originalField = this.getField();
if (originalField) { if (originalField) {
const customTemplate = this.field.form.customFieldTemplates[originalField.type]; const customTemplate = this.field.form.customFieldTemplates[originalField.type];

View File

@@ -40,7 +40,8 @@
<section class="adf-grid-list-column-view" *ngIf="currentRootElement?.isExpanded"> <section class="adf-grid-list-column-view" *ngIf="currentRootElement?.isExpanded">
<div class="adf-grid-list-single-column" <div class="adf-grid-list-single-column"
*ngFor="let column of currentRootElement?.columns" *ngFor="let column of currentRootElement?.columns"
[style.width.%]="getColumnWidth(currentRootElement)"> [style.width.%]="getColumnWidth(currentRootElement)"
>
<ng-container *ngFor="let field of column?.fields"> <ng-container *ngFor="let field of column?.fields">
<ng-container *ngIf="field.type === 'section'; else formField"> <ng-container *ngIf="field.type === 'section'; else formField">
<adf-form-section [field]="field"/> <adf-form-section [field]="field"/>
@@ -61,9 +62,11 @@
</div> </div>
</ng-template> </ng-template>
</div> </div>
<div *ngIf="currentRootElement.type === 'dynamic-table'" class="adf-container-widget"> <div *ngIf="currentRootElement.type === 'dynamic-table'" class="adf-container-widget">
<adf-form-field [field]="currentRootElement" /> <adf-form-field [field]="currentRootElement" />
</div> </div>
<div class="adf-container-widget" <div class="adf-container-widget"
*ngIf="currentRootElement.type === 'readonly' && currentRootElement.field.params.field.type === 'dynamic-table'"> *ngIf="currentRootElement.type === 'readonly' && currentRootElement.field.params.field.type === 'dynamic-table'">
<adf-form-field [field]="currentRootElement.field"/> <adf-form-field [field]="currentRootElement.field"/>

View File

@@ -0,0 +1,45 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FormOutcomeModel } from '../widgets';
interface IsOutcomeButtonVisibleProps {
isFormReadOnly: boolean;
showCompleteButton: boolean;
showSaveButton: boolean;
}
export const isOutcomeButtonVisible = (outcome: FormOutcomeModel, props: IsOutcomeButtonVisibleProps): boolean => {
const { isFormReadOnly, showCompleteButton, showSaveButton } = props;
if (outcome?.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
};

View File

@@ -23,6 +23,7 @@ export * from './components/form-renderer.component';
export * from './components/widgets'; export * from './components/widgets';
export * from './components/middlewares/middleware'; export * from './components/middlewares/middleware';
export * from './components/middlewares/decimal-middleware.service'; export * from './components/middlewares/decimal-middleware.service';
export * from './components/helpers/buttons-visibility';
export * from './services/form-rendering.service'; export * from './services/form-rendering.service';
export * from './services/form.service'; export * from './services/form.service';

View File

@@ -670,12 +670,36 @@ describe('FormCloudComponent', () => {
done(); done();
}); });
const formValues: any[] = []; const formValues: TaskVariableCloud[] = [
{
name: 'var1',
value: 'value1',
id: 'var1',
type: 'string',
hasValue: () => true
}
];
const change = new SimpleChange(null, formValues, false); const change = new SimpleChange(null, formValues, false);
formComponent.data = formValues; formComponent.data = formValues;
formComponent.ngOnChanges({ data: change }); formComponent.ngOnChanges({ data: change });
}); });
it('should not change form if custom form values is empty array', () => {
const formModel = new FormModel({
id: 'id',
taskId: 'task-id',
fields: [{ id: 'field1' }, { id: 'field2' }]
});
formComponent.form = formModel;
const formValues: TaskVariableCloud[] = [];
const change = new SimpleChange(null, formValues, false);
formComponent.ngOnChanges({ data: change });
expect(formComponent.form).toEqual(formModel);
});
it('should save task form and raise corresponding event', () => { it('should save task form and raise corresponding event', () => {
spyOn(formCloudService, 'saveTaskForm').and.callFake( spyOn(formCloudService, 'saveTaskForm').and.callFake(
() => () =>

View File

@@ -242,13 +242,14 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
return; return;
} }
const data = changes['data']; const data = changes['data']?.currentValue;
if (data?.currentValue) { if (data?.length > 0) {
this.refreshFormData(); this.refreshFormData();
return; return;
} }
const formRepresentation = changes['form']; const formRepresentation = changes['form'];
if (formRepresentation?.currentValue) { if (formRepresentation?.currentValue) {
this.form = formRepresentation.currentValue; this.form = formRepresentation.currentValue;
this.onFormLoaded(this.form); this.onFormLoaded(this.form);

View File

@@ -17,13 +17,16 @@
class="adf-process-input-container" class="adf-process-input-container"
floatLabel="always" floatLabel="always"
*ngIf="showSelectProcessDropdown" *ngIf="showSelectProcessDropdown"
data-automation-id="adf-select-cloud-process-dropdown"> data-automation-id="adf-select-cloud-process-dropdown"
>
<mat-label class="adf-start-process-input-label">{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.TYPE' | translate }}</mat-label> <mat-label class="adf-start-process-input-label">{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.TYPE' | translate }}</mat-label>
<input <input
matInput matInput
formControlName="processDefinition" formControlName="processDefinition"
[matAutocomplete]="auto" [matAutocomplete]="auto"
id="processDefinitionName"> id="processDefinitionName"
>
<div class="adf-process-input-autocomplete"> <div class="adf-process-input-autocomplete">
<mat-autocomplete <mat-autocomplete
#auto="matAutocomplete" #auto="matAutocomplete"
@@ -37,6 +40,7 @@
{{ getProcessDefinitionValue(processDef) }} {{ getProcessDefinitionValue(processDef) }}
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<button <button
id="adf-select-process-dropdown" id="adf-select-process-dropdown"
title="{{'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.SELECT_PROCESS_DROPDOWN' | translate}}" title="{{'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.SELECT_PROCESS_DROPDOWN' | translate}}"
@@ -44,6 +48,7 @@
(click)="displayDropdown($event)"> (click)="displayDropdown($event)">
<mat-icon>arrow_drop_down</mat-icon> <mat-icon>arrow_drop_down</mat-icon>
</button> </button>
</div> </div>
<mat-error <mat-error
*ngIf="processDefinition.hasError('required')" *ngIf="processDefinition.hasError('required')"
@@ -79,8 +84,8 @@
[data]="resolvedValues" [data]="resolvedValues"
[formId]="processDefinitionCurrent.formKey" [formId]="processDefinitionCurrent.formKey"
[displayModeConfigurations]="displayModeConfigurations" [displayModeConfigurations]="displayModeConfigurations"
[showSaveButton]="false" [showSaveButton]="showSaveButton"
[showCompleteButton]="false" [showCompleteButton]="showCompleteButton"
[showRefreshButton]="false" [showRefreshButton]="false"
[showValidationIcon]="false" [showValidationIcon]="false"
[showTitle]="false" [showTitle]="false"

View File

@@ -35,7 +35,8 @@ import {
FormModel, FormModel,
InplaceFormInputComponent, InplaceFormInputComponent,
LocalizedDatePipe, LocalizedDatePipe,
TranslationService TranslationService,
isOutcomeButtonVisible
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms'; import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';
import { MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete';
@@ -95,8 +96,6 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
@ViewChild(MatAutocompleteTrigger) @ViewChild(MatAutocompleteTrigger)
inputAutocomplete: MatAutocompleteTrigger; inputAutocomplete: MatAutocompleteTrigger;
@ViewChild('startForm') startForm: FormCloudComponent;
/** (required) Name of the app. */ /** (required) Name of the app. */
@Input() @Input()
appName: string = ''; appName: string = '';
@@ -207,6 +206,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
private readonly hasVisibleOutcomesSubject = new BehaviorSubject<boolean>(false); private readonly hasVisibleOutcomesSubject = new BehaviorSubject<boolean>(false);
private readonly dialog = inject(MatDialog); private readonly dialog = inject(MatDialog);
showSaveButton = false;
showCompleteButton = false;
get isProcessFormValid(): boolean { get isProcessFormValid(): boolean {
if (this.hasForm && this.isFormCloudLoaded) { if (this.hasForm && this.isFormCloudLoaded) {
return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting; return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting;
@@ -256,6 +258,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
.subscribe((processDefinitionName) => { .subscribe((processDefinitionName) => {
this.selectProcessDefinitionByProcessDefinitionName(processDefinitionName); this.selectProcessDefinitionByProcessDefinitionName(processDefinitionName);
}); });
this.showStartProcessButton$ = combineLatest([this.displayStartSubject, this.hasVisibleOutcomesSubject]).pipe( this.showStartProcessButton$ = combineLatest([this.displayStartSubject, this.hasVisibleOutcomesSubject]).pipe(
map(([displayStart, hasVisibleOutcomes]) => (displayStart !== null ? displayStart === 'true' : !hasVisibleOutcomes)) map(([displayStart, hasVisibleOutcomes]) => (displayStart !== null ? displayStart === 'true' : !hasVisibleOutcomes))
); );
@@ -284,9 +287,14 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
this.isFormCloudLoaded = true; this.isFormCloudLoaded = true;
this.formCloud = form; this.formCloud = form;
if (this.startForm) { const anyOutcomeVisible = form?.outcomes?.some((outcome) =>
this.hasVisibleOutcomesSubject.next(this.startForm.hasVisibleOutcomes); isOutcomeButtonVisible(outcome, {
} isFormReadOnly: form.readOnly,
showCompleteButton: this.showCompleteButton,
showSaveButton: this.showSaveButton
})
);
this.hasVisibleOutcomesSubject.next(anyOutcomeVisible);
} }
private getMaxNameLength(): number { private getMaxNameLength(): number {