From 23f0392c124a06462018c4492bf6a453390a85e3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:48:09 +0000 Subject: [PATCH] AAE-47634 Refactor: switch form service getTask to use runtime bundle API (#12020) * refactor: switch form service getTask to use runtime bundle API instead of query service * Update form-cloud.service.ts * Add TaskDetailsCloudModelRuntimeBundle interface * Update getTaskById to return TaskDetailsCloudModelRuntimeBundle * Modify return type of getTaskById method Updated return type of getTaskById to include TaskDetailsCloudModel. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Refactor TaskDetailsCloudModel by removing fields Removed unused properties from TaskDetailsCloudModel. * Update task-cloud.service.ts * Update task-cloud.service.ts * fix: add 404 fallback to query service in FormCloudService.getTask for completed tasks * AAE-47634 getTaskById: call Runtime Bundle first, fall back to Query Service on 404 * AAE-47634 Gate Runtime Bundle task fallback behind ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED token and drop no longer used taskDetailsSource input * refactor: update task fetching tests to use async/await and firstValueFrom --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Eugenio Romano Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: alep85 --- .../form/services/form-cloud.service.spec.ts | 55 ++++- .../lib/form/services/form-cloud.service.ts | 25 ++- .../src/lib/services/public-api.ts | 1 + .../task-runtime-bundle-fallback.token.ts | 40 ++++ .../task/models/task-details-cloud.model.ts | 18 ++ .../task/services/task-cloud.service.spec.ts | 194 +++++++++++++++++- .../lib/task/services/task-cloud.service.ts | 28 ++- ...query-task-details-source.strategy.spec.ts | 61 ------ .../query-task-details-source.strategy.ts | 43 ---- ...undle-task-details-source.strategy.spec.ts | 107 ---------- ...ime-bundle-task-details-source.strategy.ts | 55 ----- .../task-details-source.strategy.ts | 34 --- .../user-task-cloud.component.spec.ts | 88 +------- .../user-task-cloud.component.ts | 35 +--- 14 files changed, 344 insertions(+), 440 deletions(-) create mode 100644 lib/process-services-cloud/src/lib/services/task-runtime-bundle-fallback.token.ts delete mode 100644 lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.spec.ts delete mode 100644 lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.ts delete mode 100644 lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.spec.ts delete mode 100644 lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.ts delete mode 100644 lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/task-details-source.strategy.ts diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts index f950c6eac8..5497653141 100644 --- a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts @@ -17,9 +17,11 @@ import { TestBed } from '@angular/core/testing'; import { FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN, FormCloudService } from './form-cloud.service'; -import { of } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, of } from 'rxjs'; import { AdfHttpClient } from '@alfresco/adf-core/api'; import { FORM_FIELD_VALIDATORS, FormFieldValidator, NoopAuthModule } from '@alfresco/adf-core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED } from '../../services/task-runtime-bundle-fallback.token'; const mockTaskResponseBody = { entry: { id: 'id', name: 'name', formKey: 'form-key' } @@ -37,14 +39,19 @@ describe('Form Cloud service', () => { let service: FormCloudService; let adfHttpClient: AdfHttpClient; let requestSpy: jasmine.Spy; + let runtimeBundleFallback$: BehaviorSubject; const appName = 'app-name'; const taskId = 'task-id'; const processInstanceId = 'process-instance-id'; beforeEach(() => { + runtimeBundleFallback$ = new BehaviorSubject(true); TestBed.configureTestingModule({ imports: [NoopAuthModule], - providers: [{ provide: FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }] + providers: [ + { provide: FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }, + { provide: ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED, useValue: runtimeBundleFallback$ } + ] }); service = TestBed.inject(FormCloudService); adfHttpClient = TestBed.inject(AdfHttpClient); @@ -86,17 +93,45 @@ describe('Form Cloud service', () => { }); describe('Task tests', () => { - it('should fetch and parse task', (done) => { + it('should fetch task from runtime bundle', async () => { requestSpy.and.returnValue(Promise.resolve(mockTaskResponseBody)); - service.getTask(appName, taskId).subscribe((result) => { - expect(result).toBeDefined(); - expect(result.id).toBe('id'); - expect(result.name).toBe('name'); - expect(requestSpy.calls.mostRecent().args[0]).toContain(`${appName}/query/v1/tasks/${taskId}`); - expect(requestSpy.calls.mostRecent().args[1].httpMethod).toBe('GET'); - done(); + const result = await firstValueFrom(service.getTask(appName, taskId)); + + expect(result).toBeDefined(); + expect(result.id).toBe('id'); + expect(result.name).toBe('name'); + expect(requestSpy.calls.first().args[0]).toContain(`${appName}/rb/v1/tasks/${taskId}`); + expect(requestSpy.calls.first().args[1].httpMethod).toBe('GET'); + }); + + it('should fall back to query service when runtime bundle returns 404', async () => { + const notFoundError = new HttpErrorResponse({ status: 404 }); + requestSpy.and.callFake((url: string) => { + if (url.includes('/rb/')) { + return Promise.reject(notFoundError); + } + return Promise.resolve(mockTaskResponseBody); }); + + const result = await firstValueFrom(service.getTask(appName, taskId)); + + expect(result).toBeDefined(); + expect(result.id).toBe('id'); + expect(requestSpy.calls.first().args[0]).toContain(`${appName}/rb/v1/tasks/${taskId}`); + expect(requestSpy.calls.mostRecent().args[0]).toContain(`${appName}/query/v1/tasks/${taskId}`); + }); + + it('should use the query service only when the runtime bundle fallback is disabled', async () => { + runtimeBundleFallback$.next(false); + requestSpy.and.returnValue(Promise.resolve(mockTaskResponseBody)); + + const result = await firstValueFrom(service.getTask(appName, taskId)); + + expect(result).toBeDefined(); + expect(result.id).toBe('id'); + expect(requestSpy.calls.first().args[0]).toContain(`${appName}/query/v1/tasks/${taskId}`); + expect(requestSpy.calls.all().some((call) => call.args[0].includes('/rb/'))).toBe(false); }); it('should fetch task variables', (done) => { diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts index 6d607471b6..f04bf8926f 100644 --- a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts @@ -17,14 +17,15 @@ import { inject, Injectable, InjectionToken } from '@angular/core'; import { FormValues, FormModel, FormFieldOption, FormFieldValidator, FormService } from '@alfresco/adf-core'; -import { Observable, from, EMPTY } from 'rxjs'; -import { expand, map, reduce, switchMap } from 'rxjs/operators'; +import { Observable, from, EMPTY, throwError } from 'rxjs'; +import { catchError, expand, map, reduce, switchMap, take } from 'rxjs/operators'; import { TaskDetailsCloudModel } from '../../task/models/task-details-cloud.model'; import { CompleteFormRepresentation, LazyApi, UploadApi } from '@alfresco/js-api'; import { TaskVariableCloud } from '../models/task-variable-cloud.model'; import { BaseCloudService } from '../../services/base-cloud.service'; import { FormContent } from '../../services/form-fields.interfaces'; import { FormCloudServiceInterface } from './form-cloud.service.interface'; +import { ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED, resolveTaskRuntimeBundleFallback$ } from '../../services/task-runtime-bundle-fallback.token'; export const FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN = new InjectionToken('FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN'); @@ -34,6 +35,9 @@ export const FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN = new InjectionToken
new UploadApi(self.apiService.getInstance())) declare readonly uploadApi: UploadApi; @@ -132,16 +136,27 @@ export class FormCloudService extends BaseCloudService implements FormCloudServi } /** - * Gets details of a task + * Gets details of a task. Tries the Runtime Bundle first (always up to date for + * active tasks) and transparently falls back to the Query Service on 404 so that + * completed/archived tasks can still be loaded. * * @param appName Name of the app * @param taskId ID of the target task * @returns Details of the task */ getTask(appName: string, taskId: string): Observable { - const apiUrl = `${this.getBasePath(appName)}/query/v1/tasks/${taskId}`; + const rbUrl = `${this.getBasePath(appName)}/rb/v1/tasks/${taskId}`; + const queryUrl = `${this.getBasePath(appName)}/query/v1/tasks/${taskId}`; - return this.get(apiUrl).pipe(map((res: any) => res.entry)); + return this.runtimeBundleFallbackEnabled$.pipe( + take(1), + switchMap((runtimeBundleFirst) => + runtimeBundleFirst + ? this.get(rbUrl).pipe(catchError((error) => (error?.status === 404 ? this.get(queryUrl) : throwError(() => error)))) + : this.get(queryUrl) + ), + map((res: any) => res.entry) + ); } /** 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 8c819b32e1..79f3d17776 100644 --- a/lib/process-services-cloud/src/lib/services/public-api.ts +++ b/lib/process-services-cloud/src/lib/services/public-api.ts @@ -22,6 +22,7 @@ export * from './local-preference-cloud.service'; export * from './notification-cloud.service'; export * from './preference-cloud.interface'; export * from './task-list-cloud.service.interface'; +export * from './task-runtime-bundle-fallback.token'; 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/task-runtime-bundle-fallback.token.ts b/lib/process-services-cloud/src/lib/services/task-runtime-bundle-fallback.token.ts new file mode 100644 index 0000000000..f6f7193040 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/task-runtime-bundle-fallback.token.ts @@ -0,0 +1,40 @@ +/*! + * @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 { InjectionToken } from '@angular/core'; +import { isObservable, Observable, of } from 'rxjs'; + +/** + * Controls whether task details are read from the Runtime Bundle first, falling back to the + * Query Service on a 404. The Runtime Bundle is always up to date, whereas the Query Service is + * eventually consistent. + * + * When it resolves to a falsy value (the default when not provided), task details are read from + * the Query Service only, preserving the historical behavior for every consumer that does not + * override it. Host applications can wire it to a feature flag, e.g. an `Observable`. + */ +export const ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED = new InjectionToken | boolean>('ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED'); + +/** + * Normalizes the injected {@link ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED} value to an + * `Observable`, defaulting to `false` when it is not provided. + * + * @param token the injected token value (observable, boolean, or null when not provided) + * @returns an observable emitting whether the Runtime Bundle fallback is enabled + */ +export const resolveTaskRuntimeBundleFallback$ = (token: Observable | boolean | null): Observable => + isObservable(token) ? token : of(token ?? false); diff --git a/lib/process-services-cloud/src/lib/task/models/task-details-cloud.model.ts b/lib/process-services-cloud/src/lib/task/models/task-details-cloud.model.ts index e86190b9e9..e0a55c9bd8 100644 --- a/lib/process-services-cloud/src/lib/task/models/task-details-cloud.model.ts +++ b/lib/process-services-cloud/src/lib/task/models/task-details-cloud.model.ts @@ -48,6 +48,24 @@ export interface TaskDetailsCloudModel { processDefinitionDeploymentId?: string; } +export interface TaskDetailsCloudModelRuntimeBundle { + id?: string; + name?: string; + appName?: string; + assignee?: string; + appVersion?: number; + createdDate?: Date; + claimedDate?: Date; + formKey?: any; + priority?: number; + processDefinitionId?: string; + processInstanceId?: string; + status?: TaskStatus; + standalone?: boolean; + candidateUsers?: string[]; + candidateGroups?: string[]; +} + export interface StartTaskCloudResponseModel { entry: TaskDetailsCloudModel; } diff --git a/lib/process-services-cloud/src/lib/task/services/task-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/task/services/task-cloud.service.spec.ts index 444907140b..de7662cc0b 100644 --- a/lib/process-services-cloud/src/lib/task/services/task-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/task/services/task-cloud.service.spec.ts @@ -17,7 +17,9 @@ import { TestBed } from '@angular/core/testing'; import { AppConfigService, TranslationService, NoopTranslateModule, NoopAuthModule } from '@alfresco/adf-core'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { TaskCloudService } from './task-cloud.service'; +import { ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED } from '../../services/task-runtime-bundle-fallback.token'; import { taskCompleteCloudMock } from '../task-header/mocks/fake-complete-task.mock'; import { assignedTaskDetailsCloudMock, @@ -26,7 +28,14 @@ import { } from '../task-header/mocks/task-details-cloud.mock'; import { IdentityUserService } from '../../people/services/identity-user.service'; import { AdfHttpClient } from '@alfresco/adf-core/api'; -import { TASK_COMPLETED_STATE, TASK_CREATED_STATE, TASK_ASSIGNED_STATE } from '../models/task-details-cloud.model'; +import { + TASK_COMPLETED_STATE, + TASK_CREATED_STATE, + TASK_ASSIGNED_STATE, + TASK_CLAIM_PERMISSION, + TASK_RELEASE_PERMISSION, + TASK_VIEW_PERMISSION +} from '../models/task-details-cloud.model'; const fakeTaskDetailsCloud = { entry: { @@ -69,6 +78,7 @@ describe('Task Cloud Service', () => { let translateService: TranslationService; let appConfigService: AppConfigService; let requestSpy: jasmine.Spy; + let runtimeBundleFallback$: BehaviorSubject; const returnFakeTaskCompleteResults = () => Promise.resolve(taskCompleteCloudMock); @@ -81,8 +91,10 @@ describe('Task Cloud Service', () => { const returnFakeCandidateGroupResults = () => Promise.resolve(['mockgroup1', 'mockgroup2', 'mockgroup3']); beforeEach(() => { + runtimeBundleFallback$ = new BehaviorSubject(false); TestBed.configureTestingModule({ - imports: [NoopTranslateModule, NoopAuthModule] + imports: [NoopTranslateModule, NoopAuthModule], + providers: [{ provide: ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED, useValue: runtimeBundleFallback$ }] }); adfHttpClient = TestBed.inject(AdfHttpClient); identityUserService = TestBed.inject(IdentityUserService); @@ -579,6 +591,134 @@ describe('Task Cloud Service', () => { }); }); + describe('canClaimTask', () => { + describe('when the task exposes permissions', () => { + it('should allow claiming when the task is created and has the CLAIM permission', () => { + const task = { + ...createdTaskDetailsCloudMock, + status: TASK_CREATED_STATE, + standalone: false, + permissions: [TASK_CLAIM_PERMISSION] + }; + expect(service.canClaimTask(task)).toBe(true); + }); + + it('should not allow claiming when the task is standalone', () => { + const task = { + ...createdTaskDetailsCloudMock, + status: TASK_CREATED_STATE, + standalone: true, + permissions: [TASK_CLAIM_PERMISSION] + }; + expect(service.canClaimTask(task)).toBe(false); + }); + + it('should not allow claiming when the task is not created', () => { + const task = { + ...createdTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + standalone: false, + permissions: [TASK_CLAIM_PERMISSION] + }; + expect(service.canClaimTask(task)).toBe(false); + }); + + it('should not allow claiming when the CLAIM permission is missing', () => { + const task = { + ...createdTaskDetailsCloudMock, + status: TASK_CREATED_STATE, + standalone: false, + permissions: [TASK_VIEW_PERMISSION] + }; + expect(service.canClaimTask(task)).toBe(false); + }); + }); + + describe('when the task does not expose permissions', () => { + it('should allow claiming a created, non-standalone task when permissions are missing', () => { + const task = { ...createdTaskDetailsCloudMock, status: TASK_CREATED_STATE, standalone: false, permissions: undefined }; + expect(service.canClaimTask(task)).toBe(true); + }); + + it('should not allow claiming a standalone task when permissions are empty', () => { + const task = { ...createdTaskDetailsCloudMock, status: TASK_CREATED_STATE, standalone: true, permissions: [] }; + expect(service.canClaimTask(task)).toBe(false); + }); + }); + }); + + describe('canUnclaimTask', () => { + describe('when the task exposes permissions', () => { + it('should allow releasing when the task is assigned to me and has the RELEASE permission', () => { + const task = { + ...assignedTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + assignee: cloudMockUser.username, + standalone: false, + permissions: [TASK_RELEASE_PERMISSION] + }; + expect(service.canUnclaimTask(task)).toBe(true); + }); + + it('should not allow releasing when the task is standalone', () => { + const task = { + ...assignedTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + assignee: cloudMockUser.username, + standalone: true, + permissions: [TASK_RELEASE_PERMISSION] + }; + expect(service.canUnclaimTask(task)).toBe(false); + }); + + it('should not allow releasing when the task is assigned to someone else', () => { + const task = { + ...assignedTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + assignee: 'DifferentUser', + standalone: false, + permissions: [TASK_RELEASE_PERMISSION] + }; + expect(service.canUnclaimTask(task)).toBe(false); + }); + + it('should not allow releasing when the RELEASE permission is missing', () => { + const task = { + ...assignedTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + assignee: cloudMockUser.username, + standalone: false, + permissions: [TASK_VIEW_PERMISSION] + }; + expect(service.canUnclaimTask(task)).toBe(false); + }); + }); + + describe('when the task does not expose permissions', () => { + it('should allow releasing a task assigned to me when permissions are missing', () => { + const task = { + ...assignedTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + assignee: cloudMockUser.username, + standalone: false, + permissions: undefined + }; + expect(service.canUnclaimTask(task)).toBe(true); + }); + + it('should not allow releasing a task assigned to someone else when permissions are empty', () => { + const task = { + ...assignedTaskDetailsCloudMock, + status: TASK_ASSIGNED_STATE, + assignee: 'DifferentUser', + standalone: false, + permissions: [] + }; + expect(service.canUnclaimTask(task)).toBe(false); + }); + }); + }); + describe('canClaimTaskByState', () => { it('should return true for a created, non-standalone task without relying on permissions', () => { const task = { ...createdTaskDetailsCloudMock, status: TASK_CREATED_STATE, standalone: false, permissions: undefined }; @@ -623,4 +763,54 @@ describe('Task Cloud Service', () => { expect(service.canUnclaimTaskByState(task)).toBe(false); }); }); + + describe('getTaskById fallback', () => { + const appName = 'task-app'; + const taskId = '12345678'; + + it('should use the Query Service only when the Runtime Bundle fallback is disabled', async () => { + runtimeBundleFallback$.next(false); + requestSpy.and.callFake(returnFakeTaskDetailsResults); + + await firstValueFrom(service.getTaskById(appName, taskId)); + + const requestedUrls = requestSpy.calls.all().map((call) => call.args[0]); + expect(requestedUrls[0]).toContain(`/query/v1/tasks/${taskId}`); + expect(requestedUrls.some((url) => url.includes('/rb/'))).toBe(false); + }); + + it('should call the Runtime Bundle first when the fallback is enabled', async () => { + runtimeBundleFallback$.next(true); + requestSpy.and.callFake(returnFakeTaskDetailsResults); + + await firstValueFrom(service.getTaskById(appName, taskId)); + + const [url] = requestSpy.calls.first().args; + expect(url).toContain(`/rb/v1/tasks/${taskId}`); + }); + + it('should use the Query Service when the Runtime Bundle returns 404', async () => { + runtimeBundleFallback$.next(true); + const notFoundError = Object.assign(new Error('Not Found'), { status: 404 }); + requestSpy.and.callFake((url: string) => (url.includes('/rb/') ? Promise.reject(notFoundError) : Promise.resolve(fakeTaskDetailsCloud))); + + const task: any = await firstValueFrom(service.getTaskById(appName, taskId)); + + const requestedUrls = requestSpy.calls.all().map((call) => call.args[0]); + expect(requestedUrls[0]).toContain(`/rb/v1/tasks/${taskId}`); + expect(requestedUrls[1]).toContain(`/query/v1/tasks/${taskId}`); + expect(task.id).toBe(fakeTaskDetailsCloud.entry.id); + }); + + it('should not use the Query Service for errors other than 404', async () => { + runtimeBundleFallback$.next(true); + const error = Object.assign(new Error('Server Error'), { status: 500 }); + requestSpy.and.callFake((url: string) => (url.includes('/rb/') ? Promise.reject(error) : Promise.resolve(fakeTaskDetailsCloud))); + + await expectAsync(firstValueFrom(service.getTaskById(appName, taskId))).toBeRejectedWith(error); + + const requestedUrls = requestSpy.calls.all().map((call) => call.args[0]); + expect(requestedUrls.some((url) => url.includes('/query/'))).toBe(false); + }); + }); }); diff --git a/lib/process-services-cloud/src/lib/task/services/task-cloud.service.ts b/lib/process-services-cloud/src/lib/task/services/task-cloud.service.ts index b9d0330e3e..7c927d5730 100644 --- a/lib/process-services-cloud/src/lib/task/services/task-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/services/task-cloud.service.ts @@ -18,7 +18,7 @@ import { inject, Injectable } from '@angular/core'; import { CardViewArrayItem, TranslationService } from '@alfresco/adf-core'; import { Observable, of, Subject, throwError } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { catchError, map, switchMap, take } from 'rxjs/operators'; import { StartTaskCloudResponseModel, TASK_ASSIGNED_STATE, @@ -33,6 +33,7 @@ import { StartTaskCloudRequestModel } from '../models/start-task-cloud-request.m import { ProcessDefinitionCloud } from '../../models/process-definition-cloud.model'; import { DEFAULT_TASK_PRIORITIES, TaskPriorityOption } from '../models/task.model'; import { IdentityUserService } from '../../people/services/identity-user.service'; +import { ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED, resolveTaskRuntimeBundleFallback$ } from '../../services/task-runtime-bundle-fallback.token'; @Injectable({ providedIn: 'root' @@ -40,6 +41,9 @@ import { IdentityUserService } from '../../people/services/identity-user.service export class TaskCloudService extends BaseCloudService { private readonly translateService = inject(TranslationService); private readonly identityUserService = inject(IdentityUserService); + private readonly runtimeBundleFallbackEnabled$ = resolveTaskRuntimeBundleFallback$( + inject(ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED, { optional: true }) + ); dataChangesDetected$ = new Subject(); @@ -111,6 +115,9 @@ export class TaskCloudService extends BaseCloudService { * @returns Boolean value if the task can be completed */ canClaimTask(taskDetails: TaskDetailsCloudModel): boolean { + if (!taskDetails?.permissions || taskDetails?.permissions?.length === 0) { + return this.canClaimTaskByState(taskDetails); + } return taskDetails?.status === TASK_CREATED_STATE && taskDetails?.permissions.includes(TASK_CLAIM_PERMISSION) && !taskDetails?.standalone; } @@ -122,6 +129,9 @@ export class TaskCloudService extends BaseCloudService { */ canUnclaimTask(taskDetails: TaskDetailsCloudModel): boolean { const currentUser = this.identityUserService.getCurrentUserInfo().username; + if (!taskDetails?.permissions || taskDetails?.permissions?.length === 0) { + return this.canUnclaimTaskByState(taskDetails); + } return ( taskDetails?.status === TASK_ASSIGNED_STATE && taskDetails?.assignee === currentUser && @@ -226,14 +236,22 @@ export class TaskCloudService extends BaseCloudService { * * @param appName Name of the app * @param taskId ID of the task whose details you want - * @param service The service to call. Either Query Service or Runtime Bundle Service. * @returns Task details */ - getTaskById(appName: string, taskId: string, service: 'query' | 'rb' = 'query'): Observable { + getTaskById(appName: string, taskId: string): Observable { if ((appName || appName === '') && taskId) { - const queryUrl = `${this.getBasePath(appName)}/${service}/v1/tasks/${taskId}`; + const queryUrl = `${this.getBasePath(appName)}/query/v1/tasks/${taskId}`; + const rbUrl = `${this.getBasePath(appName)}/rb/v1/tasks/${taskId}`; - return this.get(queryUrl).pipe(map((res: any) => res.entry)); + return this.runtimeBundleFallbackEnabled$.pipe( + take(1), + switchMap((runtimeBundleFirst) => + runtimeBundleFirst + ? this.get(rbUrl).pipe(catchError((error) => (error?.status === 404 ? this.get(queryUrl) : throwError(() => error)))) + : this.get(queryUrl) + ), + map((res: any) => res.entry) + ); } else { return throwError('AppName/TaskId not configured'); } diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.spec.ts deleted file mode 100644 index a18defa795..0000000000 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*! - * @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 { of } from 'rxjs'; -import { TaskCloudService } from '../../../../services/task-cloud.service'; -import { TaskDetailsCloudModel } from '../../../../models/task-details-cloud.model'; -import { QueryTaskDetailsSourceStrategy } from './query-task-details-source.strategy'; - -describe('QueryTaskDetailsSourceStrategy', () => { - let strategy: QueryTaskDetailsSourceStrategy; - let taskCloudService: jasmine.SpyObj; - - const taskDetails = { id: 'task-1' } as TaskDetailsCloudModel; - - beforeEach(() => { - taskCloudService = jasmine.createSpyObj('TaskCloudService', ['getTaskById', 'canClaimTask', 'canUnclaimTask']); - - TestBed.configureTestingModule({ - providers: [QueryTaskDetailsSourceStrategy, { provide: TaskCloudService, useValue: taskCloudService }] - }); - - strategy = TestBed.inject(QueryTaskDetailsSourceStrategy); - }); - - it('should fetch the task details from the Query Service', () => { - taskCloudService.getTaskById.and.returnValue(of(taskDetails)); - - strategy.getTaskDetails$('app', 'task-1').subscribe(); - - expect(taskCloudService.getTaskById).toHaveBeenCalledWith('app', 'task-1'); - }); - - it('should delegate claim eligibility to canClaimTask', () => { - taskCloudService.canClaimTask.and.returnValue(true); - - expect(strategy.canClaim(taskDetails)).toBe(true); - expect(taskCloudService.canClaimTask).toHaveBeenCalledWith(taskDetails); - }); - - it('should delegate unclaim eligibility to canUnclaimTask', () => { - taskCloudService.canUnclaimTask.and.returnValue(true); - - expect(strategy.canUnclaim(taskDetails)).toBe(true); - expect(taskCloudService.canUnclaimTask).toHaveBeenCalledWith(taskDetails); - }); -}); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.ts deleted file mode 100644 index a8f46ac42b..0000000000 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/query-task-details-source.strategy.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * @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 { inject, Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { TaskCloudService } from '../../../../services/task-cloud.service'; -import { TaskDetailsCloudModel } from '../../../../models/task-details-cloud.model'; -import { TaskDetailsSourceStrategy } from './task-details-source.strategy'; - -/** - * Reads task details from the Query Service and evaluates claim/unclaim eligibility from - * the candidate `permissions` it returns. - */ -@Injectable({ providedIn: 'root' }) -export class QueryTaskDetailsSourceStrategy implements TaskDetailsSourceStrategy { - private readonly taskCloudService = inject(TaskCloudService); - - getTaskDetails$(appName: string, taskId: string): Observable { - return this.taskCloudService.getTaskById(appName, taskId); - } - - canClaim(taskDetails: TaskDetailsCloudModel): boolean { - return this.taskCloudService.canClaimTask(taskDetails); - } - - canUnclaim(taskDetails: TaskDetailsCloudModel): boolean { - return this.taskCloudService.canUnclaimTask(taskDetails); - } -} diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.spec.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.spec.ts deleted file mode 100644 index 715895e5ee..0000000000 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -/*! - * @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 { of, throwError } from 'rxjs'; -import { TaskCloudService } from '../../../../services/task-cloud.service'; -import { TaskDetailsCloudModel } from '../../../../models/task-details-cloud.model'; -import { RuntimeBundleTaskDetailsSourceStrategy } from './runtime-bundle-task-details-source.strategy'; - -describe('RuntimeBundleTaskDetailsSourceStrategy', () => { - let strategy: RuntimeBundleTaskDetailsSourceStrategy; - let taskCloudService: jasmine.SpyObj; - - const taskDetails = { id: 'task-1' } as TaskDetailsCloudModel; - - beforeEach(() => { - taskCloudService = jasmine.createSpyObj('TaskCloudService', [ - 'getTaskById', - 'canClaimTask', - 'canClaimTaskByState', - 'canUnclaimTask', - 'canUnclaimTaskByState' - ]); - - TestBed.configureTestingModule({ - providers: [RuntimeBundleTaskDetailsSourceStrategy, { provide: TaskCloudService, useValue: taskCloudService }] - }); - - strategy = TestBed.inject(RuntimeBundleTaskDetailsSourceStrategy); - }); - - it('should fetch the task details from the Runtime Bundle', () => { - taskCloudService.getTaskById.and.returnValue(of(taskDetails)); - - strategy.getTaskDetails$('app', 'task-1').subscribe(); - - expect(taskCloudService.getTaskById).toHaveBeenCalledWith('app', 'task-1', 'rb'); - }); - - it('should fall back to the Query Service when the Runtime Bundle returns 404 for a terminal task', () => { - taskCloudService.getTaskById.withArgs('app', 'task-1', 'rb').and.returnValue(throwError(() => ({ status: 404 }))); - taskCloudService.getTaskById.withArgs('app', 'task-1', 'query').and.returnValue(of(taskDetails)); - - let result: TaskDetailsCloudModel; - strategy.getTaskDetails$('app', 'task-1').subscribe((task) => (result = task)); - - expect(taskCloudService.getTaskById).toHaveBeenCalledWith('app', 'task-1', 'query'); - expect(result).toEqual(taskDetails); - }); - - it('should propagate non-404 errors from the Runtime Bundle without falling back', () => { - const error = { status: 500 }; - taskCloudService.getTaskById.withArgs('app', 'task-1', 'rb').and.returnValue(throwError(() => error)); - - let caught: unknown; - strategy.getTaskDetails$('app', 'task-1').subscribe({ error: (err) => (caught = err) }); - - expect(caught).toBe(error); - expect(taskCloudService.getTaskById).not.toHaveBeenCalledWith('app', 'task-1', 'query'); - }); - - it('should delegate claim eligibility to the state-based check when permissions are absent', () => { - taskCloudService.canClaimTaskByState.and.returnValue(true); - - expect(strategy.canClaim({ ...taskDetails, permissions: undefined })).toBe(true); - expect(taskCloudService.canClaimTaskByState).toHaveBeenCalled(); - expect(taskCloudService.canClaimTask).not.toHaveBeenCalled(); - }); - - it('should delegate claim eligibility to the permission-based check when permissions are present', () => { - taskCloudService.canClaimTask.and.returnValue(true); - - expect(strategy.canClaim({ ...taskDetails, permissions: ['CLAIM'] })).toBe(true); - expect(taskCloudService.canClaimTask).toHaveBeenCalled(); - expect(taskCloudService.canClaimTaskByState).not.toHaveBeenCalled(); - }); - - it('should delegate unclaim eligibility to the state-based check when permissions are absent', () => { - taskCloudService.canUnclaimTaskByState.and.returnValue(true); - - expect(strategy.canUnclaim({ ...taskDetails, permissions: undefined })).toBe(true); - expect(taskCloudService.canUnclaimTaskByState).toHaveBeenCalled(); - expect(taskCloudService.canUnclaimTask).not.toHaveBeenCalled(); - }); - - it('should delegate unclaim eligibility to the permission-based check when permissions are present', () => { - taskCloudService.canUnclaimTask.and.returnValue(true); - - expect(strategy.canUnclaim({ ...taskDetails, permissions: ['RELEASE'] })).toBe(true); - expect(taskCloudService.canUnclaimTask).toHaveBeenCalled(); - expect(taskCloudService.canUnclaimTaskByState).not.toHaveBeenCalled(); - }); -}); diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.ts deleted file mode 100644 index e641e2f45c..0000000000 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/runtime-bundle-task-details-source.strategy.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*! - * @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 { inject, Injectable } from '@angular/core'; -import { catchError, Observable, throwError } from 'rxjs'; -import { TaskCloudService } from '../../../../services/task-cloud.service'; -import { TaskDetailsCloudModel } from '../../../../models/task-details-cloud.model'; -import { TaskDetailsSourceStrategy } from './task-details-source.strategy'; - -/** - * Reads task details from the Runtime Bundle (always up to date, unlike the eventually - * consistent Query Service). Terminal tasks are no longer served by the Runtime Bundle, so a - * `404` transparently falls back to the Query Service. - * - * Claim/unclaim eligibility prefers the candidate `permissions` when the Runtime Bundle - * provides them, and falls back to a task-state evaluation when it does not (its responses - * historically omit `permissions`). This way the strategy keeps working as-is if the Runtime - * Bundle starts returning `permissions` in the future. - */ -@Injectable({ providedIn: 'root' }) -export class RuntimeBundleTaskDetailsSourceStrategy implements TaskDetailsSourceStrategy { - private readonly taskCloudService = inject(TaskCloudService); - - getTaskDetails$(appName: string, taskId: string): Observable { - return this.taskCloudService - .getTaskById(appName, taskId, 'rb') - .pipe( - catchError((error) => (error?.status === 404 ? this.taskCloudService.getTaskById(appName, taskId, 'query') : throwError(() => error))) - ); - } - - canClaim(taskDetails: TaskDetailsCloudModel): boolean { - return taskDetails?.permissions ? this.taskCloudService.canClaimTask(taskDetails) : this.taskCloudService.canClaimTaskByState(taskDetails); - } - - canUnclaim(taskDetails: TaskDetailsCloudModel): boolean { - return taskDetails?.permissions - ? this.taskCloudService.canUnclaimTask(taskDetails) - : this.taskCloudService.canUnclaimTaskByState(taskDetails); - } -} diff --git a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/task-details-source.strategy.ts b/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/task-details-source.strategy.ts deleted file mode 100644 index ca7e6379ee..0000000000 --- a/lib/process-services-cloud/src/lib/task/task-form/components/user-task-cloud/task-details-source/task-details-source.strategy.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * @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 { Observable } from 'rxjs'; -import { TaskDetailsCloudModel } from '../../../../models/task-details-cloud.model'; - -export type TaskDetailsSource = 'query' | 'rb'; - -/** - * Encapsulates how a task's details are read and how its claim/unclaim eligibility is - * evaluated for a given backend source, so the component can stay agnostic of the source. - */ -export interface TaskDetailsSourceStrategy { - /** Fetch the task details for the given app and task. */ - getTaskDetails$(appName: string, taskId: string): Observable; - /** Whether the task can be claimed. */ - canClaim(taskDetails: TaskDetailsCloudModel): boolean; - /** Whether the task can be unclaimed. */ - canUnclaim(taskDetails: TaskDetailsCloudModel): boolean; -} 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 9880c22e8a..65f2438343 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 @@ -153,41 +153,12 @@ describe('UserTaskCloudComponent', () => { }); }); - describe('taskDetailsSource', () => { + describe('loadTask', () => { beforeEach(() => { fixture.componentRef.setInput('appName', 'app1'); }); - it('should fetch the task from the Query Service by default', () => { - fixture.componentRef.setInput('taskId', 'task1'); - fixture.detectChanges(); - - expect(getTaskSpy).toHaveBeenCalledWith('app1', 'task1'); - }); - - it('should fetch the task from the Runtime Bundle when taskDetailsSource is rb', () => { - fixture.componentRef.setInput('taskDetailsSource', 'rb'); - fixture.componentRef.setInput('taskId', 'task1'); - fixture.detectChanges(); - - expect(getTaskSpy).toHaveBeenCalledWith('app1', 'task1', 'rb'); - }); - - it('should fall back to the Query Service when the Runtime Bundle returns 404 for a terminal task', () => { - getTaskSpy.withArgs('app1', 'task1', 'rb').and.returnValue(throwError(() => ({ status: 404 }))); - getTaskSpy.withArgs('app1', 'task1', 'query').and.returnValue(of(taskDetails)); - fixture.componentRef.setInput('taskDetailsSource', 'rb'); - fixture.componentRef.setInput('taskId', 'task1'); - fixture.detectChanges(); - - expect(getTaskSpy).toHaveBeenCalledWith('app1', 'task1', 'rb'); - expect(getTaskSpy).toHaveBeenCalledWith('app1', 'task1', 'query'); - expect(component.taskDetails).toEqual(taskDetails); - expect(errorEmitSpy).not.toHaveBeenCalled(); - }); - - it('should fall back to the Query Service when taskDetailsSource is an unsupported value', () => { - fixture.componentRef.setInput('taskDetailsSource', 'unsupported'); + it('should fetch the task details by id', () => { fixture.componentRef.setInput('taskId', 'task1'); fixture.detectChanges(); @@ -195,61 +166,6 @@ describe('UserTaskCloudComponent', () => { expect(component.taskDetails).toEqual(taskDetails); expect(errorEmitSpy).not.toHaveBeenCalled(); }); - - it('should reload the task from the new source when taskDetailsSource changes after init', () => { - fixture.componentRef.setInput('taskId', 'task1'); - fixture.detectChanges(); - expect(getTaskSpy).toHaveBeenCalledWith('app1', 'task1'); - - getTaskSpy.calls.reset(); - fixture.componentRef.setInput('taskDetailsSource', 'rb'); - fixture.detectChanges(); - - expect(getTaskSpy).toHaveBeenCalledWith('app1', 'task1', 'rb'); - }); - - it('should emit error when the Runtime Bundle fails with a non-404 error', () => { - const error = { status: 500 }; - getTaskSpy.withArgs('app1', 'task1', 'rb').and.returnValue(throwError(() => error)); - fixture.componentRef.setInput('taskDetailsSource', 'rb'); - fixture.componentRef.setInput('taskId', 'task1'); - fixture.detectChanges(); - - expect(errorEmitSpy).toHaveBeenCalledWith(error); - }); - - describe('claim/unclaim checks in runtime bundle mode (no permissions)', () => { - beforeEach(() => { - fixture.componentRef.setInput('taskDetailsSource', 'rb'); - spyOn(component, 'hasCandidateUsersOrGroups').and.returnValue(true); - }); - - it('should allow claiming a created, non-standalone task even without permissions', () => { - component.taskDetails = { status: TASK_CREATED_STATE, standalone: false } as TaskDetailsCloudModel; - - expect(component.canClaimTask()).toBe(true); - }); - - it('should not allow claiming a task that is not in the created state', () => { - component.taskDetails = { status: TASK_ASSIGNED_STATE, standalone: false } as TaskDetailsCloudModel; - - expect(component.canClaimTask()).toBe(false); - }); - - it('should allow releasing a task assigned to the current user even without permissions', () => { - getCurrentUserSpy.and.returnValue({ username: 'admin.adf' }); - component.taskDetails = { status: TASK_ASSIGNED_STATE, assignee: 'admin.adf', standalone: false } as TaskDetailsCloudModel; - - expect(component.canUnclaimTask()).toBe(true); - }); - - it('should not allow releasing a task assigned to a different user', () => { - getCurrentUserSpy.and.returnValue({ username: 'admin.adf' }); - component.taskDetails = { status: TASK_ASSIGNED_STATE, assignee: 'another.user', standalone: false } as TaskDetailsCloudModel; - - expect(component.canUnclaimTask()).toBe(false); - }); - }); }); describe('Claim/Unclaim buttons', () => { 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 1343d18f0d..a4a90d3683 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 @@ -33,9 +33,6 @@ import { CompleteTaskDirective } from './complete-task/complete-task.directive'; import { catchError, EMPTY, forkJoin } from 'rxjs'; import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox'; import { UserTaskContentType, TaskTypeResolverService, UserTaskType } from '../../../../services/task-type-resolver/task-type-resolver.service'; -import { TaskDetailsSource, TaskDetailsSourceStrategy } from './task-details-source/task-details-source.strategy'; -import { QueryTaskDetailsSourceStrategy } from './task-details-source/query-task-details-source.strategy'; -import { RuntimeBundleTaskDetailsSourceStrategy } from './task-details-source/runtime-bundle-task-details-source.strategy'; @Component({ selector: 'adf-cloud-user-task', @@ -134,17 +131,6 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { @Input() taskId: string; - /** - * Backend service used to fetch the task details. - * - * Defaults to `'query'` (Query Service). Set to `'rb'` to read the task from - * the Runtime Bundle, which is always up to date (the Query Service is - * eventually consistent). In `'rb'` mode, terminal tasks that are no longer - * served by the Runtime Bundle are transparently fetched from the Query Service. - */ - @Input() - taskDetailsSource: TaskDetailsSource = 'query'; - /** Emitted when the cancel button is clicked. */ @Output() cancelClick = new EventEmitter(); @@ -206,11 +192,6 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { private readonly taskTypeResolverService = inject(TaskTypeResolverService); private readonly destroyRef = inject(DestroyRef); - private readonly taskDetailsSourceStrategies: Record = { - query: inject(QueryTaskDetailsSourceStrategy), - rb: inject(RuntimeBundleTaskDetailsSourceStrategy) - }; - ngOnChanges(changes: SimpleChanges) { const appName = changes['appName']; if (appName && appName.currentValue !== appName.previousValue && this.taskId) { @@ -223,12 +204,6 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { this.loadTask(); return; } - - const taskDetailsSource = changes['taskDetailsSource']; - if (taskDetailsSource && !taskDetailsSource.firstChange && this.appName && this.taskId) { - this.loadTask(); - return; - } } ngOnInit() { @@ -238,7 +213,7 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { } canClaimTask(): boolean { - return !this.readOnly && this.taskDetailsStrategy.canClaim(this.taskDetails) && this.hasCandidateUsersOrGroups(); + return !this.readOnly && this.taskCloudService.canClaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); } canCompleteTask(): boolean { @@ -246,7 +221,7 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { } canUnclaimTask(): boolean { - return !this.readOnly && this.taskDetailsStrategy.canUnclaim(this.taskDetails) && this.hasCandidateUsersOrGroups(); + return !this.readOnly && this.taskCloudService.canUnclaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups(); } getTaskType(): void { @@ -324,7 +299,7 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { private loadTask(): void { this.loading = true; - const tasks$ = this.taskDetailsStrategy.getTaskDetails$(this.appName, this.taskId); + 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); @@ -351,10 +326,6 @@ export class UserTaskCloudComponent implements OnInit, OnChanges { }); } - private get taskDetailsStrategy(): TaskDetailsSourceStrategy { - return this.taskDetailsSourceStrategies[this.taskDetailsSource] ?? this.taskDetailsSourceStrategies.query; - } - public switchToDisplayMode(newDisplayMode?: string): void { if (this.adfCloudTaskForm) { this.adfCloudTaskForm.switchToDisplayMode(newDisplayMode);