From 124fd66965050b32dd6d41c6b88c783698b4b5e1 Mon Sep 17 00:00:00 2001 From: Bartosz Sekula Date: Tue, 14 Oct 2025 13:39:42 +0200 Subject: [PATCH] AAE-38328 Support screens on start process event (#11261) * AAE-38328 Start event supports using IDP dedicated screens * Added task type resolver * update tests * clean * update * add units * cr * update * update * cr --- .../start-process-cloud.component.html | 10 ++ .../start-process-cloud.component.spec.ts | 167 +++++++++++++----- .../start-process-cloud.component.ts | 80 ++++++--- .../mock/start-process.component.mock.ts | 5 +- .../base-screen-cloud.component.ts | 50 ++++++ .../start-process-screen-cloud.component.html | 9 + .../start-process-screen-cloud.component.scss | 8 + ...art-process-screen-cloud.component.spec.ts | 70 ++++++++ .../start-process-screen-cloud.component.ts | 52 ++++++ .../start-process-screen.model.ts | 28 +++ .../screen-cloud.component.html | 0 .../screen-cloud.component.spec.ts | 2 +- .../screen-cloud.component.ts | 37 +--- .../screen-cloud.model.ts | 0 .../src/lib/screen/public-api.ts | 3 +- .../task-type-resolver.service.spec.ts | 63 +++++++ .../task-type-resolver.service.ts | 61 +++++++ .../user-task-cloud.component.spec.ts | 9 +- .../user-task-cloud.component.ts | 35 ++-- .../start-process-screen-mock.component.ts | 32 ++++ 20 files changed, 590 insertions(+), 131 deletions(-) create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.html create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.scss create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen.model.ts rename lib/process-services-cloud/src/lib/screen/components/screen-cloud/{ => user-task-screen}/screen-cloud.component.html (100%) rename lib/process-services-cloud/src/lib/screen/components/screen-cloud/{ => user-task-screen}/screen-cloud.component.spec.ts (98%) rename lib/process-services-cloud/src/lib/screen/components/screen-cloud/{ => user-task-screen}/screen-cloud.component.ts (85%) rename lib/process-services-cloud/src/lib/screen/components/screen-cloud/{ => user-task-screen}/screen-cloud.model.ts (100%) create mode 100644 lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.spec.ts create mode 100644 lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.ts create mode 100644 lib/process-services-cloud/src/lib/testing/start-process-screen-mock.component.ts 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 a5c0f0b007..9f8fa48947 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 @@ -109,6 +109,16 @@ + } @else if (hasScreen) { + + + + + } @else { } 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 b06342326e..d14682df64 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 @@ -15,7 +15,7 @@ * limitations under the License. */ -import { SimpleChange } from '@angular/core'; +import { DebugElement, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core'; import { of, throwError } from 'rxjs'; @@ -49,6 +49,11 @@ import { MatButtonHarness } from '@angular/material/button/testing'; import { FormCloudDisplayMode } from '../../../services/form-fields.interfaces'; import { MatDialogHarness } from '@angular/material/dialog/testing'; import { MatDialog } from '@angular/material/dialog'; +import { ReactiveFormsModule } from '@angular/forms'; +import { StartProcessScreenCloudComponent } from '../../../screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component'; +import { provideScreen } from '../../../screen/services/provide-screen'; +import { StartProcessScreenCloud } from '../../../screen/components/screen-cloud/start-process-event-screen/start-process-screen.model'; +import { MockedTaskScreenCloudComponent } from '../../../testing/start-process-screen-mock.component'; describe('StartProcessCloudComponent', () => { let loader: HarnessLoader; @@ -57,7 +62,7 @@ describe('StartProcessCloudComponent', () => { let fixture: ComponentFixture; let processService: StartProcessCloudService; let formCloudService: FormCloudService; - let getDefinitionsSpy: jasmine.Spy; + let getProcessDefinitionsSpy: jasmine.Spy; let startProcessSpy: jasmine.Spy; let startProcessWithFormSpy: jasmine.Spy; let formDefinitionSpy: jasmine.Spy; @@ -65,6 +70,7 @@ describe('StartProcessCloudComponent', () => { let getStartEventConstantSpy: jasmine.Spy; const firstChange = new SimpleChange(undefined, 'myApp', true); + const screenId = 'screen-1234-5678-121212-123456'; const selectOptionByName = async (name: string) => { const arrowButton = await loader.getHarness(MatButtonHarness.with({ selector: '#adf-select-process-dropdown' })); @@ -84,14 +90,15 @@ describe('StartProcessCloudComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [StartProcessCloudComponent] + imports: [StartProcessCloudComponent, ReactiveFormsModule, StartProcessScreenCloudComponent], + providers: [provideScreen(screenId, MockedTaskScreenCloudComponent)] }); processService = TestBed.inject(StartProcessCloudService); formCloudService = TestBed.inject(FormCloudService); fixture = TestBed.createComponent(StartProcessCloudComponent); component = fixture.componentInstance; - getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions'); + getProcessDefinitionsSpy = spyOn(processService, 'getProcessDefinitions'); formDefinitionSpy = spyOn(formCloudService, 'getForm'); spyOn(processService, 'updateProcess').and.returnValue(of()); startProcessSpy = spyOn(processService, 'startProcess').and.returnValue(of(fakeProcessInstance)); @@ -143,7 +150,7 @@ describe('StartProcessCloudComponent', () => { component.name = 'My new process'; component.processDefinitionName = 'processwithoutform2'; - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinitionWithoutForm(component.processDefinitionName))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinitionWithoutForm(component.processDefinitionName))); fixture.detectChanges(); const change = new SimpleChange(null, 'MyApp', true); @@ -162,7 +169,7 @@ describe('StartProcessCloudComponent', () => { beforeEach(() => { component.name = 'My formless new process'; component.appName = 'myApp'; - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); fixture.detectChanges(); const change = new SimpleChange(null, 'MyApp', true); component.ngOnChanges({ appName: change }); @@ -172,7 +179,7 @@ describe('StartProcessCloudComponent', () => { it('should be able to start a process with a valid process name and process definition', fakeAsync(() => { component.name = 'My new process'; component.processDefinitionName = 'processwithoutform2'; - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinitionWithoutForm(component.processDefinitionName))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinitionWithoutForm(component.processDefinitionName))); fixture.detectChanges(); const change = new SimpleChange(null, 'MyApp', true); @@ -187,7 +194,7 @@ describe('StartProcessCloudComponent', () => { })); it('should create a process instance if the selection is valid', async () => { - getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy = getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.name = 'My new process'; component.processDefinitionName = 'process'; await selectOptionByName('processwithoutform2'); @@ -276,11 +283,81 @@ describe('StartProcessCloudComponent', () => { }); }); + describe('screen on start process event', () => { + let screenDebugComponent: DebugElement; + let screenComponent: StartProcessScreenCloud; + + beforeEach(async () => { + fixture.detectChanges(); + + const processDefinitionWithScreen = fakeSingleProcessDefinition('processWithScreen', { formKey: screenId }); + const someOtherProcess = fakeSingleProcessDefinition('otherProcess'); + getProcessDefinitionsSpy.and.returnValue(of([...processDefinitionWithScreen, ...someOtherProcess])); + component.processDefinitionName = 'processWithScreen'; + + component.ngOnChanges({ appName: new SimpleChange(null, 'myAppName', true) }); + fixture.detectChanges(); + + screenDebugComponent = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)); + screenComponent = screenDebugComponent.componentInstance; + }); + + it('should show screen', () => { + expect(screenComponent).toBeDefined(); + + const contentElement = screenDebugComponent.nativeElement.querySelector('div'); + expect(contentElement.textContent).toBe('Mock Screen Component'); + }); + + it('should toggle default process buttons', () => { + screenComponent.defaultStartProcessButtonsConfigurationChange.emit({ show: false, disable: false }); + fixture.detectChanges(); + + let startButton = fixture.nativeElement.querySelector('#button-start'); + let cancelButton = fixture.nativeElement.querySelector('#cancel_process'); + expect(startButton).toBeNull(); + expect(cancelButton).toBeNull(); + + screenComponent.defaultStartProcessButtonsConfigurationChange.emit({ show: true, disable: false }); + fixture.detectChanges(); + + startButton = fixture.nativeElement.querySelector('#button-start'); + cancelButton = fixture.nativeElement.querySelector('#cancel_process'); + expect(startButton).not.toBeNull(); + expect(cancelButton).not.toBeNull(); + }); + + it('should toggle disable start process button', () => { + screenComponent.defaultStartProcessButtonsConfigurationChange.emit({ show: true, disable: true }); + fixture.detectChanges(); + const startButton = fixture.nativeElement.querySelector('#button-start'); + + expect(startButton.disabled).toBe(true); + + screenComponent.defaultStartProcessButtonsConfigurationChange.emit({ show: true, disable: false }); + fixture.detectChanges(); + + expect(startButton.disabled).toBe(false); + }); + + it('should set payload for screen', () => { + const screenPayload = 'My payload'; + screenComponent.defaultStartProcessButtonsConfigurationChange.emit({ show: true, disable: false }); + screenComponent.startProcessPayloadChanged.emit(screenPayload); + fixture.detectChanges(); + + const startButton = fixture.debugElement.query(By.css('#button-start')); + startButton.triggerEventHandler('click', null); + + expect(startProcessWithFormSpy).toHaveBeenCalledWith('myAppName', screenId, 0, jasmine.objectContaining({ values: screenPayload })); + }); + }); + describe('start a process with start form', () => { beforeEach(() => { component.name = 'My new process with form'; component.appName = 'startformwithoutupload'; - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); fixture.detectChanges(); const change = new SimpleChange(null, 'startformwithoutupload', true); component.ngOnChanges({ appName: change }); @@ -315,7 +392,7 @@ describe('StartProcessCloudComponent', () => { it('should be able to start a process with a valid form', async () => { formDefinitionSpy.and.returnValue(of(fakeStartForm)); - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); typeValueInto('#processDefinitionName', 'processwithform'); fixture.detectChanges(); @@ -353,7 +430,7 @@ describe('StartProcessCloudComponent', () => { (fakeStartFormClone.formRepresentation as any).displayMode = FormCloudDisplayMode.fullScreen; formDefinitionSpy.and.returnValue(of(fakeStartFormClone)); - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); typeValueInto('#processDefinitionName', 'processwithform'); fixture.detectChanges(); @@ -371,7 +448,7 @@ describe('StartProcessCloudComponent', () => { it('should NOT be able to start a process with a form NOT valid', async () => { formDefinitionSpy.and.returnValue(of(fakeStartFormNotValid)); - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); typeValueInto('#processDefinitionName', 'processwithform'); fixture.detectChanges(); @@ -388,7 +465,7 @@ describe('StartProcessCloudComponent', () => { }); it('should be able to start a process with a prefilled valid form', async () => { - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); formDefinitionSpy.and.returnValue(of(fakeStartForm)); typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); typeValueInto('#processDefinitionName', 'processwithform'); @@ -409,7 +486,7 @@ describe('StartProcessCloudComponent', () => { }); it('should NOT be able to start a process with a prefilled NOT valid form', async () => { - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform'))); formDefinitionSpy.and.returnValue(of(fakeStartFormNotValid)); typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); typeValueInto('#processDefinitionName', 'processwithform'); @@ -431,7 +508,7 @@ describe('StartProcessCloudComponent', () => { }); it('should display enabled start process button if the selection is valid', async () => { - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithoutform2'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithoutform2'))); formDefinitionSpy.and.returnValue(of(fakeStartForm)); typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); typeValueInto('#processDefinitionName', 'processwithoutform2'); @@ -444,7 +521,7 @@ describe('StartProcessCloudComponent', () => { }); it('should have start button enabled when default values are set', async () => { - getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithoutform2'))); + getProcessDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithoutform2'))); formDefinitionSpy.and.returnValue(of(fakeStartForm)); const change = new SimpleChange(null, 'MyApp', true); @@ -461,7 +538,7 @@ describe('StartProcessCloudComponent', () => { beforeEach(() => { component.name = 'My new process'; component.appName = 'myApp'; - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); fixture.detectChanges(); const change = new SimpleChange(null, 'MyApp', true); component.ngOnChanges({ appName: change }); @@ -471,7 +548,7 @@ describe('StartProcessCloudComponent', () => { it('should call service to fetch process definitions with appId', async () => { await fixture.whenStable(); - expect(getDefinitionsSpy).toHaveBeenCalledWith('MyApp'); + expect(getProcessDefinitionsSpy).toHaveBeenCalledWith('MyApp'); }); it('should display the correct number of processes in the select list', async () => { @@ -497,7 +574,7 @@ describe('StartProcessCloudComponent', () => { }); it('should indicate an error to the user if process defs cannot be loaded', async () => { - getDefinitionsSpy.and.returnValue(throwError({})); + getProcessDefinitionsSpy.and.returnValue(throwError({})); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); @@ -509,7 +586,7 @@ describe('StartProcessCloudComponent', () => { }); it('should show no process available message when no process definition is loaded', async () => { - getDefinitionsSpy.and.returnValue(of([])); + getProcessDefinitionsSpy.and.returnValue(of([])); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); @@ -522,7 +599,7 @@ describe('StartProcessCloudComponent', () => { }); it('should select automatically the processDefinition if the app contain only one', async () => { - getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]])); + getProcessDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]])); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); @@ -533,7 +610,7 @@ describe('StartProcessCloudComponent', () => { }); it('should select automatically the form when processDefinition is selected as default', async () => { - getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[2]])); + getProcessDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[2]])); formDefinitionSpy.and.returnValue(of(fakeStartForm)); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); @@ -553,7 +630,7 @@ describe('StartProcessCloudComponent', () => { }); it('should not select automatically any processDefinition if the app contain multiple process and does not have any processDefinition as input', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.appName = 'myApp'; component.ngOnChanges({}); @@ -564,7 +641,7 @@ describe('StartProcessCloudComponent', () => { }); it('should select the right process when the processKey begins with the name', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); formDefinitionSpy.and.returnValue(of(fakeStartForm)); component.name = 'My new process'; component.processDefinitionName = 'process'; @@ -579,7 +656,7 @@ describe('StartProcessCloudComponent', () => { describe('dropdown', () => { it('should hide the process dropdown button if showSelectProcessDropdown is false', async () => { fixture.detectChanges(); - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.appName = 'myApp'; component.showSelectProcessDropdown = false; component.ngOnChanges({}); @@ -593,7 +670,7 @@ describe('StartProcessCloudComponent', () => { it('should show the process dropdown button if showSelectProcessDropdown is false', async () => { fixture.detectChanges(); - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.appName = 'myApp'; component.processDefinitionName = 'NewProcess 2'; component.showSelectProcessDropdown = true; @@ -608,7 +685,7 @@ describe('StartProcessCloudComponent', () => { it('should show the process dropdown button by default', async () => { fixture.detectChanges(); - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.appName = 'myApp'; component.processDefinitionName = 'NewProcess 2'; component.ngOnChanges({}); @@ -631,18 +708,18 @@ describe('StartProcessCloudComponent', () => { }); it('should reload processes when appName input changed', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: firstChange }); component.ngOnChanges({ appName: change }); fixture.detectChanges(); await fixture.whenStable(); - expect(getDefinitionsSpy).toHaveBeenCalledWith('myApp1'); + expect(getProcessDefinitionsSpy).toHaveBeenCalledWith('myApp1'); }); it('should reload processes ONLY when appName input changed', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: firstChange }); fixture.detectChanges(); @@ -650,19 +727,19 @@ describe('StartProcessCloudComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - expect(getDefinitionsSpy).toHaveBeenCalledTimes(1); + expect(getProcessDefinitionsSpy).toHaveBeenCalledTimes(1); }); it('should get current processDef', () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: change }); fixture.detectChanges(); - expect(getDefinitionsSpy).toHaveBeenCalled(); + expect(getProcessDefinitionsSpy).toHaveBeenCalled(); expect(component.processDefinitionList).toBe(fakeProcessDefinitions); }); it('should display the matching results in the dropdown as the user types down', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: change }); fixture.detectChanges(); @@ -680,7 +757,7 @@ describe('StartProcessCloudComponent', () => { }); it('should display the process definion field as empty if are more than one process definition in the list', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: change }); fixture.detectChanges(); @@ -696,7 +773,7 @@ describe('StartProcessCloudComponent', () => { fixture.detectChanges(); component.name = 'NewProcess 1'; component.appName = 'myApp'; - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); fixture.detectChanges(); }); @@ -836,7 +913,7 @@ describe('StartProcessCloudComponent', () => { }); it('should call service with the correct parameters when variables are undefined and formCloud is defined', async () => { - getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[2]])); + getProcessDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[2]])); formDefinitionSpy.and.returnValue(of(fakeStartForm)); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); @@ -894,7 +971,7 @@ describe('StartProcessCloudComponent', () => { }); it('should indicate an error to the user if process cannot be started', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); const error = { @@ -915,7 +992,7 @@ describe('StartProcessCloudComponent', () => { }); it('should use fallback error message when no specific error message is provided', async () => { - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); const change = new SimpleChange('myApp', 'myApp1', true); component.ngOnChanges({ appName: change }); const errorWithoutMessage = { @@ -1040,7 +1117,7 @@ describe('StartProcessCloudComponent', () => { fixture.detectChanges(); let noProcessElement = fixture.nativeElement.querySelector('#no-process-message'); expect(noProcessElement).toBeNull(); - getDefinitionsSpy.and.returnValue(of([])); + getProcessDefinitionsSpy.and.returnValue(of([])); component.loadProcessDefinitions(); fixture.detectChanges(); @@ -1051,7 +1128,7 @@ describe('StartProcessCloudComponent', () => { it('should set the process name using the processName cloud pipe when a process definition gets selected', async () => { const getDefaultProcessNameSpy = spyOn(component, 'getDefaultProcessName').and.returnValue('fake-transformed-name'); const expectedProcessInstanceDetails: ProcessInstanceCloud = { processDefinitionName: fakeProcessDefinitions[0].name }; - getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]])); + getProcessDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]])); formDefinitionSpy.and.stub(); component.appName = 'myApp'; @@ -1085,7 +1162,7 @@ describe('StartProcessCloudComponent', () => { done(); }); - getDefinitionsSpy.and.returnValue(of(definitions)); + getProcessDefinitionsSpy.and.returnValue(of(definitions)); const fakeTransformedName = 'fake-transformed-name'; spyOn(component, 'getDefaultProcessName').and.returnValue(fakeTransformedName); @@ -1166,7 +1243,7 @@ describe('StartProcessCloudComponent', () => { beforeEach(() => { component.name = 'NewProcess 1'; component.appName = 'myApp'; - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: firstChange }); component.processDefinitionList = fakeProcessDefinitions; component.processDefinitionName = fakeProcessDefinitions[0].name; @@ -1212,7 +1289,7 @@ describe('StartProcessCloudComponent', () => { beforeEach(() => { component.name = 'NewProcess 1'; component.appName = 'myApp'; - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); component.ngOnChanges({ appName: firstChange }); fixture.detectChanges(); }); @@ -1235,7 +1312,7 @@ describe('StartProcessCloudComponent', () => { const customOutcomeSelectedSpy = spyOn(component.customOutcomeSelected, 'emit'); const successSpy = spyOn(component.success, 'emit'); - getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); + getProcessDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions)); formDefinitionSpy.and.returnValue(of(fakeFormModelJson)); startProcessWithFormSpy.and.returnValue(of(fakeProcessInstance)); 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 b739478687..61bfaaed6a 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 @@ -63,6 +63,8 @@ import { MatOptionModule } from '@angular/material/core'; import { FormCloudComponent } from '../../../form/components/form-cloud.component'; import { FormCustomOutcomesComponent } from '../../../form/components/form-cloud-custom-outcomes.component'; import { MatDialog } from '@angular/material/dialog'; +import { StartProcessScreenCloudComponent } from '../../../screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component'; +import { TaskTypeResolverService } from '../../../services/task-type-resolver/task-type-resolver.service'; const MAX_NAME_LENGTH: number = 255; const PROCESS_DEFINITION_DEBOUNCE: number = 300; @@ -85,7 +87,8 @@ const PROCESS_DEFINITION_IDENTIFIER_REG_EXP = /%{processdefinition}/i; MatAutocompleteModule, ReactiveFormsModule, FormCloudComponent, - FormCustomOutcomesComponent + FormCustomOutcomesComponent, + StartProcessScreenCloudComponent ], providers: [LocalizedDatePipe], templateUrl: './start-process-cloud.component.html', @@ -177,6 +180,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { isFormCloudLoading = true; processDefinitionLoaded = false; + disableStartProcessForScreen = false; showStartProcessButton$: Observable; startProcessButtonLabel: string; cancelButtonLabel: string; @@ -197,6 +201,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { private readonly displayStartSubject = new BehaviorSubject(null); private readonly hasVisibleOutcomesSubject = new BehaviorSubject(false); private readonly dialog = inject(MatDialog); + private readonly taskTypeResolverService = inject(TaskTypeResolverService); + + private screenSubmitPayload: unknown; showSaveButton = false; showCompleteButton = false; @@ -204,13 +211,17 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { get isProcessFormValid(): boolean { if (this.hasForm && this.isFormCloudLoaded) { return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting; + } else if (this.hasScreen) { + return true; } else { return this.processForm.valid || this.isProcessStarting; } } get disableStartButton(): boolean { - return !this.appName || !this.processDefinition.valid || this.isProcessStarting || this.isFormCloudLoading; + return ( + !this.appName || !this.processDefinition.valid || this.isProcessStarting || this.isFormCloudLoading || this.disableStartProcessForScreen + ); } get isProcessDefinitionsEmpty(): boolean { @@ -226,7 +237,15 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { } get hasForm(): boolean { - return !!this.processDefinitionCurrent?.formKey; + return this.taskTypeResolverService.isFormTask(this.processDefinitionCurrent?.formKey); + } + + get hasScreen(): boolean { + return this.taskTypeResolverService.isScreenTask(this.processDefinitionCurrent?.formKey); + } + + get screenId(): string { + return this.taskTypeResolverService.getScreenId(this.processDefinitionCurrent?.formKey); } get defaultStartProcessButtonLabel(): string { @@ -294,6 +313,14 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { this.hasVisibleOutcomesSubject.next(anyOutcomeVisible); } + onDisableStartProcessButtonForScreen(disable: boolean): void { + this.disableStartProcessForScreen = disable; + } + + onScreenStartProcessPayloadChange(payload: unknown): void { + this.screenSubmitPayload = payload; + } + private getMaxNameLength(): number { return this.maxNameLength > MAX_NAME_LENGTH ? MAX_NAME_LENGTH : this.maxNameLength; } @@ -301,6 +328,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { private selectProcessDefinitionByProcessDefinitionName(processDefinitionName: string): void { this.filteredProcesses = this.getProcessDefinitionListByNameOrKey(processDefinitionName); this.isFormCloudLoading = this.isProcessFormValid && this.filteredProcesses && this.filteredProcesses.length === 1; + if (this.isFormCloudLoading) { this.setProcessDefinitionOnForm(this.filteredProcesses[0].name); } @@ -447,27 +475,31 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { startProcessWithoutConfirmation() { this.isProcessStarting = true; - const action = this.hasForm - ? this.startProcessCloudService.startProcessWithForm( - this.appName, - this.processDefinitionCurrent.formKey, - this.processDefinitionCurrent.version, - new ProcessWithFormPayloadCloud({ - processName: this.processInstanceName.value, - processDefinitionKey: this.processPayloadCloud.processDefinitionKey, - variables: this.variables ?? {}, - values: this.formCloud.values, - outcome: this.customOutcomeName - }) - ) - : this.startProcessCloudService.startProcess( - this.appName, - new ProcessPayloadCloud({ - name: this.processInstanceName.value, - processDefinitionKey: this.processPayloadCloud.processDefinitionKey, - variables: this.variables ?? {} - }) - ); + let action: Observable; + + if (this.hasForm || this.hasScreen) { + action = this.startProcessCloudService.startProcessWithForm( + this.appName, + this.processDefinitionCurrent.formKey, + this.processDefinitionCurrent.version, + new ProcessWithFormPayloadCloud({ + processName: this.processInstanceName.value, + processDefinitionKey: this.processPayloadCloud.processDefinitionKey, + variables: this.variables ?? {}, + values: this.hasForm ? this.formCloud.values : this.screenSubmitPayload, + outcome: this.customOutcomeName + }) + ); + } else { + action = this.startProcessCloudService.startProcess( + this.appName, + new ProcessPayloadCloud({ + name: this.processInstanceName.value, + processDefinitionKey: this.processPayloadCloud.processDefinitionKey, + variables: this.variables ?? {} + }) + ); + } action.subscribe({ next: (res) => { 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 cbce42f06d..bf47b00134 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 @@ -81,13 +81,14 @@ export const fakeProcessDefinitions: ProcessDefinitionCloud[] = [ }) ]; -export const fakeSingleProcessDefinition = (name: string): ProcessDefinitionCloud[] => [ +export const fakeSingleProcessDefinition = (name: string, processDefinitionData?: Partial): ProcessDefinitionCloud[] => [ new ProcessDefinitionCloud({ appName: 'startformwithoutupload', formKey: 'form-a5d50817-5183-4850-802d-17af54b2632f', id: 'd00c0237-8772-11e9-859a-428f83d5904f', key: 'process-5151ad1d-f992-4ee6-9742-3a04617469fe', - name + name, + ...(processDefinitionData ?? {}) }) ]; diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts new file mode 100644 index 0000000000..a8ec356011 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts @@ -0,0 +1,50 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ComponentRef, inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { ScreenRenderingService } from '../../../services/screen-rendering.service'; + +@Component({ + template: '' +}) +export abstract class BaseScreenCloudComponent implements OnInit { + @Input() + screenId: string = ''; + + @ViewChild('container', { read: ViewContainerRef, static: true }) + container: ViewContainerRef; + + protected componentRef: ComponentRef; + protected readonly screenRenderingService = inject(ScreenRenderingService); + + ngOnInit() { + this.createDynamicComponent(); + } + + private createDynamicComponent(): void { + if (this.screenId) { + const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); + this.componentRef = this.container.createComponent(componentType); + this.setInputsForDynamicComponent(); + this.subscribeToOutputs(); + } + } + + protected abstract setInputsForDynamicComponent(): void; + + protected abstract subscribeToOutputs(): void; +} diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.html b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.html new file mode 100644 index 0000000000..21f5a25370 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.html @@ -0,0 +1,9 @@ + + + + + + @if(showStartProcessButtons()) { + + } + diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.scss b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.scss new file mode 100644 index 0000000000..5a60318c78 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.scss @@ -0,0 +1,8 @@ +.adf-cloud-start-process-screen-cloud { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; +} diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts new file mode 100644 index 0000000000..595a7d4bef --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts @@ -0,0 +1,70 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StartProcessScreenCloudComponent } from './start-process-screen-cloud.component'; +import { MockedTaskScreenCloudComponent } from '../../../../testing/start-process-screen-mock.component'; +import { provideScreen } from '../../../services/provide-screen'; +import { By } from '@angular/platform-browser'; +import { StartProcessScreenCloud, StartProcessScreenDefaultButtons } from './start-process-screen.model'; + +describe('StartProcessScreenCloudComponent', () => { + let component: StartProcessScreenCloudComponent; + let fixture: ComponentFixture; + const screenId = 'screen-1234-5678-121212-123456'; + const processDefinitionId = 'definition-id'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StartProcessScreenCloudComponent], + providers: [provideScreen(screenId, MockedTaskScreenCloudComponent)] + }).compileComponents(); + + fixture = TestBed.createComponent(StartProcessScreenCloudComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('processDefinitionId', processDefinitionId); + fixture.componentRef.setInput('screenId', screenId); + + fixture.detectChanges(); + }); + + it('should create provided screen component', () => { + const screenDebugComponent = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)); + expect(screenDebugComponent).toBeTruthy(); + }); + + it('should subscribe to outputs', () => { + const screenStartProcessPayloadChangeSpy = spyOn(component.screenStartProcessPayloadChange, 'emit'); + const disableStartProcessButtonSpy = spyOn(component.disableStartProcessButton, 'emit'); + + const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance; + + const startProcessScreenDefaultButtons: StartProcessScreenDefaultButtons = { show: true, disable: false }; + const startProcessPayload = { payload: 'TEST' }; + screenInstance.defaultStartProcessButtonsConfigurationChange.emit(startProcessScreenDefaultButtons); + screenInstance.startProcessPayloadChanged.emit(startProcessPayload); + + expect(screenStartProcessPayloadChangeSpy).toHaveBeenCalledWith(startProcessPayload); + expect(disableStartProcessButtonSpy).toHaveBeenCalledWith(startProcessScreenDefaultButtons.disable); + expect(component.showStartProcessButtons()).toBe(startProcessScreenDefaultButtons.show); + }); + + it('should set process definition id', () => { + const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance; + expect(screenInstance.processDefinitionId()).toBe(processDefinitionId); + }); +}); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts new file mode 100644 index 0000000000..d066c2ee49 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts @@ -0,0 +1,52 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; +import { BaseScreenCloudComponent } from '../base-screen/base-screen-cloud.component'; +import { MatCardModule } from '@angular/material/card'; +import { CommonModule } from '@angular/common'; +import { StartProcessScreenCloud } from './start-process-screen.model'; + +@Component({ + selector: 'adf-cloud-start-process-screen-cloud', + imports: [CommonModule, MatCardModule], + templateUrl: './start-process-screen-cloud.component.html', + host: { class: 'adf-cloud-start-process-screen-cloud' }, + styleUrls: ['./start-process-screen-cloud.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent { + processDefinitionId = input(''); + screenStartProcessPayloadChange = output(); + disableStartProcessButton = output(); + + showStartProcessButtons = signal(false); + + protected setInputsForDynamicComponent(): void { + if (this.processDefinitionId()) { + this.componentRef.setInput('processDefinitionId', this.processDefinitionId()); + } + } + + protected subscribeToOutputs(): void { + this.componentRef.instance.startProcessPayloadChanged.subscribe((payload) => this.screenStartProcessPayloadChange.emit(payload)); + this.componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => { + this.showStartProcessButtons.set(config.show); + this.disableStartProcessButton.emit(config.disable); + }); + } +} diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen.model.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen.model.ts new file mode 100644 index 0000000000..51a9964d04 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen.model.ts @@ -0,0 +1,28 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputSignal, OutputEmitterRef } from '@angular/core'; + +export interface StartProcessScreenDefaultButtons { + show: boolean; + disable: boolean; +} +export interface StartProcessScreenCloud { + processDefinitionId: InputSignal; + defaultStartProcessButtonsConfigurationChange: OutputEmitterRef; + startProcessPayloadChanged: OutputEmitterRef; +} diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.html b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.html similarity index 100% rename from lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.html rename to lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.html diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts similarity index 98% rename from lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.spec.ts rename to lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts index d55281891b..a78eea608b 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts @@ -18,7 +18,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { ScreenRenderingService } from '../../services/screen-rendering.service'; +import { ScreenRenderingService } from '../../../services/screen-rendering.service'; import { TaskScreenCloudComponent } from './screen-cloud.component'; @Component({ diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts similarity index 85% rename from lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.ts rename to lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts index eca14fcec7..5542954777 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts @@ -16,19 +16,19 @@ */ import { CommonModule } from '@angular/common'; -import { Component, ComponentRef, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core'; -import { ScreenRenderingService } from '../../services/screen-rendering.service'; +import { Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { UserTaskCustomUi } from './screen-cloud.model'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { BaseScreenCloudComponent } from '../base-screen/base-screen-cloud.component'; @Component({ selector: 'adf-cloud-task-screen', imports: [CommonModule, MatCardModule], templateUrl: './screen-cloud.component.html' }) -export class TaskScreenCloudComponent implements OnInit { +export class TaskScreenCloudComponent extends BaseScreenCloudComponent { /** Task id to fetch corresponding form and values. */ @Input() taskId: string; @@ -48,10 +48,6 @@ export class TaskScreenCloudComponent implements OnInit { @Input() showCancelButton: boolean; - /** Screen id to create dynamic component. */ - @Input() - screenId: string = ''; - /** Process Instance Id to fetch corresponding data. */ @Input() processInstanceId: string = ''; @@ -104,27 +100,9 @@ export class TaskScreenCloudComponent implements OnInit { @Output() nextTaskCheckboxCheckedChanged = new EventEmitter(); - @ViewChild('container', { read: ViewContainerRef, static: true }) - container: ViewContainerRef; + private readonly destroyRef = inject(DestroyRef); - private destroyRef = inject(DestroyRef); - componentRef: ComponentRef; - private readonly screenRenderingService = inject(ScreenRenderingService); - - ngOnInit() { - this.createDynamicComponent(); - } - - createDynamicComponent() { - if (this.screenId) { - const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); - this.componentRef = this.container.createComponent(componentType); - this.setInputsForDynamicComponent(); - this.subscribeToOutputs(); - } - } - - setInputsForDynamicComponent(): void { + protected override setInputsForDynamicComponent(): void { if (this.taskId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskId')) { this.componentRef.setInput('taskId', this.taskId); } @@ -160,7 +138,7 @@ export class TaskScreenCloudComponent implements OnInit { } } - subscribeToOutputs() { + protected override subscribeToOutputs(): void { if (this.componentRef.instance?.taskSaved) { this.componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit()); } @@ -172,7 +150,6 @@ export class TaskScreenCloudComponent implements OnInit { if (this.componentRef.instance?.error) { this.componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data)); } - if (this.componentRef.instance?.claimTask) { this.componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data)); } @@ -189,7 +166,7 @@ export class TaskScreenCloudComponent implements OnInit { } } - switchToDisplayMode(newDisplayMode?: string) { + switchToDisplayMode(newDisplayMode?: string): void { if (this.componentRef?.instance?.switchToDisplayMode) { this.componentRef.instance.switchToDisplayMode(newDisplayMode); } diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.model.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.model.ts similarity index 100% rename from lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.model.ts rename to lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.model.ts diff --git a/lib/process-services-cloud/src/lib/screen/public-api.ts b/lib/process-services-cloud/src/lib/screen/public-api.ts index c8decd7901..2068895975 100644 --- a/lib/process-services-cloud/src/lib/screen/public-api.ts +++ b/lib/process-services-cloud/src/lib/screen/public-api.ts @@ -15,6 +15,7 @@ * limitations under the License. */ -export * from './components/screen-cloud/screen-cloud.model'; +export * from './components/screen-cloud/user-task-screen/screen-cloud.model'; +export * from './components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component'; export * from './services/screen-rendering.service'; export * from './services/provide-screen'; diff --git a/lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.spec.ts b/lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.spec.ts new file mode 100644 index 0000000000..385c760db7 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.spec.ts @@ -0,0 +1,63 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { TaskTypeResolverService, UserTaskContentType } from './task-type-resolver.service'; + +describe('TaskTypeResolverService', () => { + let service: TaskTypeResolverService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TaskTypeResolverService); + }); + + it('should return proper user task type', () => { + expect(service.getUserTaskType('form-1234')).toBe(UserTaskContentType.Form); + expect(service.getUserTaskType('screen-1234')).toBe(UserTaskContentType.Screen); + + expect(service.getUserTaskType('other-1234')).toBe(UserTaskContentType.None); + expect(service.getUserTaskType('')).toBe(UserTaskContentType.None); + expect(service.getUserTaskType()).toBe(UserTaskContentType.None); + }); + + it('should detect form task', () => { + expect(service.isFormTask('form-1234')).toBeTrue(); + + expect(service.isFormTask('screen-1234')).toBeFalse(); + expect(service.isFormTask('other-1234')).toBeFalse(); + expect(service.isFormTask('')).toBeFalse(); + expect(service.isFormTask()).toBeFalse(); + }); + + it('should detect screen task', () => { + expect(service.isScreenTask('screen-1234')).toBeTrue(); + + expect(service.isScreenTask('form-1234')).toBeFalse(); + expect(service.isScreenTask('other-1234')).toBeFalse(); + expect(service.isScreenTask('')).toBeFalse(); + expect(service.isScreenTask()).toBeFalse(); + }); + + it('should extract screen id from form key', () => { + expect(service.getScreenId('screen-1234-5678-9121')).toBe('1234-5678-9121'); + expect(service.getScreenId('form-1234')).toBe(''); + expect(service.getScreenId('other-1234')).toBe(''); + expect(service.getScreenId('')).toBe(''); + expect(service.getScreenId()).toBe(''); + }); +}); diff --git a/lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.ts b/lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.ts new file mode 100644 index 0000000000..232f71c410 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/task-type-resolver/task-type-resolver.service.ts @@ -0,0 +1,61 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; + +export const UserTaskContentType = { + Form: 'form', + Screen: 'screen', + None: '' +} as const; + +export type UserTaskType = (typeof UserTaskContentType)[keyof typeof UserTaskContentType]; + +@Injectable({ + providedIn: 'root' +}) +export class TaskTypeResolverService { + isScreenTask(formKey?: string): boolean { + return this.getUserTaskType(formKey) === UserTaskContentType.Screen; + } + + isFormTask(formKey?: string): boolean { + return this.getUserTaskType(formKey) === UserTaskContentType.Form; + } + + getScreenId(formKey?: string): string { + const taskType = this.getUserTaskType(formKey); + + if (taskType === UserTaskContentType.Screen) { + return formKey.replace(`${taskType}-`, ''); + } + + return ''; + } + + getUserTaskType(formKey?: string): UserTaskType { + if (formKey?.includes(UserTaskContentType.Form)) { + return UserTaskContentType.Form; + } + + if (formKey?.includes(UserTaskContentType.Screen)) { + return UserTaskContentType.Screen; + } + + return UserTaskContentType.None; + } +} 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 864e99364c..988ad2e29b 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 @@ -26,7 +26,7 @@ import { of, throwError } from 'rxjs'; import { IdentityUserService } from '../../../../people/services/identity-user.service'; import { UserTaskCloudComponent } from './user-task-cloud.component'; import { By } from '@angular/platform-browser'; -import { TaskScreenCloudComponent } from '../../../../screen/components/screen-cloud/screen-cloud.component'; +import { TaskScreenCloudComponent } from '../../../../screen/components/screen-cloud/user-task-screen/screen-cloud.component'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { NoopAuthModule, NoopTranslateModule } from '@alfresco/adf-core'; import { @@ -42,7 +42,7 @@ 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 = { +const createTaskDetailsCloudModel = (): TaskDetailsCloudModel => ({ appName: 'simple-app', assignee: 'admin.adf', completedDate: null, @@ -55,7 +55,7 @@ const taskDetails: TaskDetailsCloudModel = { standalone: false, status: TASK_ASSIGNED_STATE, permissions: [TASK_VIEW_PERMISSION] -}; +}); @Component({ selector: 'adf-cloud-task-screen', @@ -94,8 +94,11 @@ describe('UserTaskCloudComponent', () => { let loader: HarnessLoader; let identityUserService: IdentityUserService; let errorEmitSpy: jasmine.Spy; + let taskDetails: TaskDetailsCloudModel; beforeEach(() => { + taskDetails = createTaskDetailsCloudModel(); + TestBed.configureTestingModule({ imports: [NoopTranslateModule, NoopAuthModule, UserTaskCloudComponent, TaskFormCloudComponent] }).overrideComponent(UserTaskCloudComponent, { 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 32167fec94..602f8c2cdb 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 @@ -28,18 +28,11 @@ import { UserTaskCloudButtonsComponent } from '../user-task-cloud-buttons/user-t import { TranslatePipe } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; -import { TaskScreenCloudComponent } from '../../../../screen/components/screen-cloud/screen-cloud.component'; +import { TaskScreenCloudComponent } from '../../../../screen/components/screen-cloud/user-task-screen/screen-cloud.component'; import { CompleteTaskDirective } from './complete-task/complete-task.directive'; import { catchError, EMPTY, forkJoin } from 'rxjs'; import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox'; - -const TaskTypes = { - Form: 'form', - Screen: 'screen', - None: '' -} as const; - -type TaskTypesType = (typeof TaskTypes)[keyof typeof TaskTypes]; +import { UserTaskContentType, TaskTypeResolverService, UserTaskType } from '../../../../services/task-type-resolver/task-type-resolver.service'; @Component({ selector: 'adf-cloud-user-task', @@ -183,12 +176,13 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { candidateUsers: string[] = []; candidateGroups: string[] = []; loading: boolean = false; - screenId: string; taskDetails: TaskDetailsCloudModel; - taskType: TaskTypesType; - taskTypeEnum = TaskTypes; + taskType: UserTaskType; + taskTypeEnum = UserTaskContentType; + screenId: string; private taskCloudService: TaskCloudService = inject(TaskCloudService); + private readonly taskTypeResolverService = inject(TaskTypeResolverService); private readonly destroyRef = inject(DestroyRef); ngOnChanges(changes: SimpleChanges) { @@ -224,19 +218,8 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { } getTaskType(): void { - if (this.taskDetails && !!this.taskDetails.formKey) { - if (this.taskDetails.formKey.includes(this.taskTypeEnum.Form)) { - this.taskType = this.taskTypeEnum.Form; - return; - } else if (this.taskDetails.formKey.includes(this.taskTypeEnum.Screen)) { - this.taskType = this.taskTypeEnum.Screen; - const screenId = this.taskDetails.formKey.replace(this.taskTypeEnum.Screen + '-', ''); - this.screenId = screenId; - return; - } - } - - this.taskType = this.taskTypeEnum.None; + this.taskType = this.taskTypeResolverService.getUserTaskType(this.taskDetails?.formKey); + this.screenId = this.taskTypeResolverService.getScreenId(this.taskDetails?.formKey); } hasCandidateUsers(): boolean { @@ -284,6 +267,7 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { onExecuteOutcome(outcome: FormOutcomeEvent): void { this.executeOutcome.emit(outcome); } + onFormContentClicked(content: ContentLinkModel): void { this.formContentClicked.emit(content); } @@ -309,6 +293,7 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { const tasks$ = this.taskCloudService.getTaskById(this.appName, this.taskId); const candidateUsers$ = this.taskCloudService.getCandidateUsers(this.appName, this.taskId); const candidateGroups$ = this.taskCloudService.getCandidateGroups(this.appName, this.taskId); + forkJoin({ tasks: tasks$, candidateUsers: candidateUsers$, diff --git a/lib/process-services-cloud/src/lib/testing/start-process-screen-mock.component.ts b/lib/process-services-cloud/src/lib/testing/start-process-screen-mock.component.ts new file mode 100644 index 0000000000..92138774fa --- /dev/null +++ b/lib/process-services-cloud/src/lib/testing/start-process-screen-mock.component.ts @@ -0,0 +1,32 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, input, output } from '@angular/core'; +import { + StartProcessScreenCloud, + StartProcessScreenDefaultButtons +} from '../screen/components/screen-cloud/start-process-event-screen/start-process-screen.model'; + +@Component({ + selector: 'adf-cloud-mock-screen-component', + template: `
Mock Screen Component
` +}) +export class MockedTaskScreenCloudComponent implements StartProcessScreenCloud { + processDefinitionId = input(''); + defaultStartProcessButtonsConfigurationChange = output(); + startProcessPayloadChanged = output(); +}