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/screen-cloud.component.html new file mode 100644 index 00000000000..94f8ea35f10 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.html @@ -0,0 +1 @@ +
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/screen-cloud.component.spec.ts new file mode 100644 index 00000000000..43fc628f48e --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.spec.ts @@ -0,0 +1,53 @@ +/*! + * @license + * Copyright © 2005-2024 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 { TaskScreenCloudComponent } from './screen-cloud.component'; +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ScreenRenderingService } from '../../../services/public-api'; +import { By } from '@angular/platform-browser'; + +@Component({ + selector: 'adf-cloud-test-component', + template: `
test component
`, + imports: [CommonModule], + standalone: true +}) +class TestComponent {} + +describe('TaskScreenCloudComponent', () => { + let fixture: ComponentFixture; + let screenRenderingService: ScreenRenderingService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TaskScreenCloudComponent, TestComponent] + }); + fixture = TestBed.createComponent(TaskScreenCloudComponent); + screenRenderingService = TestBed.inject(ScreenRenderingService); + screenRenderingService.register({ ['test']: () => TestComponent }); + fixture.componentRef.setInput('screenId', 'test'); + fixture.detectChanges(); + }); + + it('should create custom component instance', () => { + const dynamicComponent = fixture.debugElement.query(By.css('.adf-cloud-test-container')); + expect(dynamicComponent).toBeTruthy(); + }); +}); 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/screen-cloud.component.ts new file mode 100644 index 00000000000..3ea9c1a4bb9 --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/screen-cloud.component.ts @@ -0,0 +1,62 @@ +/*! + * @license + * Copyright © 2005-2024 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 { CommonModule } from '@angular/common'; +import { Component, ComponentRef, inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { ScreenRenderingService } from '../../../services/public-api'; + +@Component({ + selector: 'adf-cloud-task-screen', + standalone: true, + imports: [CommonModule], + template: '
' +}) +export class TaskScreenCloudComponent implements OnInit { + /** Task id to fetch corresponding form and values. */ + @Input() taskId: string; + /** App id to fetch corresponding form and values. */ + @Input() + appName: string = ''; + /** Screen id to fetch corresponding screen widget. */ + @Input() + screenId: string = ''; + /** Toggle readonly state of the task. */ + @Input() + readOnly = false; + + @ViewChild('container', { read: ViewContainerRef, static: true }) + container: ViewContainerRef; + componentRef: ComponentRef; + + private readonly screenRenderingService = inject(ScreenRenderingService); + + ngOnInit() { + if (this.screenId) { + const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); + this.componentRef = this.container.createComponent(componentType); + if (this.taskId) { + this.componentRef.setInput('taskId', this.taskId); + } + if (this.appName) { + this.componentRef.setInput('appName', this.appName); + } + if (this.screenId) { + this.componentRef.setInput('screenId', this.screenId); + } + } + } +} diff --git a/lib/process-services-cloud/src/lib/services/public-api.ts b/lib/process-services-cloud/src/lib/services/public-api.ts index 40684e03a66..f07bd965135 100644 --- a/lib/process-services-cloud/src/lib/services/public-api.ts +++ b/lib/process-services-cloud/src/lib/services/public-api.ts @@ -15,13 +15,14 @@ * limitations under the License. */ -export * from './user-preference-cloud.service'; -export * from './local-preference-cloud.service'; +export * from './base-cloud.service'; export * from './cloud-token.service'; +export * from './form-fields.interfaces'; +export * from './local-preference-cloud.service'; export * from './notification-cloud.service'; export * from './preference-cloud.interface'; -export * from './form-fields.interfaces'; -export * from './base-cloud.service'; +export * from './screen-rendering.service'; export * from './task-list-cloud.service.interface'; +export * from './user-preference-cloud.service'; export * from './variable-mapper.sevice'; export * from './web-socket.service'; diff --git a/lib/process-services-cloud/src/lib/services/screen-rendering.service.spec.ts b/lib/process-services-cloud/src/lib/services/screen-rendering.service.spec.ts new file mode 100644 index 00000000000..542c61a872b --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/screen-rendering.service.spec.ts @@ -0,0 +1,32 @@ +/*! + * @license + * Copyright © 2005-2024 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 { ScreenRenderingService } from './screen-rendering.service'; + +describe('ScreenRenderingService', () => { + let service: ScreenRenderingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ScreenRenderingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/lib/process-services-cloud/src/lib/services/screen-rendering.service.ts b/lib/process-services-cloud/src/lib/services/screen-rendering.service.ts new file mode 100644 index 00000000000..156f727a454 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/screen-rendering.service.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright © 2005-2024 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 { DynamicComponentMapper } from '@alfresco/adf-core'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ScreenRenderingService extends DynamicComponentMapper {} diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.html deleted file mode 100644 index 05a1d244644..00000000000 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.html +++ /dev/null @@ -1,73 +0,0 @@ -
- - - - - - - - - - - -

- - {{ taskDetails?.name || 'FORM.FORM_RENDERER.NAMELESS_TASK' | translate }} - -

-
-
- - - - - - - - -
-
- - - - - -
- - - - diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html new file mode 100644 index 00000000000..233ffabda54 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.html @@ -0,0 +1,39 @@ +
+ + + + + +
diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.scss b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.scss similarity index 67% rename from lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.scss rename to lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.scss index 80e7995fcca..3ed08852124 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.scss @@ -29,23 +29,4 @@ } } } - - &-cloud-spinner { - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } -} - -adf-cloud-task-form { - .adf-task-form-cloud-spinner { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - overflow: hidden; - } } diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts new file mode 100644 index 00000000000..4d9a9f3be20 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.spec.ts @@ -0,0 +1,282 @@ +/*! + * @license + * Copyright © 2005-2024 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 { FORM_FIELD_VALIDATORS, FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core'; +import { FormCustomOutcomesComponent } from '@alfresco/adf-process-services-cloud'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { FormCloudComponent } from '../../../../form/components/form-cloud.component'; +import { DisplayModeService } from '../../../../form/services/display-mode.service'; +import { IdentityUserService } from '../../../../people/services/identity-user.service'; +import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module'; +import { TaskCloudService } from '../../../services/task-cloud.service'; +import { + TASK_ASSIGNED_STATE, + TASK_CLAIM_PERMISSION, + TASK_CREATED_STATE, + TASK_RELEASE_PERMISSION, + TASK_VIEW_PERMISSION, + TaskDetailsCloudModel +} from '../../../start-task/models/task-details-cloud.model'; +import { MockFormFieldValidator } from '../../mocks/task-form-cloud.mock'; +import { UserTaskCloudButtonsComponent } from '../user-task-cloud-buttons/user-task-cloud-buttons.component'; +import { TaskFormCloudComponent } from './task-form-cloud.component'; + +const taskDetails: TaskDetailsCloudModel = { + appName: 'simple-app', + appVersion: 1, + assignee: 'admin.adf', + completedDate: null, + createdDate: new Date(1555419255340), + description: null, + formKey: null, + id: 'bd6b1741-6046-11e9-80f0-0a586460040d', + name: 'Task1', + owner: 'admin.adf', + standalone: false, + status: TASK_ASSIGNED_STATE, + permissions: [TASK_VIEW_PERMISSION] +}; + +describe('TaskFormCloudComponent', () => { + let taskCloudService: TaskCloudService; + let identityUserService: IdentityUserService; + let getCurrentUserSpy: jasmine.Spy; + let component: TaskFormCloudComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ProcessServiceCloudTestingModule], + declarations: [FormCloudComponent, UserTaskCloudButtonsComponent, FormCustomOutcomesComponent] + }); + taskDetails.status = TASK_ASSIGNED_STATE; + taskDetails.permissions = [TASK_VIEW_PERMISSION]; + taskDetails.standalone = false; + + identityUserService = TestBed.inject(IdentityUserService); + getCurrentUserSpy = spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue({ username: 'admin.adf' }); + taskCloudService = TestBed.inject(TaskCloudService); + fixture = TestBed.createComponent(TaskFormCloudComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + fixture.destroy(); + }); + + describe('Claim/Unclaim buttons', () => { + beforeEach(() => { + spyOn(component, 'hasCandidateUsers').and.returnValue(true); + fixture.componentRef.setInput('taskDetails', taskDetails); + component.taskId = 'task1'; + component.showCancelButton = true; + fixture.detectChanges(); + }); + + it('should not show release button for standalone task', () => { + taskDetails.permissions = [TASK_RELEASE_PERMISSION]; + taskDetails.standalone = true; + fixture.detectChanges(); + const canUnclaimTask = component.canUnclaimTask(); + + expect(canUnclaimTask).toBe(false); + }); + + it('should not show claim button for standalone task', () => { + taskDetails.status = TASK_CREATED_STATE; + taskDetails.permissions = [TASK_CLAIM_PERMISSION]; + taskDetails.standalone = true; + fixture.detectChanges(); + const canClaimTask = component.canClaimTask(); + + expect(canClaimTask).toBe(false); + }); + + it('should show release button when task is assigned to one of the candidate users', () => { + taskDetails.permissions = [TASK_RELEASE_PERMISSION]; + fixture.detectChanges(); + const canUnclaimTask = component.canUnclaimTask(); + + expect(canUnclaimTask).toBe(true); + }); + + it('should not show unclaim button when status is ASSIGNED but assigned to different person', () => { + getCurrentUserSpy.and.returnValue({}); + fixture.detectChanges(); + const canUnclaimTask = component.canUnclaimTask(); + + expect(canUnclaimTask).toBe(false); + }); + + it('should not show unclaim button when status is not ASSIGNED', () => { + taskDetails.status = undefined; + fixture.detectChanges(); + const canUnclaimTask = component.canUnclaimTask(); + + expect(canUnclaimTask).toBe(false); + }); + + it('should not show unclaim button when status is ASSIGNED and permissions not include RELEASE', () => { + taskDetails.status = TASK_ASSIGNED_STATE; + taskDetails.permissions = [TASK_VIEW_PERMISSION]; + fixture.detectChanges(); + const canUnclaimTask = component.canUnclaimTask(); + + expect(canUnclaimTask).toBe(false); + }); + + it('should show claim button when status is CREATED and permission includes CLAIM', () => { + taskDetails.status = TASK_CREATED_STATE; + taskDetails.permissions = [TASK_CLAIM_PERMISSION]; + fixture.detectChanges(); + const canClaimTask = component.canClaimTask(); + + expect(canClaimTask).toBe(true); + }); + + it('should not show claim button when status is not CREATED', () => { + taskDetails.status = undefined; + fixture.detectChanges(); + const canClaimTask = component.canClaimTask(); + + expect(canClaimTask).toBe(false); + }); + + it('should not show claim button when status is CREATED and permission not includes CLAIM', () => { + taskDetails.status = TASK_CREATED_STATE; + taskDetails.permissions = [TASK_VIEW_PERMISSION]; + fixture.detectChanges(); + const canClaimTask = component.canClaimTask(); + + expect(canClaimTask).toBe(false); + }); + }); + + describe('Inputs', () => { + beforeEach(() => { + fixture.componentRef.setInput('taskDetails', taskDetails); + }); + + it('should not show complete/claim/unclaim buttons when readOnly=true', () => { + component.appName = 'app1'; + component.taskId = 'task1'; + component.readOnly = true; + fixture.detectChanges(); + + const canShowCompleteBtn = component.canCompleteTask(); + expect(canShowCompleteBtn).toBe(false); + + const canClaimTask = component.canClaimTask(); + expect(canClaimTask).toBe(false); + + const canUnclaimTask = component.canUnclaimTask(); + expect(canUnclaimTask).toBe(false); + }); + + it('should append additional field validators to the default ones when provided', () => { + const mockFirstCustomFieldValidator = new MockFormFieldValidator(); + const mockSecondCustomFieldValidator = new MockFormFieldValidator(); + fixture.componentRef.setInput('fieldValidators', [mockFirstCustomFieldValidator, mockSecondCustomFieldValidator]); + fixture.detectChanges(); + + expect(component.fieldValidators).toEqual([...FORM_FIELD_VALIDATORS, mockFirstCustomFieldValidator, mockSecondCustomFieldValidator]); + }); + + it('should use default field validators when no additional validators are provided', () => { + fixture.detectChanges(); + + expect(component.fieldValidators).toEqual([...FORM_FIELD_VALIDATORS]); + }); + }); + + describe('Events', () => { + beforeEach(() => { + fixture.componentRef.setInput('taskDetails', taskDetails); + component.appName = 'app1'; + component.taskId = 'task1'; + fixture.detectChanges(); + }); + + it('should emit cancelClick when cancel button is clicked', async () => { + spyOn(component.cancelClick, 'emit').and.stub(); + component.onCancelClick(); + fixture.detectChanges(); + + expect(component.cancelClick.emit).toHaveBeenCalledOnceWith('task1'); + }); + + it('should emit taskClaimed when task is claimed', async () => { + spyOn(taskCloudService, 'claimTask').and.returnValue(of({})); + spyOn(component, 'hasCandidateUsers').and.returnValue(true); + spyOn(component.taskClaimed, 'emit').and.stub(); + taskDetails.status = TASK_CREATED_STATE; + taskDetails.permissions = [TASK_CLAIM_PERMISSION]; + component.onClaimTask(); + fixture.detectChanges(); + + expect(component.taskClaimed.emit).toHaveBeenCalledOnceWith('task1'); + }); + + it('should emit error when error occurs', async () => { + spyOn(component.error, 'emit').and.stub(); + component.onError({}); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.error.emit).toHaveBeenCalled(); + }); + + it('should emit an executeOutcome event when form outcome executed', () => { + const executeOutcomeSpy: jasmine.Spy = spyOn(component.executeOutcome, 'emit'); + component.onFormExecuteOutcome(new FormOutcomeEvent(new FormOutcomeModel(new FormModel()))); + + expect(executeOutcomeSpy).toHaveBeenCalled(); + }); + + it('should emit displayModeOn when display mode is turned on', async () => { + spyOn(component.displayModeOn, 'emit').and.stub(); + component.onDisplayModeOn(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.displayModeOn.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); + }); + + it('should emit displayModeOff when display mode is turned on', async () => { + spyOn(component.displayModeOff, 'emit').and.stub(); + component.onDisplayModeOff(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.displayModeOff.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); + }); + }); + + it('should call children cloud task form change display mode when changing the display mode', () => { + const displayMode = 'displayMode'; + component.taskDetails = { ...taskDetails, formKey: 'some-form' }; + fixture.detectChanges(); + + expect(component.adfCloudForm).toBeDefined(); + + const switchToDisplayModeSpy = spyOn(component.adfCloudForm, 'switchToDisplayMode'); + component.switchToDisplayMode(displayMode); + + expect(switchToDisplayModeSpy).toHaveBeenCalledOnceWith(displayMode); + }); +}); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.stories.ts b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.stories.ts similarity index 91% rename from lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.stories.ts rename to lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.stories.ts index e1d34d1ebcf..91f657f5c47 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.stories.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.stories.ts @@ -16,13 +16,13 @@ */ import { applicationConfig, Meta, moduleMetadata, StoryFn } from '@storybook/angular'; -import { FormCloudService } from '../../../form/public-api'; -import { TaskCloudService } from '../../services/task-cloud.service'; -import { TaskFormModule } from '../task-form.module'; +import { FormCloudService } from '../../../../form/public-api'; +import { TaskCloudService } from '../../../services/task-cloud.service'; +import { TaskFormModule } from '../../task-form.module'; import { TaskFormCloudComponent } from './task-form-cloud.component'; -import { TaskCloudServiceMock } from '../../mock/task-cloud.service.mock'; -import { FormCloudServiceMock } from '../../../form/mocks/form-cloud.service.mock'; -import { ProcessServicesCloudStoryModule } from '../../../testing/process-services-cloud-story.module'; +import { TaskCloudServiceMock } from '../../../mock/task-cloud.service.mock'; +import { FormCloudServiceMock } from '../../../../form/mocks/form-cloud.service.mock'; +import { ProcessServicesCloudStoryModule } from '../../../../testing/process-services-cloud-story.module'; import { importProvidersFrom } from '@angular/core'; export default { @@ -37,9 +37,7 @@ export default { ] }), applicationConfig({ - providers: [ - importProvidersFrom(ProcessServicesCloudStoryModule) - ] + providers: [importProvidersFrom(ProcessServicesCloudStoryModule)] }) ], argTypes: { diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts similarity index 73% rename from lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.ts rename to lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts index fbf86f03843..5427b01a244 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud/task-form-cloud.component.ts @@ -15,28 +15,15 @@ * limitations under the License. */ -import { - Component, - DestroyRef, - EventEmitter, - inject, - Input, - OnChanges, - OnInit, - Output, - SimpleChanges, - ViewChild, - ViewEncapsulation -} from '@angular/core'; -import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; -import { TaskCloudService } from '../../services/task-cloud.service'; import { ContentLinkModel, FORM_FIELD_VALIDATORS, FormFieldValidator, FormModel, FormOutcomeEvent, FormRenderingService } from '@alfresco/adf-core'; -import { AttachFileCloudWidgetComponent } from '../../../form/components/widgets/attach-file/attach-file-cloud-widget.component'; -import { DropdownCloudWidgetComponent } from '../../../form/components/widgets/dropdown/dropdown-cloud.widget'; -import { DateCloudWidgetComponent } from '../../../form/components/widgets/date/date-cloud.widget'; -import { FormCloudDisplayModeConfiguration } from '../../../services/form-fields.interfaces'; -import { FormCloudComponent } from '../../../form/components/form-cloud.component'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { FormCloudComponent } from '../../../../form/components/form-cloud.component'; +import { AttachFileCloudWidgetComponent } from '../../../../form/components/widgets/attach-file/attach-file-cloud-widget.component'; +import { DateCloudWidgetComponent } from '../../../../form/components/widgets/date/date-cloud.widget'; +import { DropdownCloudWidgetComponent } from '../../../../form/components/widgets/dropdown/dropdown-cloud.widget'; +import { FormCloudDisplayModeConfiguration } from '../../../../services/form-fields.interfaces'; +import { TaskCloudService } from '../../../services/task-cloud.service'; +import { TaskDetailsCloudModel } from '../../../start-task/models/task-details-cloud.model'; @Component({ selector: 'adf-cloud-task-form', @@ -44,11 +31,19 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; styleUrls: ['./task-form-cloud.component.scss'], encapsulation: ViewEncapsulation.None }) -export class TaskFormCloudComponent implements OnInit, OnChanges { +export class TaskFormCloudComponent implements OnInit { /** App id to fetch corresponding form and values. */ @Input() appName: string = ''; + /**Candidates users*/ + @Input() + candidateUsers: string[] = []; + + /**Candidates groups */ + @Input() + candidateGroups: string[] = []; + /** Task id to fetch corresponding form and values. */ @Input() taskId: string; @@ -87,6 +82,10 @@ export class TaskFormCloudComponent implements OnInit, OnChanges { @Input() fieldValidators: FormFieldValidator[]; + /** Task details. */ + @Input() + taskDetails: TaskDetailsCloudModel; + /** Emitted when the form is saved. */ @Output() formSaved = new EventEmitter(); @@ -126,12 +125,6 @@ export class TaskFormCloudComponent implements OnInit, OnChanges { @Output() executeOutcome = new EventEmitter(); - /** - * Emitted when a task is loaded`. - */ - @Output() - onTaskLoaded = new EventEmitter(); /* eslint-disable-line */ - /** Emitted when a display mode configuration is turned on. */ @Output() displayModeOn = new EventEmitter(); @@ -143,15 +136,8 @@ export class TaskFormCloudComponent implements OnInit, OnChanges { @ViewChild('adfCloudForm', { static: false }) adfCloudForm: FormCloudComponent; - taskDetails: TaskDetailsCloudModel; - - candidateUsers: string[] = []; - candidateGroups: string[] = []; - loading: boolean = false; - private readonly destroyRef = inject(DestroyRef); - constructor(private taskCloudService: TaskCloudService, private formRenderingService: FormRenderingService) { this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileCloudWidgetComponent, true); this.formRenderingService.setComponentTypeResolver('dropdown', () => DropdownCloudWidgetComponent, true); @@ -160,46 +146,12 @@ export class TaskFormCloudComponent implements OnInit, OnChanges { ngOnInit() { this.initFieldValidators(); - - if (this.appName === '' && this.taskId) { - this.loadTask(); - } - } - - ngOnChanges(changes: SimpleChanges) { - const appName = changes['appName']; - if (appName && appName.currentValue !== appName.previousValue && this.taskId) { - this.loadTask(); - return; - } - - const taskId = changes['taskId']; - if (taskId?.currentValue && this.appName) { - this.loadTask(); - return; - } } private initFieldValidators() { this.fieldValidators = this.fieldValidators ? [...FORM_FIELD_VALIDATORS, ...this.fieldValidators] : [...FORM_FIELD_VALIDATORS]; } - private loadTask() { - this.loading = true; - this.taskCloudService - .getTaskById(this.appName, this.taskId) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((details) => { - this.taskDetails = details; - this.loading = false; - this.onTaskLoaded.emit(this.taskDetails); - }); - - this.taskCloudService.getCandidateUsers(this.appName, this.taskId).subscribe((users) => (this.candidateUsers = users || [])); - - this.taskCloudService.getCandidateGroups(this.appName, this.taskId).subscribe((groups) => (this.candidateGroups = groups || [])); - } - hasForm(): boolean { return this.taskDetails && !!this.taskDetails.formKey; } @@ -212,6 +164,10 @@ export class TaskFormCloudComponent implements OnInit, OnChanges { return !this.readOnly && this.taskCloudService.canClaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); } + canUnclaimTask(): boolean { + return !this.readOnly && this.taskCloudService.canUnclaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); + } + hasCandidateUsers(): boolean { return this.candidateUsers.length !== 0; } @@ -224,26 +180,19 @@ export class TaskFormCloudComponent implements OnInit, OnChanges { return this.hasCandidateUsers() || this.hasCandidateGroups(); } - canUnclaimTask(): boolean { - return !this.readOnly && this.taskCloudService.canUnclaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); - } - isReadOnly(): boolean { return this.readOnly || !this.taskCloudService.canCompleteTask(this.taskDetails); } onCompleteTask() { - this.loadTask(); this.taskCompleted.emit(this.taskId); } onClaimTask() { - this.loadTask(); this.taskClaimed.emit(this.taskId); } onUnclaimTask() { - this.loadTask(); this.taskUnclaimed.emit(this.taskId); } diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html new file mode 100644 index 00000000000..7b06233dee9 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.html @@ -0,0 +1,32 @@ + + + diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts new file mode 100644 index 00000000000..bc523077213 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.spec.ts @@ -0,0 +1,130 @@ +/*! + * @license + * Copyright © 2005-2024 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 { UserTaskCloudButtonsComponent } from './user-task-cloud-buttons.component'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { NoopTranslateModule } from '@alfresco/adf-core'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { ProcessServiceCloudTestingModule } from 'lib/process-services-cloud/src/lib/testing/process-service-cloud.testing.module'; +import { TaskCloudService } from '@alfresco/adf-process-services-cloud'; +import { of } from 'rxjs'; + +describe('UserTaskCloudButtonsComponent', () => { + let component: UserTaskCloudButtonsComponent; + let fixture: ComponentFixture; + let loader: HarnessLoader; + let debugElement: DebugElement; + let taskCloudService: TaskCloudService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopTranslateModule, ProcessServiceCloudTestingModule], + declarations: [UserTaskCloudButtonsComponent] + }); + fixture = TestBed.createComponent(UserTaskCloudButtonsComponent); + debugElement = fixture.debugElement; + component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + taskCloudService = TestBed.inject(TaskCloudService); + + fixture.componentRef.setInput('appName', 'app-test'); + fixture.componentRef.setInput('taskId', 'task1'); + + fixture.detectChanges(); + }); + + it('should show cancel button', async () => { + fixture.componentRef.setInput('showCancelButton', false); + let cancelButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + + expect(cancelButton).toBeNull(); + + fixture.componentRef.setInput('showCancelButton', true); + fixture.detectChanges(); + cancelButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + + expect(cancelButton).toBeTruthy(); + }); + + it('should emit onCancelClick when cancel button clicked', async () => { + const cancelClickSpy = spyOn(component.cancelClick, 'emit'); + fixture.componentRef.setInput('showCancelButton', true); + fixture.detectChanges(); + const cancelButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + await cancelButton.click(); + expect(cancelClickSpy).toHaveBeenCalled(); + }); + + it('should show claim button', async () => { + let claimButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '.adf-user-task-cloud-claim-btn' })); + + expect(claimButton).toBeNull(); + + fixture.componentRef.setInput('canClaimTask', true); + fixture.detectChanges(); + claimButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '.adf-user-task-cloud-claim-btn' })); + + expect(claimButton).toBeTruthy(); + }); + + it('should emit claimTask when claim button clicked', async () => { + spyOn(taskCloudService, 'claimTask').and.returnValue(of({})); + fixture.componentRef.setInput('canClaimTask', true); + spyOn(component.claimTask, 'emit').and.stub(); + fixture.detectChanges(); + + const claimButton = debugElement.query(By.css('[adf-cloud-claim-task]')); + expect(claimButton).toBeTruthy(); + + claimButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.claimTask.emit).toHaveBeenCalled(); + }); + + it('should show unclaim button', async () => { + let unclaimButton: MatButtonHarness = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '.adf-user-task-cloud-unclaim-btn' })); + + expect(unclaimButton).toBeNull(); + + fixture.componentRef.setInput('canUnclaimTask', true); + fixture.detectChanges(); + unclaimButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '.adf-user-task-cloud-unclaim-btn' })); + + expect(unclaimButton).toBeTruthy(); + }); + + it('should emit unclaim when button clicked', async () => { + spyOn(taskCloudService, 'unclaimTask').and.returnValue(of({})); + fixture.componentRef.setInput('canUnclaimTask', true); + spyOn(component.unclaimTask, 'emit').and.stub(); + fixture.detectChanges(); + + const unclaimButton = debugElement.query(By.css('[adf-cloud-unclaim-task]')); + expect(unclaimButton).toBeTruthy(); + unclaimButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.unclaimTask.emit).toHaveBeenCalled(); + }); +}); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts new file mode 100644 index 00000000000..c5f9ece2186 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud-buttons/user-task-cloud-buttons.component.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright © 2005-2024 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, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'adf-cloud-user-task-cloud-buttons', + styles: ['button { margin-right: 8px; }'], + templateUrl: './user-task-cloud-buttons.component.html' +}) +export class UserTaskCloudButtonsComponent { + /** App id to fetch corresponding form and values. */ + @Input() + appName: string = ''; + + @Input() + canClaimTask: boolean; + + @Input() + canUnclaimTask: boolean; + + /** Task id to fetch corresponding form and values. */ + @Input() + taskId: string; + + /** Toggle rendering of the `Cancel` button. */ + @Input() + showCancelButton = true; + + /** Emitted when any error occurs. */ + @Output() error = new EventEmitter(); + + /** Emitted when the cancel button is clicked. */ + @Output() cancelClick = new EventEmitter(); + + /** Emitted when the task is claimed. */ + @Output() claimTask = new EventEmitter(); + + /** Emitted when the task is unclaimed. */ + @Output() unclaimTask = new EventEmitter(); + + onError(data: any): void { + this.error.emit(data); + } + + onUnclaimTask(): void { + this.unclaimTask.emit(); + } + + onClaimTask(): void { + this.claimTask.emit(); + } + + onCancelClick(): void { + this.cancelClick.emit(); + } +} diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html new file mode 100644 index 00000000000..536db5439e8 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.html @@ -0,0 +1,92 @@ +
+
+ + + + + + + + + + + + + +

+ + {{ taskDetails?.name || 'FORM.FORM_RENDERER.NAMELESS_TASK' | translate }} + +

+
+
+ + + + + + + + +
+
+ +
+
+
+ + + + + + + + diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.scss b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.scss new file mode 100644 index 00000000000..0878f3b860c --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.scss @@ -0,0 +1,13 @@ +.adf-user-task-cloud-container { + height: 100%; + + > div { + height: 100%; + } +} + +.adf-user-task-cloud-spinner { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts similarity index 53% rename from lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.spec.ts rename to lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts index cbba6d68888..cbdd777850f 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/components/task-form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.spec.ts @@ -15,29 +15,28 @@ * limitations under the License. */ -import { DebugElement, SimpleChange } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { of } from 'rxjs'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FORM_FIELD_VALIDATORS, FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core'; -import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; -import { TaskFormCloudComponent } from './task-form-cloud.component'; +import { NoopTranslateModule } from '@alfresco/adf-core'; import { - TaskDetailsCloudModel, TASK_ASSIGNED_STATE, TASK_CLAIM_PERMISSION, TASK_CREATED_STATE, TASK_RELEASE_PERMISSION, - TASK_VIEW_PERMISSION -} from '../../start-task/models/task-details-cloud.model'; -import { TaskCloudService } from '../../services/task-cloud.service'; -import { IdentityUserService } from '../../../people/services/identity-user.service'; + TASK_VIEW_PERMISSION, + TaskCloudService, + TaskDetailsCloudModel, + TaskFormCloudComponent +} from '@alfresco/adf-process-services-cloud'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { SimpleChange } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { MatCardHarness } from '@angular/material/card/testing'; import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing'; -import { DisplayModeService } from '../../../form/services/display-mode.service'; -import { FormCloudComponent } from '../../../form/components/form-cloud.component'; -import { MockFormFieldValidator } from '../mocks/task-form-cloud.mock'; +import { ProcessServiceCloudTestingModule } from 'lib/process-services-cloud/src/lib/testing/process-service-cloud.testing.module'; +import { of } from 'rxjs'; +import { IdentityUserService } from '../../../../people/services/identity-user.service'; +import { UserTaskCloudComponent } from './user-task-cloud.component'; const taskDetails: TaskDetailsCloudModel = { appName: 'simple-app', @@ -54,265 +53,259 @@ const taskDetails: TaskDetailsCloudModel = { permissions: [TASK_VIEW_PERMISSION] }; -describe('TaskFormCloudComponent', () => { - let loader: HarnessLoader; +describe('UserTaskCloudComponent', () => { + let component: UserTaskCloudComponent; + let fixture: ComponentFixture; let taskCloudService: TaskCloudService; - let identityUserService: IdentityUserService; - let getTaskSpy: jasmine.Spy; let getCurrentUserSpy: jasmine.Spy; - let debugElement: DebugElement; - - let component: TaskFormCloudComponent; - let fixture: ComponentFixture; + let loader: HarnessLoader; + let identityUserService: IdentityUserService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ProcessServiceCloudTestingModule], - declarations: [FormCloudComponent] + imports: [NoopTranslateModule, ProcessServiceCloudTestingModule], + declarations: [UserTaskCloudComponent, TaskFormCloudComponent] }); - taskDetails.status = TASK_ASSIGNED_STATE; - taskDetails.permissions = [TASK_VIEW_PERMISSION]; - taskDetails.standalone = false; - - identityUserService = TestBed.inject(IdentityUserService); - getCurrentUserSpy = spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue({ username: 'admin.adf' }); + fixture = TestBed.createComponent(UserTaskCloudComponent); + component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); taskCloudService = TestBed.inject(TaskCloudService); + identityUserService = TestBed.inject(IdentityUserService); + getTaskSpy = spyOn(taskCloudService, 'getTaskById').and.returnValue(of(taskDetails)); + getCurrentUserSpy = spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue({ username: 'admin.adf' }); spyOn(taskCloudService, 'getCandidateGroups').and.returnValue(of([])); spyOn(taskCloudService, 'getCandidateUsers').and.returnValue(of([])); - - fixture = TestBed.createComponent(TaskFormCloudComponent); - debugElement = fixture.debugElement; - component = fixture.componentInstance; - loader = TestbedHarnessEnvironment.loader(fixture); - }); - - afterEach(() => { - fixture.destroy(); + fixture.detectChanges(); }); describe('Complete button', () => { beforeEach(() => { - component.taskId = 'task1'; - component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); + fixture.componentRef.setInput('showCompleteButton', true); + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); + getTaskSpy.and.returnValue(of({ ...taskDetails })); fixture.detectChanges(); + fixture.whenStable(); }); - it('should show complete button when status is ASSIGNED', () => { - const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]')); - expect(completeBtn.nativeElement).toBeDefined(); - expect(completeBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE'); + it('should show complete button when status is ASSIGNED', async () => { + const completeButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-form-complete' })); + + expect(completeButton).not.toBeNull(); }); - it('should not show complete button when status is ASSIGNED but assigned to a different person', () => { + it('should not show complete button when status is ASSIGNED but assigned to a different person', async () => { getCurrentUserSpy.and.returnValue({}); fixture.detectChanges(); + const completeButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-form-complete' })); - const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]')); - expect(completeBtn).toBeNull(); + expect(completeButton).toBeNull(); }); - it('should not show complete button when showCompleteButton=false', () => { - component.showCompleteButton = false; + it('should not show complete button when showCompleteButton=false', async () => { + fixture.componentRef.setInput('showCompleteButton', false); fixture.detectChanges(); + const completeButton = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-form-complete' })); - const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]')); - expect(completeBtn).toBeNull(); + expect(completeButton).toBeNull(); }); }); describe('Claim/Unclaim buttons', () => { beforeEach(() => { spyOn(component, 'hasCandidateUsers').and.returnValue(true); + component.taskDetails = taskDetails; + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); getTaskSpy.and.returnValue(of(taskDetails)); - component.taskId = 'task1'; - component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); fixture.detectChanges(); }); - it('should not show release button for standalone task', () => { - taskDetails.permissions = [TASK_RELEASE_PERMISSION]; - taskDetails.standalone = true; - getTaskSpy.and.returnValue(of(taskDetails)); + it('should not show release button for standalone task', async () => { + component.taskDetails.permissions = [TASK_RELEASE_PERMISSION]; + component.taskDetails.standalone = true; fixture.detectChanges(); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); expect(unclaimBtn).toBeNull(); }); - it('should not show claim button for standalone task', () => { - taskDetails.status = TASK_CREATED_STATE; - taskDetails.permissions = [TASK_CLAIM_PERMISSION]; - taskDetails.standalone = true; - getTaskSpy.and.returnValue(of(taskDetails)); + it('should not show claim button for standalone task', async () => { + component.taskDetails.status = TASK_CREATED_STATE; + component.taskDetails.permissions = [TASK_CLAIM_PERMISSION]; + component.taskDetails.standalone = true; fixture.detectChanges(); + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); expect(claimBtn).toBeNull(); }); - it('should show release button when task is assigned to one of the candidate users', () => { - taskDetails.permissions = [TASK_RELEASE_PERMISSION]; + it('should show release button when task is assigned to one of the candidate users', async () => { + component.taskDetails = { ...taskDetails, standalone: false, status: TASK_ASSIGNED_STATE, permissions: [TASK_RELEASE_PERMISSION] }; fixture.detectChanges(); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); + expect(unclaimBtn).not.toBeNull(); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); - expect(unclaimBtn.nativeElement).toBeDefined(); - expect(unclaimBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.UNCLAIM'); + const unclaimBtnLabel = await unclaimBtn.getText(); + expect(unclaimBtnLabel).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.UNCLAIM'); }); - it('should not show unclaim button when status is ASSIGNED but assigned to different person', () => { + it('should not show unclaim button when status is ASSIGNED but assigned to different person', async () => { getCurrentUserSpy.and.returnValue({}); fixture.detectChanges(); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); expect(unclaimBtn).toBeNull(); }); - it('should not show unclaim button when status is not ASSIGNED', () => { - taskDetails.status = undefined; + it('should not show unclaim button when status is not ASSIGNED', async () => { + component.taskDetails.status = undefined; fixture.detectChanges(); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); expect(unclaimBtn).toBeNull(); }); - it('should not show unclaim button when status is ASSIGNED and permissions not include RELEASE', () => { - taskDetails.status = TASK_ASSIGNED_STATE; - taskDetails.permissions = [TASK_VIEW_PERMISSION]; + it('should not show unclaim button when status is ASSIGNED and permissions not include RELEASE', async () => { + component.taskDetails.status = TASK_ASSIGNED_STATE; + component.taskDetails.permissions = [TASK_VIEW_PERMISSION]; fixture.detectChanges(); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); expect(unclaimBtn).toBeNull(); }); - it('should show claim button when status is CREATED and permission includes CLAIM', () => { - taskDetails.status = TASK_CREATED_STATE; - taskDetails.permissions = [TASK_CLAIM_PERMISSION]; + it('should show claim button when status is CREATED and permission includes CLAIM', async () => { + component.taskDetails.standalone = false; + component.taskDetails.status = TASK_CREATED_STATE; + component.taskDetails.permissions = [TASK_CLAIM_PERMISSION]; fixture.detectChanges(); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); - expect(claimBtn.nativeElement).toBeDefined(); - expect(claimBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CLAIM'); + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); + expect(claimBtn).not.toBeNull(); + + const claimBtnLabel = await claimBtn.getText(); + expect(claimBtnLabel).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CLAIM'); }); - it('should not show claim button when status is not CREATED', () => { - taskDetails.status = undefined; + it('should not show claim button when status is not CREATED', async () => { + component.taskDetails.status = undefined; fixture.detectChanges(); + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); expect(claimBtn).toBeNull(); }); - it('should not show claim button when status is CREATED and permission not includes CLAIM', () => { - taskDetails.status = TASK_CREATED_STATE; - taskDetails.permissions = [TASK_VIEW_PERMISSION]; + it('should not show claim button when status is CREATED and permission not includes CLAIM', async () => { + component.taskDetails.status = TASK_CREATED_STATE; + component.taskDetails.permissions = [TASK_VIEW_PERMISSION]; fixture.detectChanges(); + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); expect(claimBtn).toBeNull(); }); }); describe('Cancel button', () => { - it('should show cancel button by default', () => { - component.appName = 'app1'; - component.taskId = 'task1'; - + beforeEach(() => { + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); fixture.detectChanges(); - - const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task')); - expect(cancelBtn.nativeElement).toBeDefined(); - expect(cancelBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL'); }); - it('should not show cancel button when showCancelButton=false', () => { - component.appName = 'app1'; - component.taskId = 'task1'; - component.showCancelButton = false; + it('should show cancel button by default', async () => { + const cancelBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + expect(cancelBtn).toBeDefined(); + + const cancelBtnLabel = await cancelBtn.getText(); + expect(cancelBtnLabel).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL'); + }); + it('should not show cancel button when showCancelButton=false', async () => { + fixture.componentRef.setInput('showCancelButton', false); fixture.detectChanges(); + const cancelBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); - const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task')); expect(cancelBtn).toBeNull(); }); }); describe('Inputs', () => { - it('should not show complete/claim/unclaim buttons when readOnly=true', () => { - component.appName = 'app1'; - component.taskId = 'task1'; - component.readOnly = true; - + it('should not show complete/claim/unclaim buttons when readOnly=true', async () => { + getTaskSpy.and.returnValue(of(taskDetails)); + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); + fixture.componentRef.setInput('readOnly', true); + fixture.componentRef.setInput('showCancelButton', true); + component.getTaskType(); fixture.detectChanges(); + await fixture.whenStable(); - const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]')); + const completeBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-complete-task]' })); expect(completeBtn).toBeNull(); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); expect(claimBtn).toBeNull(); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); expect(unclaimBtn).toBeNull(); - const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task')); - expect(cancelBtn.nativeElement).toBeDefined(); - expect(cancelBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL'); + const cancelBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + expect(cancelBtn).toBeDefined(); + + const cancelBtnLabel = await cancelBtn.getText(); + expect(cancelBtnLabel).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL'); }); it('should load data when appName changes', () => { component.taskId = 'task1'; component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); + expect(getTaskSpy).toHaveBeenCalled(); }); it('should load data when taskId changes', () => { component.appName = 'app1'; component.ngOnChanges({ taskId: new SimpleChange(null, 'task1', false) }); - expect(getTaskSpy).toHaveBeenCalled(); - }); - it('should not load data when appName changes and taskId is not defined', () => { - component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); - expect(getTaskSpy).not.toHaveBeenCalled(); + expect(getTaskSpy).toHaveBeenCalled(); }); - it('should not load data when taskId changes and appName is not defined', () => { - component.ngOnChanges({ taskId: new SimpleChange(null, 'task1', false) }); - expect(getTaskSpy).not.toHaveBeenCalled(); - }); + it('should not load data when appName changes and taskId is not defined', async () => { + fixture.componentRef.setInput('taskId', null); + fixture.detectChanges(); - it('should append additional field validators to the default ones when provided', () => { - const mockFirstCustomFieldValidator = new MockFormFieldValidator(); - const mockSecondCustomFieldValidator = new MockFormFieldValidator(); + expect(component.taskId).toBeNull(); - component.fieldValidators = [mockFirstCustomFieldValidator, mockSecondCustomFieldValidator]; - fixture.detectChanges(); + component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); + await fixture.whenStable(); - expect(component.fieldValidators).toEqual([...FORM_FIELD_VALIDATORS, mockFirstCustomFieldValidator, mockSecondCustomFieldValidator]); + expect(getTaskSpy).not.toHaveBeenCalled(); }); - it('should use default field validators when no additional validators are provided', () => { - fixture.detectChanges(); + it('should not load data when taskId changes and appName is not defined', async () => { + component.ngOnChanges({ taskId: new SimpleChange(null, 'task1', false) }); - expect(component.fieldValidators).toEqual([...FORM_FIELD_VALIDATORS]); + expect(getTaskSpy).not.toHaveBeenCalled(); }); }); describe('Events', () => { beforeEach(() => { - component.appName = 'app1'; - component.taskId = 'task1'; + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); + fixture.componentRef.setInput('showCancelButton', true); fixture.detectChanges(); }); it('should emit cancelClick when cancel button is clicked', async () => { spyOn(component.cancelClick, 'emit').and.stub(); - fixture.detectChanges(); - const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task')); - cancelBtn.triggerEventHandler('click', {}); + const cancelBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '#adf-cloud-cancel-task' })); + await cancelBtn.click(); fixture.detectChanges(); await fixture.whenStable(); @@ -320,14 +313,13 @@ describe('TaskFormCloudComponent', () => { }); it('should emit taskCompleted when task is completed', async () => { + component.taskDetails.status = TASK_ASSIGNED_STATE; spyOn(taskCloudService, 'completeTask').and.returnValue(of({})); spyOn(component.taskCompleted, 'emit').and.stub(); - component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); fixture.detectChanges(); - - const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]')); - completeBtn.triggerEventHandler('click', {}); + const completeBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-complete-task]' })); + await completeBtn.click(); fixture.detectChanges(); await fixture.whenStable(); @@ -344,8 +336,9 @@ describe('TaskFormCloudComponent', () => { component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); fixture.detectChanges(); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); - claimBtn.triggerEventHandler('click', {}); + + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); + await claimBtn.click(); fixture.detectChanges(); await fixture.whenStable(); @@ -354,7 +347,6 @@ describe('TaskFormCloudComponent', () => { it('should emit error when error occurs', async () => { spyOn(component.error, 'emit').and.stub(); - component.onError({}); fixture.detectChanges(); await fixture.whenStable(); @@ -365,13 +357,16 @@ describe('TaskFormCloudComponent', () => { it('should reload when task is completed', async () => { spyOn(taskCloudService, 'completeTask').and.returnValue(of({})); const reloadSpy = spyOn(component, 'ngOnChanges').and.callThrough(); + component.taskDetails.status = TASK_ASSIGNED_STATE; component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); fixture.detectChanges(); - const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]')); + await fixture.whenStable(); - completeBtn.nativeElement.click(); + const completeBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-complete-task]' })); + await completeBtn.click(); await fixture.whenStable(); + expect(reloadSpy).toHaveBeenCalled(); }); @@ -385,10 +380,11 @@ describe('TaskFormCloudComponent', () => { component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); fixture.detectChanges(); - const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]')); - claimBtn.nativeElement.click(); + const claimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-claim-task]' })); + await claimBtn.click(); await fixture.whenStable(); + expect(reloadSpy).toHaveBeenCalled(); }); @@ -403,10 +399,10 @@ describe('TaskFormCloudComponent', () => { component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) }); fixture.detectChanges(); - const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]')); - - unclaimBtn.nativeElement.click(); + const unclaimBtn = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[adf-cloud-unclaim-task]' })); + await unclaimBtn.click(); await fixture.whenStable(); + expect(reloadSpy).toHaveBeenCalled(); }); @@ -424,14 +420,6 @@ describe('TaskFormCloudComponent', () => { expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); }); - it('should emit an executeOutcome event when form outcome executed', () => { - const executeOutcomeSpy: jasmine.Spy = spyOn(component.executeOutcome, 'emit'); - - component.onFormExecuteOutcome(new FormOutcomeEvent(new FormOutcomeModel(new FormModel()))); - - expect(executeOutcomeSpy).toHaveBeenCalled(); - }); - it('should emit onTaskLoaded on initial load of component', () => { component.appName = ''; spyOn(component.onTaskLoaded, 'emit'); @@ -440,69 +428,44 @@ describe('TaskFormCloudComponent', () => { fixture.detectChanges(); expect(component.onTaskLoaded.emit).toHaveBeenCalledWith(taskDetails); }); - - it('should emit displayModeOn when display mode is turned on', async () => { - spyOn(component.displayModeOn, 'emit').and.stub(); - - component.onDisplayModeOn(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.displayModeOn.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); - }); - - it('should emit displayModeOff when display mode is turned on', async () => { - spyOn(component.displayModeOff, 'emit').and.stub(); - - component.onDisplayModeOff(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.displayModeOff.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]); - }); }); - it('should display task name as title on no form template if showTitle is true', () => { - component.taskId = taskDetails.id; - + it('should display task name as title on no form template if showTitle is true', async () => { + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); + component.taskDetails = { ...taskDetails }; fixture.detectChanges(); - const noFormTemplateTitle = debugElement.query(By.css('.adf-form-title')); - expect(noFormTemplateTitle.nativeElement.innerText).toEqual('Task1'); + const noFormTemplateTitle = await loader.getHarnessOrNull(MatCardHarness); + const noFormTemplateTitleText = await noFormTemplateTitle.getTitleText(); + + expect(noFormTemplateTitleText).toEqual('Task1'); }); - it('should display default name as title on no form template if the task name empty/undefined', () => { + it('should display default name as title on no form template if the task name empty/undefined', async () => { + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'mock-task-id'); const mockTaskDetailsWithOutName = { id: 'mock-task-id', name: null, formKey: null }; getTaskSpy.and.returnValue(of(mockTaskDetailsWithOutName)); - component.taskId = 'mock-task-id'; fixture.detectChanges(); - const noFormTemplateTitle = debugElement.query(By.css('.adf-form-title')); + const matCard = await loader.getHarnessOrNull(MatCardHarness); + const noFormTemplateTitle = await matCard.getTitleText(); - expect(noFormTemplateTitle.nativeElement.innerText).toEqual('FORM.FORM_RENDERER.NAMELESS_TASK'); + expect(noFormTemplateTitle).toEqual('FORM.FORM_RENDERER.NAMELESS_TASK'); }); - it('should not display no form title if showTitle is set to false', () => { - component.taskId = taskDetails.id; + it('should not display no form title if showTitle is set to false', async () => { + fixture.componentRef.setInput('appName', 'app1'); + fixture.componentRef.setInput('taskId', 'task1'); + fixture.componentRef.setInput('showTitle', false); component.showTitle = false; fixture.detectChanges(); - const noFormTemplateTitle = debugElement.query(By.css('.adf-form-title')); - - expect(noFormTemplateTitle).toBeNull(); - }); - - it('should call children cloud task form change display mode when changing the display mode', () => { - const displayMode = 'displayMode'; - component.taskDetails = { ...taskDetails, formKey: 'some-form' }; - - fixture.detectChanges(); - - expect(component.adfCloudForm).toBeDefined(); - const switchToDisplayModeSpy = spyOn(component.adfCloudForm, 'switchToDisplayMode'); - - component.switchToDisplayMode(displayMode); + const matCard = await loader.getHarnessOrNull(MatCardHarness); + expect(matCard).toBeDefined(); - expect(switchToDisplayModeSpy).toHaveBeenCalledOnceWith(displayMode); + const noFormTemplateTitleText = await matCard.getTitleText(); + expect(noFormTemplateTitleText).toBe(''); }); }); 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 new file mode 100644 index 00000000000..d9f58f14133 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.component.ts @@ -0,0 +1,253 @@ +/*! + * @license + * Copyright © 2005-2024 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 { ContentLinkModel, FormFieldValidator, FormModel, FormOutcomeEvent } from '@alfresco/adf-core'; +import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormCloudDisplayModeConfiguration } from '../../../../services/form-fields.interfaces'; +import { TaskCloudService } from '../../../services/task-cloud.service'; +import { TaskDetailsCloudModel } from '../../../start-task/models/task-details-cloud.model'; +import { TaskFormCloudComponent } from '../task-form-cloud/task-form-cloud.component'; + +const TaskTypes = { + Form: 'form', + Screen: 'screen', + None: '' +} as const; + +type TaskTypesType = (typeof TaskTypes)[keyof typeof TaskTypes]; + +@Component({ + selector: 'adf-cloud-user-task', + templateUrl: './user-task-cloud.component.html', + styleUrls: ['./user-task-cloud.component.scss'] +}) +export class UserTaskCloudComponent implements OnInit, OnChanges { + @ViewChild('adfCloudTaskForm') + adfCloudTaskForm: TaskFormCloudComponent; + + /** App id to fetch corresponding form and values. */ + @Input() + appName: string = ''; + + /** The available display configurations for the form */ + @Input() + displayModeConfigurations: FormCloudDisplayModeConfiguration[]; + + /** FormFieldValidator allow to provide additional validators to the form field. */ + @Input() + fieldValidators: FormFieldValidator[]; + + /** Toggle readonly state of the task. */ + @Input() + readOnly = false; + + /** Toggle rendering of the `Cancel` button. */ + @Input() + showCancelButton = true; + + /** Toggle rendering of the `Complete` button. */ + @Input() + showCompleteButton = true; + + /** Toggle rendering of the form title. */ + @Input() + showTitle: boolean = true; + + /** Toggle rendering of the `Validation` icon. */ + @Input() + showValidationIcon = true; + + /** Task id to fetch corresponding form and values. */ + @Input() + taskId: string; + + /** Emitted when the cancel button is clicked. */ + @Output() + cancelClick = new EventEmitter(); + + /** Emitted when any error occurs. */ + @Output() + error = new EventEmitter(); + + /** + * Emitted when any outcome is executed. Default behaviour can be prevented + * via `event.preventDefault()`. + */ + @Output() + executeOutcome = new EventEmitter(); + + /** Emitted when form content is clicked. */ + @Output() + formContentClicked: EventEmitter = new EventEmitter(); + + /** Emitted when the form is saved. */ + @Output() + formSaved = new EventEmitter(); + + /** + * Emitted when a task is loaded`. + */ + @Output() + onTaskLoaded = new EventEmitter(); /* eslint-disable-line */ + + /** Emitted when the task is claimed. */ + @Output() + taskClaimed = new EventEmitter(); + + /** Emitted when the task is unclaimed. */ + @Output() + taskUnclaimed = new EventEmitter(); + + /** Emitted when the task is completed. */ + @Output() + taskCompleted = new EventEmitter(); + + candidateUsers: string[] = []; + candidateGroups: string[] = []; + loading: boolean = false; + screenId: string; + taskDetails: TaskDetailsCloudModel; + taskType: TaskTypesType; + taskTypeEnum = TaskTypes; + + private taskCloudService: TaskCloudService = inject(TaskCloudService); + private readonly destroyRef = inject(DestroyRef); + + ngOnChanges(changes: SimpleChanges) { + const appName = changes['appName']; + if (appName && appName.currentValue !== appName.previousValue && this.taskId) { + this.loadTask(); + return; + } + + const taskId = changes['taskId']; + if (taskId?.currentValue && this.appName) { + this.loadTask(); + return; + } + } + + ngOnInit() { + if (this.appName === '' && this.taskId) { + this.loadTask(); + } + } + + canClaimTask(): boolean { + return !this.readOnly && this.taskCloudService.canClaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); + } + + canCompleteTask(): boolean { + return this.showCompleteButton && !this.readOnly && this.taskCloudService.canCompleteTask(this.taskDetails); + } + + canUnclaimTask(): boolean { + return !this.readOnly && this.taskCloudService.canUnclaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); + } + + getTaskType(): void { + if (this.taskDetails && !!this.taskDetails.formKey && this.taskDetails.formKey.includes(this.taskTypeEnum.Form)) { + this.taskType = this.taskTypeEnum.Form; + } else if (this.taskDetails && !!this.taskDetails.formKey && this.taskDetails.formKey.includes(this.taskTypeEnum.Screen)) { + this.taskType = this.taskTypeEnum.Screen; + const screenId = this.taskDetails.formKey.replace(this.taskTypeEnum.Screen + '-', ''); + this.screenId = screenId; + } else { + this.taskType = this.taskTypeEnum.None; + } + } + + hasCandidateUsers(): boolean { + return this.candidateUsers.length !== 0; + } + + hasCandidateGroups(): boolean { + return this.candidateGroups.length !== 0; + } + + hasCandidateUsersOrGroups(): boolean { + return this.hasCandidateUsers() || this.hasCandidateGroups(); + } + + onCancelForm(): void { + this.cancelClick.emit(); + } + + onCancelClick(): void { + this.cancelClick.emit(this.taskId); + } + + onClaimTask(): void { + this.loadTask(); + this.taskClaimed.emit(this.taskId); + } + + onCompleteTask(): void { + this.loadTask(); + this.taskCompleted.emit(this.taskId); + } + + onCompleteTaskForm(): void { + this.taskCompleted.emit(); + } + + onError(data: any): void { + this.error.emit(data); + } + + onExecuteOutcome(outcome: FormOutcomeEvent): void { + this.executeOutcome.emit(outcome); + } + onFormContentClicked(content: ContentLinkModel): void { + this.formContentClicked.emit(content); + } + onFormSaved(): void { + this.formSaved.emit(); + } + + onTaskUnclaimed(): void { + this.taskUnclaimed.emit(); + } + + onUnclaimTask(): void { + this.loadTask(); + this.taskUnclaimed.emit(this.taskId); + } + + private loadTask(): void { + this.loading = true; + this.taskCloudService + .getTaskById(this.appName, this.taskId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((details) => { + this.taskDetails = details; + this.getTaskType(); + this.loading = false; + this.onTaskLoaded.emit(this.taskDetails); + }); + + this.taskCloudService.getCandidateUsers(this.appName, this.taskId).subscribe((users) => (this.candidateUsers = users || [])); + this.taskCloudService.getCandidateGroups(this.appName, this.taskId).subscribe((groups) => (this.candidateGroups = groups || [])); + } + + public switchToDisplayMode(newDisplayMode?: string): void { + if (this.adfCloudTaskForm) { + this.adfCloudTaskForm.switchToDisplayMode(newDisplayMode); + } + } +} diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.interface.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.interface.ts new file mode 100644 index 00000000000..c76cef0dba5 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/user-task-cloud.interface.ts @@ -0,0 +1,29 @@ +/*! + * @license + * Copyright © 2005-2024 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 { EventEmitter } from '@angular/core'; + +export interface UserTaskCustomUi { + appName: string; + taskId: string; + screenId: string; + error: EventEmitter; + cancelClick: EventEmitter; + taskClaimed: EventEmitter; + taskUnclaimed: EventEmitter; + taskCompleted: EventEmitter; +} diff --git a/lib/process-services-cloud/src/lib/task/task-form/public-api.ts b/lib/process-services-cloud/src/lib/task/task-form/public-api.ts index b75729a7716..ed10ea7e25d 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/public-api.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/public-api.ts @@ -15,6 +15,7 @@ * limitations under the License. */ -export * from './components/task-form-cloud.component'; +export * from './components/task-form-cloud/task-form-cloud.component'; +export * from './components/user-task-cloud/user-task-cloud.component'; export * from './task-form.module'; diff --git a/lib/process-services-cloud/src/lib/task/task-form/task-form.module.ts b/lib/process-services-cloud/src/lib/task/task-form/task-form.module.ts index 4a23edf8199..cb3fca0743c 100644 --- a/lib/process-services-cloud/src/lib/task/task-form/task-form.module.ts +++ b/lib/process-services-cloud/src/lib/task/task-form/task-form.module.ts @@ -20,23 +20,15 @@ import { CommonModule } from '@angular/common'; import { MaterialModule } from '../../material.module'; import { FormCloudModule } from '../../form/form-cloud.module'; import { TaskDirectiveModule } from '../directives/task-directive.module'; - -import { TaskFormCloudComponent } from './components/task-form-cloud.component'; +import { TaskFormCloudComponent } from './components/task-form-cloud/task-form-cloud.component'; import { CoreModule } from '@alfresco/adf-core'; +import { TaskScreenCloudComponent } from '../../screen/components/screen-cloud/screen-cloud.component'; +import { UserTaskCloudComponent } from './components/user-task-cloud/user-task-cloud.component'; +import { UserTaskCloudButtonsComponent } from './components/user-task-cloud-buttons/user-task-cloud-buttons.component'; @NgModule({ - imports: [ - CoreModule, - CommonModule, - MaterialModule, - FormCloudModule, - TaskDirectiveModule - ], - declarations: [ - TaskFormCloudComponent - ], - exports: [ - TaskFormCloudComponent - ] + imports: [CoreModule, CommonModule, MaterialModule, FormCloudModule, TaskDirectiveModule, TaskScreenCloudComponent], + declarations: [TaskFormCloudComponent, UserTaskCloudComponent, UserTaskCloudButtonsComponent], + exports: [TaskFormCloudComponent, UserTaskCloudComponent] }) -export class TaskFormModule { } +export class TaskFormModule {}