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 {
// 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;

View File

@@ -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;

View File

@@ -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();
});
});

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) {
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)

View File

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

View File

@@ -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);
});
});

View File

@@ -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<ProcessDefinitionCloud> = new EventEmitter<ProcessDefinitionCloud>();
@Output()
customOutcomeSelected: EventEmitter<string> = new EventEmitter<string>();
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;
},

View File

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

View File

@@ -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();

View File

@@ -125,7 +125,7 @@ export class TaskFormCloudComponent {
/** Emitted when the task is completed. */
@Output()
taskCompleted = new EventEmitter<string>();
taskCompleted = new EventEmitter<FormModel>();
/** 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) {

View File

@@ -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', () => {