AAE-44435 Fix incorrect Start process button on user task forms (#11837)

This commit is contained in:
Alex Molodyh
2026-05-20 14:19:24 +05:30
committed by Anamika Dey
parent efa19245f3
commit fd9313ec16
6 changed files with 145 additions and 20 deletions
@@ -89,7 +89,7 @@ The template defined inside `empty-form` will be shown when no form definition i
| path | `string` | | Path of the folder where the metadata will be stored. |
| processInstanceId | `string` | | ProcessInstanceId id to fetch corresponding form and values. |
| readOnly | `boolean` | false | Toggle readonly state of the form. Forces all form widgets to render as readonly if enabled. |
| showCompleteButton | `boolean` | true | Toggle rendering of the `Complete` outcome button. |
| showCompleteButton | `boolean` | false | Toggle rendering of the `Complete` outcome button. |
| showRefreshButton | `boolean` | true | Toggle rendering of the `Refresh` button. |
| showSaveButton | `boolean` | true | Toggle rendering of the `Save` outcome button. |
| showTitle | `boolean` | true | Toggle rendering of the form title. |
@@ -0,0 +1,63 @@
/*!
* @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';
import { isOutcomeButtonVisible } from './buttons-visibility';
describe('isOutcomeButtonVisible', () => {
const defaultProps = {
isFormReadOnly: false,
showCompleteButton: true,
showSaveButton: true
};
const outcome = (overrides: Partial<FormOutcomeModel>): FormOutcomeModel => ({ name: '', isSelected: false, ...overrides }) as FormOutcomeModel;
it('should return false when outcome has no name', () => {
expect(isOutcomeButtonVisible(outcome({ name: '' }), defaultProps)).toBe(false);
});
it('should respect showCompleteButton for COMPLETE action', () => {
const o = outcome({ name: FormOutcomeModel.COMPLETE_ACTION });
expect(isOutcomeButtonVisible(o, { ...defaultProps, showCompleteButton: false })).toBe(false);
expect(isOutcomeButtonVisible(o, { ...defaultProps, showCompleteButton: true })).toBe(true);
});
it('should always hide START_PROCESS action regardless of readOnly state', () => {
const o = outcome({ name: FormOutcomeModel.START_PROCESS_ACTION });
expect(isOutcomeButtonVisible(o, { ...defaultProps, isFormReadOnly: false })).toBe(false);
expect(isOutcomeButtonVisible(o, { ...defaultProps, isFormReadOnly: true })).toBe(false);
});
it('should show only selected outcome when form is read-only', () => {
const selected = outcome({ name: 'custom-1', isSelected: true });
const notSelected = outcome({ name: 'custom-2', isSelected: false });
expect(isOutcomeButtonVisible(selected, { ...defaultProps, isFormReadOnly: true })).toBe(true);
expect(isOutcomeButtonVisible(notSelected, { ...defaultProps, isFormReadOnly: true })).toBe(false);
});
it('should respect showSaveButton for SAVE action on writable forms', () => {
const o = outcome({ name: FormOutcomeModel.SAVE_ACTION });
expect(isOutcomeButtonVisible(o, { ...defaultProps, showSaveButton: true })).toBe(true);
expect(isOutcomeButtonVisible(o, { ...defaultProps, showSaveButton: false })).toBe(false);
});
it('should show custom outcomes on writable forms', () => {
const o = outcome({ name: 'custom-outcome' });
expect(isOutcomeButtonVisible(o, defaultProps)).toBe(true);
});
});
@@ -30,15 +30,15 @@ export const isOutcomeButtonVisible = (outcome: FormOutcomeModel, props: IsOutco
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return showCompleteButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
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;
@@ -121,20 +121,17 @@
}
<div class="adf-cloud-form-outcome-buttons">
<ng-content select="adf-cloud-form-custom-outcomes" />
@for (outcome of form.outcomes; track outcome.name) {
@if (outcome.isVisible) {
<button
[id]="'adf-form-' + outcome.name | formatSpace"
[color]="getColorForOutcome(outcome.name)"
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
class="adf-cloud-form-custom-outcome-button"
(click)="onOutcomeClicked(outcome)"
>
{{ getCustomOutcomeButtonText(outcome) || (outcome.name | translate | uppercase) }}
</button>
}
@for (outcome of visibleOutcomes; track outcome.name) {
<button
[id]="'adf-form-' + outcome.name | formatSpace"
[color]="getColorForOutcome(outcome.name)"
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
class="adf-cloud-form-custom-outcome-button"
(click)="onOutcomeClicked(outcome)"
>
{{ getCustomOutcomeButtonText(outcome) || (outcome.name | translate | uppercase) }}
</button>
}
</div>
</mat-card-actions>
@@ -1210,6 +1210,33 @@ describe('FormCloudComponent', () => {
expect(formComponent.isOutcomeButtonEnabled(startProcessOutcome)).toBeTruthy();
});
it('should not include START_PROCESS outcome when form is loaded for a task', () => {
formComponent.taskId = 'mock-task-id';
formComponent.appName = 'mock-app';
const form = formComponent.parseForm(cloudFormMock);
const startProcessOutcome = form.outcomes.find((outcome) => outcome.id === FormModel.START_PROCESS_OUTCOME);
expect(startProcessOutcome).toBeUndefined();
});
it('should populate visibleOutcomes when the form is set', () => {
formComponent.showCompleteButton = true;
formComponent.form = new FormModel(cloudFormMock);
expect(formComponent.visibleOutcomes.length).toBeGreaterThan(0);
expect(formComponent.visibleOutcomes.every((outcome) => outcome.name !== FormOutcomeModel.START_PROCESS_ACTION)).toBe(true);
});
it('should clear visibleOutcomes when the form is cleared', () => {
formComponent.showCompleteButton = true;
formComponent.form = new FormModel(cloudFormMock);
expect(formComponent.visibleOutcomes.length).toBeGreaterThan(0);
formComponent.form = null;
expect(formComponent.visibleOutcomes).toEqual([]);
});
it('should raise [executeOutcome] event for formService', async () => {
spyOn(formComponent.executeOutcome, 'emit');
@@ -1656,6 +1683,7 @@ describe('FormCloudComponent', () => {
describe('Custom outcome button text for default outcomes', () => {
beforeEach(() => {
formComponent.showCompleteButton = true;
formComponent.form = formComponent.parseForm(emptyFormRepresentationJSON);
});
@@ -175,6 +175,19 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
formCloudRepresentationJSON: any;
fieldValidators: FormFieldValidator[] = [];
/** Pre-computed list of outcome buttons to render, filtered by visibility rules. */
visibleOutcomes: FormOutcomeModel[] = [];
override get form(): FormModel {
return super.form;
}
@Input()
override set form(form: FormModel) {
super.form = form;
this.recomputeVisibleOutcomes();
}
readonly id: string;
displayMode: string;
displayConfiguration: FormCloudDisplayModeConfiguration = DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0];
@@ -268,6 +281,13 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
this.disableSaveButton = false;
}
});
this.formService.formRulesEvent
.pipe(
filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === this.form?.id),
takeUntilDestroyed()
)
.subscribe(() => this.recomputeVisibleOutcomes());
}
@HostListener('keydown', ['$event'])
@@ -318,6 +338,10 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
this.setCheckParentVisibilityForValidationOnFields();
this.form.validateForm();
}
if (changes['readOnly'] || changes['showCompleteButton'] || changes['showSaveButton']) {
this.recomputeVisibleOutcomes();
}
}
ngOnInit(): void {
@@ -511,9 +535,11 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
});
const form = new FormModel(formCloudRepresentationJSON, formValues, this.readOnly, this.formService, undefined, this.fieldValidators);
if (!form) {
form.outcomes = this.getFormDefinitionOutcomes(form);
if (this.taskId) {
form.outcomes = (form.outcomes ?? []).filter((outcome) => outcome.id !== FormModel.START_PROCESS_OUTCOME);
}
return form;
}
@@ -533,6 +559,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
checkVisibility(field: FormFieldModel) {
if (field?.form) {
this.visibilityService.refreshVisibility(field.form);
this.recomputeVisibleOutcomes();
}
}
@@ -545,6 +572,16 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
}
}
private recomputeVisibleOutcomes(): void {
const outcomes = this.form?.outcomes;
if (!outcomes) {
this.visibleOutcomes = [];
return;
}
this.visibleOutcomes = outcomes.filter((outcome) => outcome.isVisible && this.isOutcomeButtonVisible(outcome, this.form.readOnly));
}
/**
* Sets the parent visibility check flag on all form fields.
* When enabled, fields inside hidden groups/sections will skip validation.