AAE-36335 Handle custom redirects

This commit is contained in:
Enrico Hilgendorf
2025-09-02 07:30:30 +02:00
parent 09c35ea903
commit 3fecf66898
11 changed files with 180 additions and 32 deletions

View File

@@ -223,8 +223,8 @@ export abstract class FormBaseComponent {
} }
} else { } else {
// Note: Activiti is using NAME field rather than ID for outcomes // Note: Activiti is using NAME field rather than ID for outcomes
if (outcome.name) { if (outcome.name && outcome.id) {
this.completeTaskForm(outcome.name); this.completeTaskForm(outcome.name, outcome.id);
return true; return true;
} }
} }
@@ -243,7 +243,7 @@ export abstract class FormBaseComponent {
abstract saveTaskForm(): void; abstract saveTaskForm(): void;
abstract completeTaskForm(outcome?: string): void; abstract completeTaskForm(outcome?: string, outcomeId?: string): void;
protected abstract onTaskSaved(form: FormModel): void; protected abstract onTaskSaved(form: FormModel): void;

View File

@@ -73,7 +73,6 @@ export class FormModel implements ProcessFormModel {
readonly confirmMessage: ConfirmMessage; readonly confirmMessage: ConfirmMessage;
readonly taskName = FormModel.UNSET_TASK_NAME; readonly taskName = FormModel.UNSET_TASK_NAME;
readonly processDefinitionId: string; readonly processDefinitionId: string;
readonly selectedOutcome: string;
readonly enableFixedSpace: boolean; readonly enableFixedSpace: boolean;
readonly displayMode: any; readonly displayMode: any;
@@ -88,6 +87,8 @@ export class FormModel implements ProcessFormModel {
fieldValidators: FormFieldValidator[] = []; fieldValidators: FormFieldValidator[] = [];
customFieldTemplates: FormFieldTemplates = {}; customFieldTemplates: FormFieldTemplates = {};
theme?: ThemeModel; theme?: ThemeModel;
selectedOutcomeId?: string;
selectedOutcome: string;
className: string; className: string;
readOnly = false; readOnly = false;

View File

@@ -464,7 +464,8 @@ describe('FormCloudComponent', () => {
it('should complete form on custom outcome click', () => { it('should complete form on custom outcome click', () => {
const formModel = new FormModel(); const formModel = new FormModel();
const outcomeName = 'Custom Action'; 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; let saved = false;
formComponent.form = formModel; formComponent.form = formModel;
@@ -474,7 +475,7 @@ describe('FormCloudComponent', () => {
const result = formComponent.onOutcomeClicked(outcome); const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(saved).toBeFalse(); expect(saved).toBeFalse();
expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName); expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName, outcomeId);
}); });
it('should save form on [save] outcome click', () => { it('should save form on [save] outcome click', () => {
@@ -800,8 +801,13 @@ describe('FormCloudComponent', () => {
); );
const outcome = 'complete'; const outcome = 'complete';
const outcomeId = 'custom-outcome-id';
let completed = false; let completed = false;
formComponent.formCompleted.subscribe(() => (completed = true)); let completedForm = null;
formComponent.formCompleted.subscribe((form) => {
completed = true;
completedForm = form;
});
const taskId = '123-223'; const taskId = '123-223';
const appVersion = 1; const appVersion = 1;
@@ -819,7 +825,7 @@ describe('FormCloudComponent', () => {
formComponent.taskId = taskId; formComponent.taskId = taskId;
formComponent.appName = appName; formComponent.appName = appName;
formComponent.processInstanceId = processInstanceId; formComponent.processInstanceId = processInstanceId;
formComponent.completeTaskForm(outcome); formComponent.completeTaskForm(outcome, outcomeId);
expect(formCloudService.completeTaskForm).toHaveBeenCalledWith( expect(formCloudService.completeTaskForm).toHaveBeenCalledWith(
appName, appName,
@@ -831,6 +837,9 @@ describe('FormCloudComponent', () => {
appVersion appVersion
); );
expect(completed).toBeTruthy(); 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 () => { it('should open confirmation dialog on complete task', async () => {
@@ -865,9 +874,11 @@ describe('FormCloudComponent', () => {
formComponent.appName = 'appName'; formComponent.appName = 'appName';
spyOn(formComponent['formCloudService'], 'completeTaskForm').and.returnValue(of(formModel as any)); 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['formCloudService'].completeTaskForm).toHaveBeenCalled();
expect(formComponent.form.selectedOutcomeId).toBe(outcomeId);
}); });
it('should not confirm form if user rejects', () => { it('should not confirm form if user rejects', () => {
@@ -940,7 +951,7 @@ describe('FormCloudComponent', () => {
const result = formComponent.onOutcomeClicked(outcome); const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy(); 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', () => { it('should check visibility only if field with form provided', () => {
@@ -1760,4 +1771,76 @@ describe('retrieve metadata on submit', () => {
expect(formComponent.disableSaveButton).toBeFalse(); 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();
});
}); });

View File

@@ -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) { if (this.form?.confirmMessage?.show === true) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, { const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { data: {
@@ -406,22 +406,24 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
dialogRef.afterClosed().subscribe((result) => { dialogRef.afterClosed().subscribe((result) => {
if (result === true) { if (result === true) {
this.completeForm(outcome); this.completeForm(outcome, outcomeId);
} }
}); });
} else { } else {
this.completeForm(outcome); this.completeForm(outcome, outcomeId);
} }
this.displayModeService.onCompleteTask(this.id, this.displayMode, this.displayModeConfigurations); 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) { if (this.form && this.appName && this.taskId) {
this.formCloudService this.formCloudService
.completeTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values, outcome, this.appVersion) .completeTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values, outcome, this.appVersion)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.form.selectedOutcome = outcome;
this.form.selectedOutcomeId = outcomeId;
this.onTaskCompleted(this.form); this.onTaskCompleted(this.form);
}, },
error: (error) => this.onTaskCompletedError(error) error: (error) => this.onTaskCompletedError(error)

View File

@@ -103,7 +103,7 @@
[showTitle]="false" [showTitle]="false"
(formContentClicked)="onFormContentClicked($event)" (formContentClicked)="onFormContentClicked($event)"
(formLoaded)="onFormLoaded($event)" (formLoaded)="onFormLoaded($event)"
(executeOutcome)="onCustomOutcomeClicked($event.outcome.name)" (executeOutcome)="onCustomOutcomeClicked($event)"
> >
<adf-cloud-form-custom-outcomes> <adf-cloud-form-custom-outcomes>
<ng-template [ngTemplateOutlet]="taskFormCloudButtons" /> <ng-template [ngTemplateOutlet]="taskFormCloudButtons" />

View File

@@ -17,7 +17,7 @@
import { SimpleChange } from '@angular/core'; import { SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; 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 { of, throwError } from 'rxjs';
import { StartProcessCloudService } from '../services/start-process-cloud.service'; import { StartProcessCloudService } from '../services/start-process-cloud.service';
import { FormCloudService } from '../../../form/services/form-cloud.service'; import { FormCloudService } from '../../../form/services/form-cloud.service';
@@ -820,9 +820,12 @@ describe('StartProcessCloudComponent', () => {
outcome: 'custom_outcome' outcome: 'custom_outcome'
}); });
const formOutcomeModel = new FormOutcomeModel(null, fakeFormModelJson.outcomes[0]);
const event = new FormOutcomeEvent(formOutcomeModel);
fixture.detectChanges(); fixture.detectChanges();
component.onCustomOutcomeClicked('custom_outcome'); component.onCustomOutcomeClicked(event);
expect(startProcessWithFormSpy).toHaveBeenCalledWith( expect(startProcessWithFormSpy).toHaveBeenCalledWith(
component.appName, 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'); const emitSpy = spyOn(component.success, 'emit');
component.startProcess(); component.startProcess();
expect(emitSpy).toHaveBeenCalledWith(fakeProcessInstance); expect(emitSpy).toHaveBeenCalledWith(fakeProcessInstance);
@@ -1214,4 +1217,40 @@ describe('StartProcessCloudComponent', () => {
component.cancelStartProcess(); 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);
});
}); });

View File

@@ -33,6 +33,7 @@ import {
ConfirmDialogComponent, ConfirmDialogComponent,
ContentLinkModel, ContentLinkModel,
FormModel, FormModel,
FormOutcomeEvent,
InplaceFormInputComponent, InplaceFormInputComponent,
LocalizedDatePipe, LocalizedDatePipe,
TranslationService, TranslationService,
@@ -158,6 +159,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
@Output() @Output()
processDefinitionSelection: EventEmitter<ProcessDefinitionCloud> = new EventEmitter<ProcessDefinitionCloud>(); processDefinitionSelection: EventEmitter<ProcessDefinitionCloud> = new EventEmitter<ProcessDefinitionCloud>();
@Output()
customOutcomeSelected: EventEmitter<string> = new EventEmitter<string>();
processDefinitionList: ProcessDefinitionCloud[] = []; processDefinitionList: ProcessDefinitionCloud[] = [];
processDefinitionCurrent?: ProcessDefinitionCloud; processDefinitionCurrent?: ProcessDefinitionCloud;
errorMessageId: string = ''; errorMessageId: string = '';
@@ -165,7 +169,8 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
filteredProcesses: ProcessDefinitionCloud[] = []; filteredProcesses: ProcessDefinitionCloud[] = [];
staticMappings: TaskVariableCloud[] = []; staticMappings: TaskVariableCloud[] = [];
resolvedValues?: TaskVariableCloud[]; resolvedValues?: TaskVariableCloud[];
customOutcome: string; customOutcomeName: string;
customOutcomeId: string;
isProcessStarting = false; isProcessStarting = false;
isFormCloudLoaded = false; isFormCloudLoaded = false;
@@ -421,8 +426,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
} }
} }
onCustomOutcomeClicked(outcome: string) { onCustomOutcomeClicked(outcome: FormOutcomeEvent) {
this.customOutcome = outcome; this.customOutcomeName = outcome.outcome.name;
this.customOutcomeId = outcome.outcome.id;
this.startProcess(); this.startProcess();
} }
@@ -439,7 +445,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
processDefinitionKey: this.processPayloadCloud.processDefinitionKey, processDefinitionKey: this.processPayloadCloud.processDefinitionKey,
variables: this.variables ?? {}, variables: this.variables ?? {},
values: this.formCloud.values, values: this.formCloud.values,
outcome: this.customOutcome outcome: this.customOutcomeName
}) })
) )
: this.startProcessCloudService.startProcess( : this.startProcessCloudService.startProcess(
@@ -453,6 +459,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
action.subscribe({ action.subscribe({
next: (res) => { next: (res) => {
this.customOutcomeSelected.emit(this.customOutcomeId);
this.success.emit(res); this.success.emit(res);
this.isProcessStarting = false; this.isProcessStarting = false;
}, },

View File

@@ -323,7 +323,13 @@ export const fakeFormModelJson = {
} }
} }
], ],
outcomes: [], outcomes: [
{
id: 'custom_outcome_id',
name: 'custom_outcome',
visibilityCondition: null
}
],
metadata: {}, metadata: {},
variables: [] variables: []
}; };

View File

@@ -348,6 +348,17 @@ describe('TaskFormCloudComponent', () => {
expect(component.formLoaded.emit).toHaveBeenCalledOnceWith(mockForm); 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', () => { it('should handle formLoaded event from adf-cloud-form and re-emit it', () => {
const mockForm = new FormModel(); const mockForm = new FormModel();
spyOn(component.formLoaded, 'emit').and.stub(); spyOn(component.formLoaded, 'emit').and.stub();

View File

@@ -125,7 +125,7 @@ export class TaskFormCloudComponent {
/** Emitted when the task is completed. */ /** Emitted when the task is completed. */
@Output() @Output()
taskCompleted = new EventEmitter<string>(); taskCompleted = new EventEmitter<FormModel>();
/** Emitted when the task is claimed. */ /** Emitted when the task is claimed. */
@Output() @Output()
@@ -167,7 +167,10 @@ export class TaskFormCloudComponent {
loading: boolean = false; 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('upload', () => AttachFileCloudWidgetComponent, true);
this.formRenderingService.setComponentTypeResolver('dropdown', () => DropdownCloudWidgetComponent, true); this.formRenderingService.setComponentTypeResolver('dropdown', () => DropdownCloudWidgetComponent, true);
this.formRenderingService.setComponentTypeResolver('date', () => DateCloudWidgetComponent, true); this.formRenderingService.setComponentTypeResolver('date', () => DateCloudWidgetComponent, true);
@@ -205,10 +208,6 @@ export class TaskFormCloudComponent {
return this.readOnly || !this.taskCloudService.canCompleteTask(this.taskDetails); return this.readOnly || !this.taskCloudService.canCompleteTask(this.taskDetails);
} }
onCompleteTask() {
this.taskCompleted.emit(this.taskId);
}
onClaimTask() { onClaimTask() {
this.taskClaimed.emit(this.taskId); this.taskClaimed.emit(this.taskId);
} }
@@ -227,7 +226,7 @@ export class TaskFormCloudComponent {
onFormCompleted(form: FormModel) { onFormCompleted(form: FormModel) {
this.formCompleted.emit(form); this.formCompleted.emit(form);
this.taskCompleted.emit(this.taskId); this.taskCompleted.emit(form);
} }
onError(data: any) { onError(data: any) {

View File

@@ -347,7 +347,7 @@ describe('FormComponent', () => {
const result = formComponent.onOutcomeClicked(outcome); const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(saved).toBeFalse(); expect(saved).toBeFalse();
expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName); expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName, outcome.id);
}); });
it('should save form on [save] outcome click', () => { it('should save form on [save] outcome click', () => {
@@ -707,7 +707,7 @@ describe('FormComponent', () => {
const result = formComponent.onOutcomeClicked(outcome); const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy(); 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', () => { it('should check visibility only if field with form provided', () => {