[MNT-24496] ADW Integration with APS Improvements - Re-assign Tasks (#10350)

* [MNT-24496] ADW Integration with APS Improvements - Re-assign Tasks

* [MNT-24496] code improvements

* [MNT-24496] remove duplications

* [MNT-24496] add unit test

* [MNT-24496] cr fixes

* [MNT-24496] empty commit [ci:force]

* [MNT-24496] fix unit test

* [MNT-24496] empty commit [ci:force]

* [MNT-24496] cr fix

* [MNT-24496] remove redundant import
This commit is contained in:
Mykyta Maliarchuk
2024-11-06 10:48:40 +01:00
committed by GitHub
parent 558ff71878
commit 258f01803c
14 changed files with 434 additions and 94 deletions

View File

@@ -54,6 +54,7 @@ describe('TaskDetailsComponent', () => {
let getTaskDetailsSpy: jasmine.Spy;
let getTasksSpy: jasmine.Spy;
let assignTaskSpy: jasmine.Spy;
let getWorkflowUsersSpy: jasmine.Spy;
let taskCommentsService: CommentsService;
let peopleProcessService: PeopleProcessService;
@@ -63,6 +64,7 @@ describe('TaskDetailsComponent', () => {
});
peopleProcessService = TestBed.inject(PeopleProcessService);
spyOn(peopleProcessService, 'getCurrentUserInfo').and.returnValue(of({ email: 'fake-email' } as any));
getWorkflowUsersSpy = spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of([]));
const taskListService = TestBed.inject(TaskListService);
spyOn(taskListService, 'getTaskChecklist').and.returnValue(of(noDataMock));
@@ -371,7 +373,7 @@ describe('TaskDetailsComponent', () => {
});
it('should return an observable with user search results', () => {
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(
getWorkflowUsersSpy.and.returnValue(
of([
{
id: 1,
@@ -402,7 +404,7 @@ describe('TaskDetailsComponent', () => {
});
it('should return an empty list for not valid search', () => {
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of([]));
getWorkflowUsersSpy.and.returnValue(of([]));
let lastValue: LightUserRepresentation[];
component.peopleSearch.subscribe((users) => (lastValue = users));

View File

@@ -1,23 +1,23 @@
<mat-card appearance="outlined" *ngIf="taskDetails" class="adf-card-container">
<mat-card-content>
<adf-card-view [properties]="properties" [editable]="!isCompleted()" [displayClearAction]="displayDateClearAction" />
<adf-card-view [properties]="properties" [editable]="!readOnly && !isCompleted()" [displayClearAction]="displayDateClearAction"/>
</mat-card-content>
<mat-card-actions class="adf-controls" *ngIf="showClaimRelease">
<button *ngIf="isTaskClaimedByCandidateMember()"
mat-button
data-automation-id="header-unclaim-button"
id="unclaim-task"
<button *ngIf="isTaskClaimedByCandidateMember()"
mat-button
data-automation-id="header-unclaim-button"
id="unclaim-task"
class="adf-claim-controls"
adf-unclaim-task
[taskId]="taskDetails.id"
(success)="onUnclaimTask($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}
</button>
<button *ngIf="isTaskClaimable()"
mat-button
data-automation-id="header-claim-button"
id="claim-task"
<button *ngIf="isTaskClaimable()"
mat-button
data-automation-id="header-claim-button"
id="claim-task"
class="adf-claim-controls"
adf-claim-task
[taskId]="taskDetails.id"

View File

@@ -15,10 +15,10 @@
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppConfigService } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { AppConfigService, CardViewUpdateService } from '@alfresco/adf-core';
import { of, Subject } from 'rxjs';
import {
completedTaskDetailsMock,
taskDetailsMock,
@@ -32,6 +32,7 @@ import { TaskHeaderComponent } from './task-header.component';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
import { PeopleProcessService } from '../../../services/people-process.service';
import { TaskRepresentation } from '@alfresco/js-api';
import { SimpleChanges } from '@angular/core';
describe('TaskHeaderComponent', () => {
let service: TaskListService;
@@ -39,6 +40,7 @@ describe('TaskHeaderComponent', () => {
let fixture: ComponentFixture<TaskHeaderComponent>;
let peopleProcessService: PeopleProcessService;
let appConfigService: AppConfigService;
let cardViewUpdateService: CardViewUpdateService;
const fakeBpmAssignedUser: any = {
id: 1001,
@@ -63,8 +65,10 @@ describe('TaskHeaderComponent', () => {
service = TestBed.inject(TaskListService);
peopleProcessService = TestBed.inject(PeopleProcessService);
spyOn(peopleProcessService, 'getCurrentUserInfo').and.returnValue(of(fakeBpmAssignedUser));
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of([{ id: 1, firstName: 'Test', lastName: 'User' }]));
component.taskDetails = new TaskRepresentation(taskDetailsMock);
appConfigService = TestBed.inject(AppConfigService);
cardViewUpdateService = TestBed.inject(CardViewUpdateService);
});
const getClaimButton = () => fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]'))?.nativeElement as HTMLButtonElement;
@@ -72,6 +76,66 @@ describe('TaskHeaderComponent', () => {
const getUnclaimButton = () =>
fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'))?.nativeElement as HTMLButtonElement;
const triggerNgOnChanges = (currentValue: any, previousValue: any) => {
const changes: SimpleChanges = {
taskDetails: {
currentValue,
previousValue,
firstChange: false,
isFirstChange: () => false
}
};
component.ngOnChanges(changes);
};
it('should set users$ when autocompleteInputValue$ emits new value', fakeAsync(() => {
const autocompleteInputValue$ = cardViewUpdateService.autocompleteInputValue$;
component.ngOnInit();
autocompleteInputValue$.next('test');
tick(300);
component.users$.subscribe((users) => {
expect(users).toEqual([{ key: 1, label: 'Test User' }]);
});
}));
it('should call initData on resetChanges subscription', () => {
const resetChanges$ = new Subject<void>();
component.resetChanges = resetChanges$;
spyOn(component, 'initData');
component.ngOnInit();
expect(component.initData).toHaveBeenCalledTimes(1);
resetChanges$.next();
expect(component.initData).toHaveBeenCalledTimes(2);
});
it('should call initData when assignee changes', () => {
spyOn(component, 'initData');
triggerNgOnChanges({ id: '1', assignee: { id: '2' } }, { id: '1', assignee: { id: '1' } });
expect(component.initData).toHaveBeenCalled();
});
it('should call initData when task id changes', () => {
spyOn(component, 'initData');
triggerNgOnChanges({ id: '2', assignee: { id: '1' } }, { id: '1', assignee: { id: '1' } });
expect(component.initData).toHaveBeenCalled();
});
it('should call refreshData when taskDetails change', () => {
spyOn(component, 'refreshData');
triggerNgOnChanges(
{ id: '1', assignee: { id: '1' }, description: 'one' },
{
id: '1',
assignee: { id: '1' },
description: 'two'
}
);
expect(component.refreshData).toHaveBeenCalled();
});
it('should render empty component if no task details provided', async () => {
component.taskDetails = undefined;
@@ -87,7 +151,7 @@ describe('TaskHeaderComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const formNameEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-textitem-clickable-value'));
const formNameEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-property-value'));
expect(formNameEl.nativeElement.value).toBe('Wilbur Adams');
});
@@ -98,7 +162,7 @@ describe('TaskHeaderComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-textitem-clickable-value'));
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-property-value'));
expect(valueEl.nativeElement.value).toBe('ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT');
});

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Component, EventEmitter, DestroyRef, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation, inject } from '@angular/core';
import {
CardViewDateItemModel,
CardViewMapItemModel,
@@ -25,7 +25,10 @@ import {
AppConfigService,
CardViewIntItemModel,
CardViewItemLengthValidator,
CardViewComponent
CardViewComponent,
CardViewUpdateService,
CardViewSelectItemModel,
CardViewSelectItemOption
} from '@alfresco/adf-core';
import { PeopleProcessService } from '../../../services/people-process.service';
import { TaskDescriptionValidator } from '../../validators/task-description.validator';
@@ -36,6 +39,9 @@ import { MatButtonModule } from '@angular/material/button';
import { UnclaimTaskDirective } from '../task-form/unclaim-task.directive';
import { ClaimTaskDirective } from '../task-form/claim-task.directive';
import { TranslateModule } from '@ngx-translate/core';
import { debounceTime, filter, map, switchMap } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'adf-task-header',
@@ -58,6 +64,18 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
@Input()
showClaimRelease = true;
/**
* (optional) This flag sets read-only mode, preventing changes.
*/
@Input()
readOnly = false;
/**
* Refreshes the card data when an event emitted.
*/
@Input()
resetChanges = new Subject<void>();
/** Emitted when the task is claimed. */
@Output()
claim: EventEmitter<any> = new EventEmitter<any>();
@@ -72,24 +90,51 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
dateLocale: string;
private currentUserId: number;
private readonly destroyRef = inject(DestroyRef);
private readonly usersSubject$ = new BehaviorSubject<CardViewSelectItemOption<number>[]>([]);
users$ = this.usersSubject$.asObservable();
constructor(
private peopleProcessService: PeopleProcessService,
private translationService: TranslationService,
private appConfig: AppConfigService
private readonly appConfig: AppConfigService,
private readonly cardViewUpdateService: CardViewUpdateService
) {
this.dateFormat = this.appConfig.get('dateValues.defaultDateFormat');
this.dateLocale = this.appConfig.get('dateValues.defaultDateLocale');
}
ngOnInit() {
this.loadCurrentBpmUserId();
this.initData();
this.peopleProcessService
.getCurrentUserInfo()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((res) => {
this.currentUserId = res ? +res.id : null;
this.initData();
});
this.cardViewUpdateService.autocompleteInputValue$
.pipe(
filter((res) => res.length > 0),
debounceTime(300),
switchMap((res) => this.getUsers(res)),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((users) => {
this.usersSubject$.next(users);
});
this.resetChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.initData();
});
}
ngOnChanges(changes: SimpleChanges) {
const taskDetailsChange = changes['taskDetails'];
if (taskDetailsChange?.currentValue?.id !== taskDetailsChange?.previousValue?.id) {
if (
taskDetailsChange?.currentValue?.id !== taskDetailsChange?.previousValue?.id ||
taskDetailsChange?.currentValue?.assignee?.id !== taskDetailsChange?.previousValue?.assignee?.id
) {
this.initData();
} else {
this.refreshData();
@@ -249,15 +294,31 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
return this.taskDetails.duration ? `${this.taskDetails.duration} ms` : '';
}
private getUsers(searchQuery: string): Observable<CardViewSelectItemOption<number>[]> {
return this.peopleProcessService.getWorkflowUsers(undefined, searchQuery).pipe(
map((users) =>
users
.filter((user) => user.id !== this.currentUserId)
.map(({ id, firstName = '', lastName = '' }) => ({
key: id,
label: `${firstName} ${lastName}`.trim()
}))
)
);
}
private initDefaultProperties(parentInfoMap: Map<string, string>): any[] {
return [
new CardViewTextItemModel({
new CardViewSelectItemModel({
label: 'ADF_TASK_LIST.PROPERTIES.ASSIGNEE',
value: this.taskDetails.getFullName(),
value: this.taskDetails.getFullName()
? this.taskDetails.getFullName()
: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT'),
key: 'assignee',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT'),
clickable: !this.isCompleted(),
icon: 'create'
editable: this.isAssignedToCurrentUser(),
autocompleteBased: true,
icon: 'create',
options$: this.users$
}),
new CardViewTextItemModel({
label: 'ADF_TASK_LIST.PROPERTIES.STATUS',
@@ -345,13 +406,4 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
private isValidSelection(filteredProperties: string[], cardItem: CardViewBaseItemModel): boolean {
return filteredProperties ? filteredProperties.indexOf(cardItem.key) >= 0 : true;
}
/**
* Loads current bpm userId
*/
private loadCurrentBpmUserId(): void {
this.peopleProcessService.getCurrentUserInfo().subscribe((res) => {
this.currentUserId = res ? +res.id : null;
});
}
}