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 3ad6c0bbf3..40607575e9 100644 --- a/lib/core/src/lib/form/components/form-base.component.ts +++ b/lib/core/src/lib/form/components/form-base.component.ts @@ -223,8 +223,8 @@ export abstract class FormBaseComponent { } } else { // Note: Activiti is using NAME field rather than ID for outcomes - if (outcome.name) { - this.completeTaskForm(outcome.name); + if (outcome.name && outcome.id) { + this.completeTaskForm(outcome.name, outcome.id); return true; } } @@ -243,7 +243,7 @@ export abstract class FormBaseComponent { abstract saveTaskForm(): void; - abstract completeTaskForm(outcome?: string): void; + abstract completeTaskForm(outcome?: string, outcomeId?: string): void; protected abstract onTaskSaved(form: FormModel): void; 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 5c07471c5c..2654453061 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 @@ -73,7 +73,6 @@ export class FormModel implements ProcessFormModel { readonly confirmMessage: ConfirmMessage; readonly taskName = FormModel.UNSET_TASK_NAME; readonly processDefinitionId: string; - readonly selectedOutcome: string; readonly enableFixedSpace: boolean; readonly displayMode: any; @@ -88,6 +87,8 @@ export class FormModel implements ProcessFormModel { fieldValidators: FormFieldValidator[] = []; customFieldTemplates: FormFieldTemplates = {}; theme?: ThemeModel; + selectedOutcomeId?: string; + selectedOutcome: string; className: string; readOnly = false; 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 d850f6c17b..c426423ec0 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 @@ -464,7 +464,8 @@ describe('FormCloudComponent', () => { it('should complete form on custom outcome click', () => { const formModel = new FormModel(); const outcomeName = 'Custom Action'; - const outcome = new FormOutcomeModel(formModel, { id: 'custom1', name: outcomeName }); + const outcomeId = 'custom1'; + const outcome = new FormOutcomeModel(formModel, { id: outcomeId, name: outcomeName }); let saved = false; formComponent.form = formModel; @@ -474,7 +475,7 @@ describe('FormCloudComponent', () => { const result = formComponent.onOutcomeClicked(outcome); expect(result).toBeTruthy(); expect(saved).toBeFalse(); - expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName); + expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName, outcomeId); }); it('should save form on [save] outcome click', () => { @@ -800,8 +801,13 @@ describe('FormCloudComponent', () => { ); const outcome = 'complete'; + const outcomeId = 'custom-outcome-id'; let completed = false; - formComponent.formCompleted.subscribe(() => (completed = true)); + let completedForm = null; + formComponent.formCompleted.subscribe((form) => { + completed = true; + completedForm = form; + }); const taskId = '123-223'; const appVersion = 1; @@ -819,7 +825,7 @@ describe('FormCloudComponent', () => { formComponent.taskId = taskId; formComponent.appName = appName; formComponent.processInstanceId = processInstanceId; - formComponent.completeTaskForm(outcome); + formComponent.completeTaskForm(outcome, outcomeId); expect(formCloudService.completeTaskForm).toHaveBeenCalledWith( appName, @@ -831,6 +837,9 @@ describe('FormCloudComponent', () => { appVersion ); expect(completed).toBeTruthy(); + expect(completedForm.selectedOutcome).toBe(outcome); + expect(completedForm.selectedOutcomeId).toBe(outcomeId); + expect(completedForm).toBe(formComponent.form); }); it('should open confirmation dialog on complete task', async () => { @@ -865,9 +874,11 @@ describe('FormCloudComponent', () => { formComponent.appName = 'appName'; spyOn(formComponent['formCloudService'], 'completeTaskForm').and.returnValue(of(formModel as any)); - formComponent.completeTaskForm('complete'); + const outcomeId = 'test-outcome-id'; + formComponent.completeTaskForm('complete', outcomeId); expect(formComponent['formCloudService'].completeTaskForm).toHaveBeenCalled(); + expect(formComponent.form.selectedOutcomeId).toBe(outcomeId); }); it('should not confirm form if user rejects', () => { @@ -940,7 +951,7 @@ describe('FormCloudComponent', () => { const result = formComponent.onOutcomeClicked(outcome); expect(result).toBeTruthy(); - expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcome.name); + expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcome.name, outcome.id); }); it('should check visibility only if field with form provided', () => { @@ -1760,4 +1771,76 @@ describe('retrieve metadata on submit', () => { expect(formComponent.disableSaveButton).toBeFalse(); }); + + it('should handle outcomeId correctly when completing form with confirmation dialog', () => { + let matDialog = TestBed.inject(MatDialog); + spyOn(matDialog, 'open').and.returnValue({ afterClosed: () => of(true) } as any); + spyOn(formComponent['formCloudService'], 'completeTaskForm').and.returnValue(of({} as any)); + + const formModel = new FormModel({ + confirmMessage: { + show: true, + message: 'Are you sure you want to submit the form?' + } + }); + formComponent.form = formModel; + formComponent.taskId = 'task-123'; + formComponent.appName = 'test-app'; + + const outcome = 'approve'; + const outcomeId = 'approve-outcome-id'; + + formComponent.completeTaskForm(outcome, outcomeId); + + expect(matDialog.open).toHaveBeenCalled(); + expect(formComponent.form.selectedOutcome).toBe(outcome); + expect(formComponent.form.selectedOutcomeId).toBe(outcomeId); + }); + + it('should pass outcomeId when completing form without confirmation dialog', () => { + spyOn(formComponent['formCloudService'], 'completeTaskForm').and.returnValue(of({} as any)); + + const formModel = new FormModel(); + formComponent.form = formModel; + formComponent.taskId = 'task-123'; + formComponent.appName = 'test-app'; + + const outcome = 'reject'; + const outcomeId = 'reject-outcome-id'; + + formComponent.completeTaskForm(outcome, outcomeId); + + expect(formComponent.form.selectedOutcome).toBe(outcome); + expect(formComponent.form.selectedOutcomeId).toBe(outcomeId); + expect(formComponent['formCloudService'].completeTaskForm).toHaveBeenCalled(); + }); + + it('should set form values before calling onTaskCompleted', () => { + const formModel = new FormModel({ + id: '23', + taskId: '123-223', + fields: [{ id: 'field1' }, { id: 'field2' }] + }); + + formComponent.form = formModel; + formComponent.taskId = '123-223'; + formComponent.appName = 'test-app'; + + const outcome = 'approve'; + const outcomeId = 'custom-approve-id'; + let emittedForm = null; + + spyOn(formComponent['formCloudService'], 'completeTaskForm').and.returnValue(of({} as any)); + + formComponent.formCompleted.subscribe((form) => { + emittedForm = form; + }); + + formComponent.completeTaskForm(outcome, outcomeId); + + expect(emittedForm).not.toBeNull(); + expect(emittedForm.selectedOutcome).toBe(outcome); + expect(emittedForm.selectedOutcomeId).toBe(outcomeId); + expect(formComponent['formCloudService'].completeTaskForm).toHaveBeenCalled(); + }); }); 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 d9f407a914..f121e76041 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 @@ -395,7 +395,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, } } - completeTaskForm(outcome?: string) { + completeTaskForm(outcome?: string, outcomeId?: string) { if (this.form?.confirmMessage?.show === true) { const dialogRef = this.dialog.open(ConfirmDialogComponent, { data: { @@ -406,22 +406,24 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, dialogRef.afterClosed().subscribe((result) => { if (result === true) { - this.completeForm(outcome); + this.completeForm(outcome, outcomeId); } }); } else { - this.completeForm(outcome); + this.completeForm(outcome, outcomeId); } this.displayModeService.onCompleteTask(this.id, this.displayMode, this.displayModeConfigurations); } - private completeForm(outcome?: string) { + private completeForm(outcome?: string, outcomeId?: string) { if (this.form && this.appName && this.taskId) { this.formCloudService .completeTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values, outcome, this.appVersion) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { + this.form.selectedOutcome = outcome; + this.form.selectedOutcomeId = outcomeId; this.onTaskCompleted(this.form); }, error: (error) => this.onTaskCompletedError(error) diff --git a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.html b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.html index 3d789e623f..d7f32700f8 100755 --- a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.html +++ b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.html @@ -103,7 +103,7 @@ [showTitle]="false" (formContentClicked)="onFormContentClicked($event)" (formLoaded)="onFormLoaded($event)" - (executeOutcome)="onCustomOutcomeClicked($event.outcome.name)" + (executeOutcome)="onCustomOutcomeClicked($event)" > diff --git a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts index 9f37ffc48d..5ca7fb185d 100755 --- a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts @@ -17,7 +17,7 @@ import { SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { FormModel } from '@alfresco/adf-core'; +import { FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core'; import { of, throwError } from 'rxjs'; import { StartProcessCloudService } from '../services/start-process-cloud.service'; import { FormCloudService } from '../../../form/services/form-cloud.service'; @@ -820,9 +820,12 @@ describe('StartProcessCloudComponent', () => { outcome: 'custom_outcome' }); + const formOutcomeModel = new FormOutcomeModel(null, fakeFormModelJson.outcomes[0]); + const event = new FormOutcomeEvent(formOutcomeModel); + fixture.detectChanges(); - component.onCustomOutcomeClicked('custom_outcome'); + component.onCustomOutcomeClicked(event); expect(startProcessWithFormSpy).toHaveBeenCalledWith( component.appName, @@ -869,7 +872,7 @@ describe('StartProcessCloudComponent', () => { ); }); - it('should output start event when process started successfully', () => { + it('should emit start event when process started successfully', () => { const emitSpy = spyOn(component.success, 'emit'); component.startProcess(); expect(emitSpy).toHaveBeenCalledWith(fakeProcessInstance); @@ -1214,4 +1217,40 @@ describe('StartProcessCloudComponent', () => { component.cancelStartProcess(); }); }); + + it('should emit customOutcomeSelected and success events when onCustomOutcomeClicked is called', async () => { + const customOutcomeSelectedSpy = spyOn(component.customOutcomeSelected, 'emit'); + const successSpy = spyOn(component.success, 'emit'); + + getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + formDefinitionSpy.and.returnValue(of(fakeFormModelJson)); + startProcessWithFormSpy.and.returnValue(of(fakeProcessInstance)); + + component.ngOnChanges({ appName: firstChange }); + component.processForm.controls['processInstanceName'].setValue('My Process 1'); + component.appName = 'test app name'; + component.formCloud = new FormModel(JSON.stringify(fakeFormModelJson)); + component.formCloud.values = { dropdown: { id: '1', name: 'label 2' } }; + component.processDefinitionCurrent = fakeProcessDefinitions[2]; + component.processPayloadCloud.processDefinitionKey = fakeProcessDefinitions[2].key; + + const customOutcome = { + id: 'custom_outcome_id', + name: 'custom_outcome' + }; + const formOutcomeModel = new FormOutcomeModel(null, customOutcome); + const event = new FormOutcomeEvent(formOutcomeModel); + + fixture.detectChanges(); + + component.onCustomOutcomeClicked(event); + + await fixture.whenStable(); + + expect(customOutcomeSelectedSpy).toHaveBeenCalledWith(customOutcome.id); + expect(successSpy).toHaveBeenCalledWith(fakeProcessInstance); + expect(startProcessWithFormSpy).toHaveBeenCalledTimes(1); + expect(component.customOutcomeName).toBe(customOutcome.name); + expect(component.customOutcomeId).toBe(customOutcome.id); + }); }); diff --git a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts index 504c7ca6b6..d06b9ae1f5 100755 --- a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts @@ -33,6 +33,7 @@ import { ConfirmDialogComponent, ContentLinkModel, FormModel, + FormOutcomeEvent, InplaceFormInputComponent, LocalizedDatePipe, TranslationService, @@ -158,6 +159,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { @Output() processDefinitionSelection: EventEmitter = new EventEmitter(); + @Output() + customOutcomeSelected: EventEmitter = new EventEmitter(); + processDefinitionList: ProcessDefinitionCloud[] = []; processDefinitionCurrent?: ProcessDefinitionCloud; errorMessageId: string = ''; @@ -165,7 +169,8 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { filteredProcesses: ProcessDefinitionCloud[] = []; staticMappings: TaskVariableCloud[] = []; resolvedValues?: TaskVariableCloud[]; - customOutcome: string; + customOutcomeName: string; + customOutcomeId: string; isProcessStarting = false; isFormCloudLoaded = false; @@ -421,8 +426,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { } } - onCustomOutcomeClicked(outcome: string) { - this.customOutcome = outcome; + onCustomOutcomeClicked(outcome: FormOutcomeEvent) { + this.customOutcomeName = outcome.outcome.name; + this.customOutcomeId = outcome.outcome.id; this.startProcess(); } @@ -439,7 +445,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { processDefinitionKey: this.processPayloadCloud.processDefinitionKey, variables: this.variables ?? {}, values: this.formCloud.values, - outcome: this.customOutcome + outcome: this.customOutcomeName }) ) : this.startProcessCloudService.startProcess( @@ -453,6 +459,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { action.subscribe({ next: (res) => { + this.customOutcomeSelected.emit(this.customOutcomeId); this.success.emit(res); this.isProcessStarting = false; }, diff --git a/lib/process-services-cloud/src/lib/process/start-process/mock/start-process.component.mock.ts b/lib/process-services-cloud/src/lib/process/start-process/mock/start-process.component.mock.ts index 2d27a08d56..cbce42f06d 100755 --- a/lib/process-services-cloud/src/lib/process/start-process/mock/start-process.component.mock.ts +++ b/lib/process-services-cloud/src/lib/process/start-process/mock/start-process.component.mock.ts @@ -323,7 +323,13 @@ export const fakeFormModelJson = { } } ], - outcomes: [], + outcomes: [ + { + id: 'custom_outcome_id', + name: 'custom_outcome', + visibilityCondition: null + } + ], metadata: {}, variables: [] }; 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 e51cf355f5..965976e097 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 @@ -348,6 +348,17 @@ describe('TaskFormCloudComponent', () => { expect(component.formLoaded.emit).toHaveBeenCalledOnceWith(mockForm); }); + it('should emit both formCompleted and taskCompleted events when form is completed', () => { + const mockForm = new FormModel(); + spyOn(component.formCompleted, 'emit').and.stub(); + spyOn(component.taskCompleted, 'emit').and.stub(); + + component.onFormCompleted(mockForm); + + expect(component.formCompleted.emit).toHaveBeenCalledOnceWith(mockForm); + expect(component.taskCompleted.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(); 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 411099be13..33bb10d48c 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 @@ -125,7 +125,7 @@ export class TaskFormCloudComponent { /** Emitted when the task is completed. */ @Output() - taskCompleted = new EventEmitter(); + taskCompleted = new EventEmitter(); /** Emitted when the task is claimed. */ @Output() @@ -167,7 +167,10 @@ export class TaskFormCloudComponent { loading: boolean = false; - constructor(private taskCloudService: TaskCloudService, private formRenderingService: FormRenderingService) { + constructor( + private taskCloudService: TaskCloudService, + private formRenderingService: FormRenderingService + ) { this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileCloudWidgetComponent, true); this.formRenderingService.setComponentTypeResolver('dropdown', () => DropdownCloudWidgetComponent, true); this.formRenderingService.setComponentTypeResolver('date', () => DateCloudWidgetComponent, true); @@ -205,10 +208,6 @@ export class TaskFormCloudComponent { return this.readOnly || !this.taskCloudService.canCompleteTask(this.taskDetails); } - onCompleteTask() { - this.taskCompleted.emit(this.taskId); - } - onClaimTask() { this.taskClaimed.emit(this.taskId); } @@ -227,7 +226,7 @@ export class TaskFormCloudComponent { onFormCompleted(form: FormModel) { this.formCompleted.emit(form); - this.taskCompleted.emit(this.taskId); + this.taskCompleted.emit(form); } onError(data: any) { diff --git a/lib/process-services/src/lib/form/form.component.spec.ts b/lib/process-services/src/lib/form/form.component.spec.ts index 0fdfba3e30..8fdfbb90bf 100644 --- a/lib/process-services/src/lib/form/form.component.spec.ts +++ b/lib/process-services/src/lib/form/form.component.spec.ts @@ -347,7 +347,7 @@ describe('FormComponent', () => { const result = formComponent.onOutcomeClicked(outcome); expect(result).toBeTruthy(); expect(saved).toBeFalse(); - expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName); + expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName, outcome.id); }); it('should save form on [save] outcome click', () => { @@ -707,7 +707,7 @@ describe('FormComponent', () => { const result = formComponent.onOutcomeClicked(outcome); expect(result).toBeTruthy(); - expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcome.name); + expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcome.name, outcome.id); }); it('should check visibility only if field with form provided', () => {