diff --git a/docs/process-services-cloud/components/form-cloud.component.md b/docs/process-services-cloud/components/form-cloud.component.md index 709cad1d37..7a5771c6de 100644 --- a/docs/process-services-cloud/components/form-cloud.component.md +++ b/docs/process-services-cloud/components/form-cloud.component.md @@ -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. | diff --git a/lib/core/src/lib/form/components/helpers/buttons-visibility.spec.ts b/lib/core/src/lib/form/components/helpers/buttons-visibility.spec.ts new file mode 100644 index 0000000000..c201701a16 --- /dev/null +++ b/lib/core/src/lib/form/components/helpers/buttons-visibility.spec.ts @@ -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 => ({ 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); + }); +}); diff --git a/lib/core/src/lib/form/components/helpers/buttons-visibility.ts b/lib/core/src/lib/form/components/helpers/buttons-visibility.ts index cb2c431b7b..bd06b6e938 100644 --- a/lib/core/src/lib/form/components/helpers/buttons-visibility.ts +++ b/lib/core/src/lib/form/components/helpers/buttons-visibility.ts @@ -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; diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.html b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.html index 7405cda37c..291741c0bd 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.html +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.html @@ -121,20 +121,17 @@ }
- @for (outcome of form.outcomes; track outcome.name) { - @if (outcome.isVisible) { - - } + @for (outcome of visibleOutcomes; track outcome.name) { + }
diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts index 9eeb110e94..82918b8d80 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts @@ -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); }); diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts index 2eca9be65d..927ae9ef67 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts @@ -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.