From d7ec3a1b7753e0b36cf07b3c95887d516b097e92 Mon Sep 17 00:00:00 2001 From: Fabian Kindgen <39992669+fkindgen@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:37:35 +0200 Subject: [PATCH] AAE-35981 Form button customization (#11047) --- .../form/components/form-base.component.ts | 38 ++-- .../widgets/core/form-outcome.model.ts | 6 +- .../components/widgets/core/form.model.ts | 47 ++--- .../src/lib/form/services/form.service.ts | 2 +- .../form/components/form-cloud.component.html | 2 +- .../components/form-cloud.component.spec.ts | 48 +++++ .../form/components/form-cloud.component.ts | 25 ++- .../lib/form/services/form-cloud.service.ts | 2 +- .../task-form-cloud.component.html | 8 +- .../task-form-cloud.component.spec.ts | 104 +++++++++++ .../task-form-cloud.component.ts | 33 ++++ .../user-task-cloud-buttons.component.html | 13 +- .../user-task-cloud-buttons.component.spec.ts | 23 +++ .../user-task-cloud-buttons.component.ts | 7 + .../user-task-cloud.component.html | 13 +- .../user-task-cloud.component.spec.ts | 166 ++++++++++++++++++ .../user-task-cloud.component.ts | 33 ++++ .../src/lib/form/form.component.ts | 2 +- 18 files changed, 516 insertions(+), 56 deletions(-) diff --git a/lib/core/src/lib/form/components/form-base.component.ts b/lib/core/src/lib/form/components/form-base.component.ts index 84205a0328..3ad6c0bbf3 100644 --- a/lib/core/src/lib/form/components/form-base.component.ts +++ b/lib/core/src/lib/form/components/form-base.component.ts @@ -27,12 +27,28 @@ import { isOutcomeButtonVisible } from './helpers/buttons-visibility'; export abstract class FormBaseComponent { protected _form: FormModel; - static SAVE_OUTCOME_ID: string = '$save'; - static COMPLETE_OUTCOME_ID: string = '$complete'; - static START_PROCESS_OUTCOME_ID: string = '$startProcess'; - static CUSTOM_OUTCOME_ID: string = '$custom'; - static COMPLETE_BUTTON_COLOR: ThemePalette = 'primary'; - static COMPLETE_OUTCOME_NAME: string = 'COMPLETE'; + /** + * @deprecated Use {@link FormModel.SAVE_OUTCOME} instead. + */ + static readonly SAVE_OUTCOME_ID: string = FormModel.SAVE_OUTCOME; + + /** + * @deprecated Use {@link FormModel.COMPLETE_OUTCOME} instead. + */ + static readonly COMPLETE_OUTCOME_ID: string = FormModel.COMPLETE_OUTCOME; + + /** + * @deprecated Use {@link FormModel.START_PROCESS_OUTCOME} instead. + */ + static readonly START_PROCESS_OUTCOME_ID: string = FormModel.START_PROCESS_OUTCOME; + + static readonly CUSTOM_OUTCOME_ID: string = '$custom'; + static readonly COMPLETE_BUTTON_COLOR: ThemePalette = 'primary'; + + /** + * @deprecated Use {@link FormOutcomeModel.COMPLETE_ACTION} instead. + */ + static readonly COMPLETE_OUTCOME_NAME: string = FormOutcomeModel.COMPLETE_ACTION; /** Path of the folder where the metadata will be stored. */ @Input() @@ -50,7 +66,7 @@ export abstract class FormBaseComponent { @Input() showCompleteButton: boolean = true; - /** If true then the `Complete` outcome button is shown but it will be disabled. */ + /** If true then the `Complete` outcome button is shown, but it will be disabled. */ @Input() disableCompleteButton: boolean = false; @@ -138,7 +154,7 @@ export abstract class FormBaseComponent { } getColorForOutcome(outcomeName: string): ThemePalette { - return outcomeName === FormBaseComponent.COMPLETE_OUTCOME_NAME ? FormBaseComponent.COMPLETE_BUTTON_COLOR : null; + return outcomeName === FormOutcomeModel.COMPLETE_ACTION ? FormBaseComponent.COMPLETE_BUTTON_COLOR : null; } isOutcomeButtonEnabled(outcome?: FormOutcomeModel): boolean { @@ -182,20 +198,20 @@ export abstract class FormBaseComponent { } if (outcome.isSystem) { - if (outcome.id === FormBaseComponent.SAVE_OUTCOME_ID) { + if (outcome.id === FormModel.SAVE_OUTCOME) { this.disableSaveButton = true; this.saveTaskForm(); return true; } - if (outcome.id === FormBaseComponent.COMPLETE_OUTCOME_ID) { + if (outcome.id === FormModel.COMPLETE_OUTCOME) { this.disableSaveButton = true; this.disableCompleteButton = true; this.completeTaskForm(); return true; } - if (outcome.id === FormBaseComponent.START_PROCESS_OUTCOME_ID) { + if (outcome.id === FormModel.START_PROCESS_OUTCOME) { this.completeTaskForm(); return true; } diff --git a/lib/core/src/lib/form/components/widgets/core/form-outcome.model.ts b/lib/core/src/lib/form/components/widgets/core/form-outcome.model.ts index 035eab0b50..de56ce501a 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-outcome.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-outcome.model.ts @@ -21,9 +21,9 @@ import { FormWidgetModel } from './form-widget.model'; import { WidgetVisibilityModel } from '../../../models/widget-visibility.model'; export class FormOutcomeModel extends FormWidgetModel { - static SAVE_ACTION: string = 'SAVE'; // Activiti 'Save' action name - static COMPLETE_ACTION: string = 'COMPLETE'; // Activiti 'Complete' action name - static START_PROCESS_ACTION: string = 'START PROCESS'; // Activiti 'Start Process' action name + static readonly SAVE_ACTION: string = 'SAVE'; // Activiti 'Save' action name + static readonly COMPLETE_ACTION: string = 'COMPLETE'; // Activiti 'Complete' action name + static readonly START_PROCESS_ACTION: string = 'START PROCESS'; // Activiti 'Start Process' action name isSystem: boolean = false; isSelected: boolean = false; diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.ts b/lib/core/src/lib/form/components/widgets/core/form.model.ts index 2b1a763b9f..bab205e0d7 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.ts @@ -62,10 +62,10 @@ export interface FormRepresentationModel { theme?: ThemeModel; } export class FormModel implements ProcessFormModel { - static UNSET_TASK_NAME: string = 'Nameless task'; - static SAVE_OUTCOME: string = '$save'; - static COMPLETE_OUTCOME: string = '$complete'; - static START_PROCESS_OUTCOME: string = '$startProcess'; + static readonly UNSET_TASK_NAME: string = 'Nameless task'; + static readonly SAVE_OUTCOME: string = '$save'; + static readonly COMPLETE_OUTCOME: string = '$complete'; + static readonly START_PROCESS_OUTCOME: string = '$startProcess'; readonly id: string | number; readonly name: string; @@ -410,27 +410,28 @@ export class FormModel implements ProcessFormModel { } protected parseOutcomes() { - if (this.json.fields) { - const saveOutcome = new FormOutcomeModel(this, { - id: FormModel.SAVE_OUTCOME, - name: 'SAVE', - isSystem: true - }); - const completeOutcome = new FormOutcomeModel(this, { - id: FormModel.COMPLETE_OUTCOME, - name: 'COMPLETE', - isSystem: true - }); - const startProcessOutcome = new FormOutcomeModel(this, { - id: FormModel.START_PROCESS_OUTCOME, - name: 'START PROCESS', - isSystem: true - }); + if (!this.json.fields) return; - const customOutcomes = (this.json.outcomes || []).map((obj) => new FormOutcomeModel(this, obj)); + const saveOutcome = new FormOutcomeModel(this, { + id: FormModel.SAVE_OUTCOME, + name: FormOutcomeModel.SAVE_ACTION, + isSystem: true + }); - this.outcomes = [saveOutcome].concat(customOutcomes.length > 0 ? customOutcomes : [completeOutcome, startProcessOutcome]); - } + const completeOutcome = new FormOutcomeModel(this, { + id: FormModel.COMPLETE_OUTCOME, + name: FormOutcomeModel.COMPLETE_ACTION, + isSystem: true + }); + + const startProcessOutcome = new FormOutcomeModel(this, { + id: FormModel.START_PROCESS_OUTCOME, + name: FormOutcomeModel.START_PROCESS_ACTION, + isSystem: true + }); + + const customOutcomes = (this.json.outcomes ?? ([] as FormModel[])).map((formModel: FormModel) => new FormOutcomeModel(this, formModel)); + this.outcomes = [saveOutcome].concat(customOutcomes.length > 0 ? customOutcomes : [completeOutcome, startProcessOutcome]); } addValuesNotPresent(valuesToSetIfNotPresent: FormValues) { diff --git a/lib/core/src/lib/form/services/form.service.ts b/lib/core/src/lib/form/services/form.service.ts index 38dbe187e1..d562e3973c 100644 --- a/lib/core/src/lib/form/services/form.service.ts +++ b/lib/core/src/lib/form/services/form.service.ts @@ -81,7 +81,7 @@ export class FormService implements FormValidationService { if (!json.fields) { form.outcomes = [ new FormOutcomeModel(form, { - id: '$save', + id: FormModel.SAVE_OUTCOME, name: FormOutcomeModel.SAVE_ACTION, isSystem: true }) 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 a35ee80ea3..9d3ecfa643 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 @@ -91,7 +91,7 @@ class="adf-cloud-form-custom-outcome-button" (click)="onOutcomeClicked(outcome)" > - {{ outcome.name | translate | uppercase }} + {{ getCustomOutcomeButtonText(outcome) || (outcome.name | translate | uppercase) }} 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 6c17dfb6da..a932cec322 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 @@ -1492,6 +1492,54 @@ describe('FormCloudComponent', () => { }); }); }); + + describe('Custom outcome button text for default outcomes', () => { + beforeEach(() => { + formComponent.form = formComponent.parseForm(emptyFormRepresentationJSON); + }); + + it('should display custom save button text when set', () => { + const customText = 'Custom Save Text'; + formComponent.customSaveButtonText = customText; + + fixture.detectChanges(); + + const buttonSelector = '#adf-form-save'; + const outcomeButton = fixture.debugElement.query(By.css(buttonSelector)); + expect(outcomeButton).toBeTruthy(); + expect(outcomeButton.nativeElement.textContent.trim()).toBe(customText); + }); + + it('should display default save button text when not set', () => { + fixture.detectChanges(); + + const buttonSelector = '#adf-form-save'; + const outcomeButton = fixture.debugElement.query(By.css(buttonSelector)); + expect(outcomeButton).toBeTruthy(); + expect(outcomeButton.nativeElement.textContent.trim()).toBe('SAVE'); + }); + + it('should display custom complete button text when set', () => { + const customText = 'Custom Complete Text'; + formComponent.customCompleteButtonText = customText; + + fixture.detectChanges(); + + const buttonSelector = '#adf-form-complete'; + const outcomeButton = fixture.debugElement.query(By.css(buttonSelector)); + expect(outcomeButton).toBeTruthy(); + expect(outcomeButton.nativeElement.textContent.trim()).toBe(customText); + }); + + it('should display default complete button text when not set', () => { + fixture.detectChanges(); + + const buttonSelector = '#adf-form-complete'; + const outcomeButton = fixture.debugElement.query(By.css(buttonSelector)); + expect(outcomeButton).toBeTruthy(); + expect(outcomeButton.nativeElement.textContent.trim()).toBe('COMPLETE'); + }); + }); }); describe('Multilingual Form', () => { 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 bbf44e6cba..f437c4c427 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 @@ -121,6 +121,20 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, @Input() showCompleteButton = false; + /** + * Custom text for the `Save` button. + * If not provided, the default text will be used. + */ + @Input() + customSaveButtonText: string = ''; + + /** + * Custom text for the `Complete` button. + * If not provided, the default text will be used. + */ + @Input() + customCompleteButtonText: string = ''; + /** Emitted when the form is submitted with the `Save` or custom outcomes. */ @Output() formSaved = new EventEmitter(); @@ -298,6 +312,15 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, ); } + getCustomOutcomeButtonText(outcome: FormOutcomeModel): string { + if (outcome?.id === FormModel.SAVE_OUTCOME || outcome?.name === FormOutcomeModel.SAVE_ACTION) { + return this.customSaveButtonText; + } else if (outcome?.id === FormModel.COMPLETE_OUTCOME || outcome?.name === FormOutcomeModel.COMPLETE_ACTION) { + return this.customCompleteButtonText; + } + return ''; + } + isAProcessTask(taskRepresentation: TaskDetailsCloudModel): boolean { return taskRepresentation.processDefinitionId && taskRepresentation.processDefinitionDeploymentId !== 'null'; } @@ -430,7 +453,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, * @returns list of form outcomes */ getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] { - return [new FormOutcomeModel(form, { id: '$save', name: FormOutcomeModel.SAVE_ACTION, isSystem: true })]; + return [new FormOutcomeModel(form, { id: FormModel.SAVE_OUTCOME, name: FormOutcomeModel.SAVE_ACTION, isSystem: true })]; } checkVisibility(field: FormFieldModel) { diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts index 4ba62c4622..dacb861699 100644 --- a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts @@ -210,7 +210,7 @@ export class FormCloudService extends BaseCloudService implements FormCloudServi * * @param json JSON data to create the form * @param data Values for the form's fields - * @param readOnly Toggles whether or not the form should be read-only + * @param readOnly Toggles whether the form should be read-only * @returns Form created from the JSON specification */ parseForm(json: any, data?: TaskVariableCloud[], readOnly: boolean = false): FormModel { diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html index 8aa9e99bf5..0d9a3c64c7 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html @@ -9,9 +9,12 @@ [readOnly]="isReadOnly()" [showRefreshButton]="showRefreshButton" [showValidationIcon]="showValidationIcon" - [showCompleteButton]="canCompleteTask()" - [showSaveButton]="canCompleteTask()" + [showCompleteButton]="showCompleteButton && canCompleteTask()" + [showSaveButton]="showSaveButton && canCompleteTask()" + [customSaveButtonText]="customSaveButtonText" + [customCompleteButtonText]="customCompleteButtonText" [displayModeConfigurations]="displayModeConfigurations" + (formLoaded)="onFormLoaded($event)" (formSaved)="onFormSaved($event)" (formCompleted)="onFormCompleted($event)" (formError)="onError($event)" @@ -27,6 +30,7 @@ [canClaimTask]="canClaimTask()" [canUnclaimTask]="canUnclaimTask()" [showCancelButton]="showCancelButton" + [customCancelButtonText]="customCancelButtonText" [taskId]="taskId" (cancelClick)="onCancelClick()" (claimTask)="onClaimTask()" diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts index 0dc98fc6d9..e51cf355f5 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts @@ -33,6 +33,7 @@ import { import { UserTaskCloudButtonsComponent } from '../user-task-cloud-buttons/user-task-cloud-buttons.component'; import { TaskFormCloudComponent } from './task-form-cloud.component'; import { FormCustomOutcomesComponent } from '../../../../form/components/form-cloud-custom-outcomes.component'; +import { By } from '@angular/platform-browser'; const taskDetails: TaskDetailsCloudModel = { appName: 'simple-app', @@ -191,6 +192,90 @@ describe('TaskFormCloudComponent', () => { const canUnclaimTask = component.canUnclaimTask(); expect(canUnclaimTask).toBe(false); }); + + it('should pass showCompleteButton to adf-cloud-form when task can be completed', () => { + component.appName = 'app1'; + component.taskId = 'task1'; + component.showCompleteButton = true; + spyOn(component, 'canCompleteTask').and.returnValue(true); + fixture.detectChanges(); + + const cloudFormElement = fixture.debugElement.query(By.css('adf-cloud-form')); + expect(cloudFormElement).toBeDefined(); + const cloudFormComponent = cloudFormElement.componentInstance as FormCloudComponent; + expect(cloudFormComponent).toBeDefined(); + expect(cloudFormComponent.showCompleteButton).toBe(true); + }); + + it('should not pass showCompleteButton to adf-cloud-form when task cannot be completed', () => { + component.appName = 'app1'; + component.taskId = 'task1'; + component.showCompleteButton = true; + spyOn(component, 'canCompleteTask').and.returnValue(false); + fixture.detectChanges(); + + const cloudFormElement = fixture.debugElement.query(By.css('adf-cloud-form')); + expect(cloudFormElement).toBeDefined(); + const cloudFormComponent = cloudFormElement.componentInstance as FormCloudComponent; + expect(cloudFormComponent).toBeDefined(); + expect(cloudFormComponent.showCompleteButton).toBe(false); + }); + + it('should pass showSaveButton to adf-cloud-form when task can be completed', () => { + component.appName = 'app1'; + component.taskId = 'task1'; + component.showSaveButton = true; + spyOn(component, 'canCompleteTask').and.returnValue(true); + fixture.detectChanges(); + + const cloudFormElement = fixture.debugElement.query(By.css('adf-cloud-form')); + expect(cloudFormElement).toBeDefined(); + const cloudFormComponent = cloudFormElement.componentInstance as FormCloudComponent; + expect(cloudFormComponent).toBeDefined(); + expect(cloudFormComponent.showSaveButton).toBe(true); + }); + + it('should not pass showSaveButton to adf-cloud-form when task cannot be completed', () => { + component.appName = 'app1'; + component.taskId = 'task1'; + component.showSaveButton = true; + spyOn(component, 'canCompleteTask').and.returnValue(false); + fixture.detectChanges(); + + const cloudFormElement = fixture.debugElement.query(By.css('adf-cloud-form')); + expect(cloudFormElement).toBeDefined(); + const cloudFormComponent = cloudFormElement.componentInstance as FormCloudComponent; + expect(cloudFormComponent).toBeDefined(); + expect(cloudFormComponent.showSaveButton).toBe(false); + }); + + it('should pass customSaveButtonText to adf-cloud-form', () => { + const customText = 'Custom Save'; + component.appName = 'app1'; + component.taskId = 'task1'; + component.customSaveButtonText = customText; + fixture.detectChanges(); + + const cloudFormElement = fixture.debugElement.query(By.css('adf-cloud-form')); + expect(cloudFormElement).toBeDefined(); + const cloudFormComponent = cloudFormElement.componentInstance as FormCloudComponent; + expect(cloudFormComponent).toBeDefined(); + expect(cloudFormComponent.customSaveButtonText).toBe(customText); + }); + + it('should pass customCompleteButtonText to adf-cloud-form', () => { + const customText = 'Custom Complete'; + component.appName = 'app1'; + component.taskId = 'task1'; + component.customCompleteButtonText = customText; + fixture.detectChanges(); + + const cloudFormElement = fixture.debugElement.query(By.css('adf-cloud-form')); + expect(cloudFormElement).toBeDefined(); + const cloudFormComponent = cloudFormElement.componentInstance as FormCloudComponent; + expect(cloudFormComponent).toBeDefined(); + expect(cloudFormComponent.customCompleteButtonText).toBe(customText); + }); }); describe('Events', () => { @@ -254,6 +339,25 @@ describe('TaskFormCloudComponent', () => { expect(component.displayModeOff.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); }); + + it('should emit formLoaded when form is loaded', () => { + const mockForm = new FormModel(); + spyOn(component.formLoaded, 'emit').and.stub(); + component.onFormLoaded(mockForm); + + expect(component.formLoaded.emit).toHaveBeenCalledOnceWith(mockForm); + }); + + it('should handle formLoaded event from adf-cloud-form and re-emit it', () => { + const mockForm = new FormModel(); + spyOn(component.formLoaded, 'emit').and.stub(); + + // Trigger the formLoaded event from the child adf-cloud-form component + const cloudFormElement = fixture.debugElement.query((sel) => sel.name === 'adf-cloud-form'); + cloudFormElement.triggerEventHandler('formLoaded', mockForm); + + expect(component.formLoaded.emit).toHaveBeenCalledOnceWith(mockForm); + }); }); it('should call children cloud task form change display mode when changing the display mode', () => { diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts index d9afca3e69..411099be13 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts @@ -72,6 +72,31 @@ export class TaskFormCloudComponent { @Input() showCompleteButton = true; + /** Toggle rendering of the `Save` button. */ + @Input() + showSaveButton = true; + + /** + * Custom text for the `Cancel` button. + * If not provided, the default text will be used. + */ + @Input() + customCancelButtonText: string = ''; + + /** + * Custom text for the `Complete` button. + * If not provided, the default text will be used. + */ + @Input() + customCompleteButtonText: string = ''; + + /** + * Custom text for the `Save` button. + * If not provided, the default text will be used. + */ + @Input() + customSaveButtonText: string = ''; + /** Toggle readonly state of the task. */ @Input() readOnly = false; @@ -86,6 +111,10 @@ export class TaskFormCloudComponent { @Input() taskDetails: TaskDetailsCloudModel; + /** Emitted when the form is loaded or reloaded. */ + @Output() + formLoaded = new EventEmitter(); + /** Emitted when the form is saved. */ @Output() formSaved = new EventEmitter(); @@ -224,4 +253,8 @@ export class TaskFormCloudComponent { onDisplayModeOff(displayModeConfiguration: FormCloudDisplayModeConfiguration) { this.displayModeOff.emit(displayModeConfiguration); } + + onFormLoaded(form: FormModel) { + this.formLoaded.emit(form); + } } diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html index 7b06233dee..8508bd26f0 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html @@ -1,10 +1,5 @@ - diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts index caf5a40846..4a6a274b17 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts @@ -71,6 +71,29 @@ describe('UserTaskCloudButtonsComponent', () => { expect(cancelClickSpy).toHaveBeenCalled(); }); + it('should display custom text when customCancelButtonText is provided', async () => { + const customText = 'Custom Cancel Text'; + fixture.componentRef.setInput('showCancelButton', true); + fixture.componentRef.setInput('customCancelButtonText', customText); + fixture.detectChanges(); + + const cancelButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + const buttonText = await cancelButton.getText(); + + expect(buttonText.trim()).toBe(customText); + }); + + it('should display default text when customCancelButtonText is not provided', async () => { + fixture.componentRef.setInput('showCancelButton', true); + fixture.componentRef.setInput('customCancelButtonText', ''); + fixture.detectChanges(); + + const cancelButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + const buttonText = await cancelButton.getText(); + + expect(buttonText.trim()).toBe('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL'); + }); + it('should show claim button', async () => { let claimButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '.adf-user-task-cloud-claim-btn' })); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts index 19611f40a7..8612a21b87 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts @@ -49,6 +49,13 @@ export class UserTaskCloudButtonsComponent { @Input() showCancelButton = true; + /** + * Custom text for the `Cancel` button. + * If not provided, the default text will be used. + */ + @Input() + customCancelButtonText: string = ''; + /** Emitted when any error occurs. */ @Output() error = new EventEmitter(); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html index 8521431e22..8804d4d8c1 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html @@ -1,4 +1,4 @@ -
+
@@ -12,9 +12,16 @@ [showTitle]="showTitle" [taskId]="taskId" [taskDetails]="taskDetails" + [showCancelButton]="showCancelButton" + [showSaveButton]="showSaveButton && canCompleteTask()" + [showCompleteButton]="showCompleteButton && canCompleteTask()" + [customCancelButtonText]="customCancelButtonText" + [customSaveButtonText]="customSaveButtonText" + [customCompleteButtonText]="customCompleteButtonText" (cancelClick)="onCancelForm()" (executeOutcome)="onExecuteOutcome($event)" (error)="onError($event)" + (formLoaded)="onFormLoaded($event)" (formSaved)="onFormSaved()" (formContentClicked)="onFormContentClicked($event)" (taskCompleted)="onCompleteTaskForm()" @@ -37,7 +44,6 @@ [taskId]="taskId" [showNextTaskCheckbox]="showNextTaskCheckbox && canCompleteTask()" [isNextTaskCheckboxChecked]="isNextTaskCheckboxChecked" - (cancelTask)="onCancelClick()" (claimTask)="onClaimTask()" (error)="onError($event)" @@ -79,7 +85,7 @@ color="primary" id="adf-form-complete" > - {{ 'ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE' | translate }} + {{ customCompleteButtonText || ('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE' | translate) }} @@ -98,6 +104,7 @@ [canClaimTask]="canClaimTask()" [canUnclaimTask]="canUnclaimTask()" [showCancelButton]="showCancelButton" + [customCancelButtonText]="customCancelButtonText" [taskId]="taskId" (cancelClick)="onCancelClick()" (claimTask)="onClaimTask()" diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts index 60a2e0e5ff..864e99364c 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts @@ -39,6 +39,8 @@ import { } from '../../../models/task-details-cloud.model'; import { TaskFormCloudComponent } from '../task-form-cloud/task-form-cloud.component'; import { TaskCloudService } from '../../../services/task-cloud.service'; +import { FormModel } from '../../../../../../../core'; +import { UserTaskCloudButtonsComponent } from '../user-task-cloud-buttons/user-task-cloud-buttons.component'; const taskDetails: TaskDetailsCloudModel = { appName: 'simple-app', @@ -591,4 +593,168 @@ describe('UserTaskCloudComponent', () => { prepareTestCase({ showNextTaskCheckbox: true, showCompleteButton: true, readOnly: false, canCompleteTask: true }); expect(isCheckboxShown()).toBeTrue(); }); + + describe('Input Properties - Button Controls', () => { + beforeEach(() => { + taskDetails.formKey = 'my-form'; + component.taskDetails = { ...taskDetails }; + component.getTaskType(); + component.taskId = 'taskId'; + component.appName = 'app'; + }); + + describe('showSaveButton', () => { + it('should pass showSaveButton to task form when task type is Form', () => { + fixture.componentRef.setInput('showSaveButton', false); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.showSaveButton).toBe(false); + }); + + it('should pass showSaveButton as true by default to task form', () => { + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.showSaveButton).toBe(true); + }); + + it('should only show save button when canCompleteTask returns true', () => { + spyOn(component, 'canCompleteTask').and.returnValue(false); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.showSaveButton).toBe(false); + }); + }); + + describe('showCompleteButton', () => { + it('should pass showCompleteButton to task form when task type is Form', () => { + fixture.componentRef.setInput('showCompleteButton', false); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.showCompleteButton).toBe(false); + }); + + it('should only show complete button when canCompleteTask returns true', () => { + spyOn(component, 'canCompleteTask').and.returnValue(false); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.showCompleteButton).toBe(false); + }); + + it('should display custom complete button text in no form template', async () => { + component.taskDetails.formKey = ''; + component.getTaskType(); + + const customText = 'Custom Complete Text'; + fixture.componentRef.setInput('customCompleteButtonText', customText); + fixture.detectChanges(); + + const completeButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-form-complete' })); + expect(completeButton).not.toBeNull(); + + const buttonText = await completeButton.getText(); + expect(buttonText).toBe(customText); + }); + + it('should display default complete button text when customCompleteButtonText is not provided', async () => { + component.taskDetails.formKey = ''; + component.getTaskType(); + const spy = spyOn(taskCloudService, 'canCompleteTask'); + spy.and.returnValue(true); + fixture.detectChanges(); + + const completeButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-form-complete' })); + expect(completeButton).not.toBeNull(); + + const buttonText = await completeButton.getText(); + expect(buttonText).toBe('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE'); + }); + }); + + describe('Custom button text', () => { + it('should pass customCancelButtonText to task form when task type is Form', () => { + const customText = 'Custom Cancel Text'; + fixture.componentRef.setInput('customCancelButtonText', customText); + component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.customCancelButtonText).toBe(customText); + }); + + it('should pass customCancelButtonText to adf-cloud-user-task-cloud-buttons', () => { + component.taskDetails.formKey = ''; + component.getTaskType(); + + const customText = 'Custom Cancel Text'; + fixture.componentRef.setInput('customCancelButtonText', customText); + fixture.detectChanges(); + + const buttonsElement = fixture.debugElement.query(By.css('adf-cloud-user-task-cloud-buttons')); + const buttonsComponent = buttonsElement?.componentInstance as UserTaskCloudButtonsComponent; + + expect(buttonsComponent.customCancelButtonText).toBe(customText); + }); + + it('should pass customSaveButtonText to task form when task type is Form', () => { + const customText = 'Custom Save Text'; + fixture.componentRef.setInput('customSaveButtonText', customText); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.customSaveButtonText).toBe(customText); + }); + + it('should pass customCompleteButtonText to task form when task type is Form', () => { + const customText = 'Custom Complete Text'; + fixture.componentRef.setInput('customCompleteButtonText', customText); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + expect(taskFormComponent.customCompleteButtonText).toBe(customText); + }); + }); + }); + + it('should emit formLoaded when task form emits formLoaded event', () => { + const mockForm = new FormModel(); + + taskDetails.formKey = 'my-form'; + component.taskDetails = { ...taskDetails }; + component.getTaskType(); + component.taskId = 'taskId'; + component.appName = 'app'; + + spyOn(component.formLoaded, 'emit'); + + component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); + fixture.detectChanges(); + + const taskFormElement = fixture.debugElement.query(By.css('adf-cloud-task-form')); + const taskFormComponent = taskFormElement?.componentInstance as TaskFormCloudComponent; + + taskFormComponent.formLoaded.emit(mockForm); + + expect(component.formLoaded.emit).toHaveBeenCalledWith(mockForm); + }); }); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.ts index 86f9784ed4..32167fec94 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.ts @@ -86,6 +86,31 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { @Input() showCompleteButton = true; + /** Toggle rendering of the `Save` button. */ + @Input() + showSaveButton = true; + + /** + * Custom text for the `Cancel` button. + * If not provided, the default text will be used. + */ + @Input() + customCancelButtonText: string = ''; + + /** + * Custom text for the `Complete` button. + * If not provided, the default text will be used. + */ + @Input() + customCompleteButtonText: string = ''; + + /** + * Custom text for the `Save` button. + * If not provided, the default text will be used. + */ + @Input() + customSaveButtonText: string = ''; + /** Toggle rendering of the `Open next task` checkbox (for screens only). */ @Input() showNextTaskCheckbox = false; @@ -129,6 +154,10 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { @Output() formContentClicked: EventEmitter = new EventEmitter(); + /** Emitted when the form is loaded or reloaded. */ + @Output() + formLoaded = new EventEmitter(); + /** Emitted when the form is saved. */ @Output() formSaved = new EventEmitter(); @@ -248,6 +277,10 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { this.error.emit(data); } + onFormLoaded(form: FormModel) { + this.formLoaded.emit(form); + } + onExecuteOutcome(outcome: FormOutcomeEvent): void { this.executeOutcome.emit(outcome); } diff --git a/lib/process-services/src/lib/form/form.component.ts b/lib/process-services/src/lib/form/form.component.ts index f2a93307c9..eef4491bd1 100644 --- a/lib/process-services/src/lib/form/form.component.ts +++ b/lib/process-services/src/lib/form/form.component.ts @@ -323,7 +323,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnChange * @returns list of form outcomes */ getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] { - return [new FormOutcomeModel(form, { id: '$save', name: FormOutcomeModel.SAVE_ACTION, isSystem: true })]; + return [new FormOutcomeModel(form, { id: FormModel.SAVE_OUTCOME, name: FormOutcomeModel.SAVE_ACTION, isSystem: true })]; } checkVisibility(field: FormFieldModel) {