[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 434 additions and 94 deletions

View File

@ -2,7 +2,7 @@
Title: Card View component
Added: v2.0.0
Status: Active
Last reviewed: 2018-05-09
Last reviewed: 2024-10-29
---
# [Card View component](../../../lib/core/src/lib/card-view/components/card-view/card-view.component.ts "Defined in card-view.component.ts")
@ -353,6 +353,7 @@ const selectItemProperty = new CardViewSelectItemModel(options);
| key\* | string | | Identifying key (important when editing the item) |
| editable | boolean | false | Toggles whether the item is editable |
| value | string | | The original data value for the item |
| autocompleteBased | boolean | false | Indicates whether the select item should use autocomplete functionality. If set to true, the select item will provide an autocomplete input field. |
| options$\* | [`Observable`](http://reactivex.io/documentation/observable.html)<[`CardViewSelectItemOption`](../../../lib/core/src/lib/card-view/interfaces/card-view-selectitem-properties.interface.ts)\[]> | | The original data value for the item |
#### Card Array Item

View File

@ -2,7 +2,7 @@
Title: Card View Update service
Added: v2.0.0
Status: Active
Last reviewed: 2022-11-25
Last reviewed: 2024-10-29
---
# [Card View Update service](../../../lib/core/src/lib/card-view/services/card-view-update.service.ts "Defined in card-view-update.service.ts")
@ -137,6 +137,19 @@ Example
this.cardViewUpdateService.updateElement(cardViewBaseItemModel)
```
## Autocomplete Input Value
The `autocompleteInputValue$` property is a Subject that emits the current value of the autocomplete input field. This can be used to track changes in the input field and respond accordingly.
### Example
You can subscribe to `autocompleteInputValue$` to get the current value of the autocomplete input field and update the options accordingly.
```ts
this.cardViewUpdateService.autocompleteInputValue$.subscribe(value => {
this.options$ = this.getOptions(value);
});
```
## See also
- [Card view component](../components/card-view.component.md)

View File

@ -1,36 +1,80 @@
<ng-container *ngIf="!property.isEmpty() || isEditable">
<div
[attr.data-automation-id]="'card-select-label-' + property.key"
class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty
}"
>{{ property.label | translate }}</div>
<div class="adf-property-field">
<div
*ngIf="!isEditable"
class="adf-property-value adf-property-read-only"
[attr.data-automation-id]="'select-readonly-value-' + property.key"
data-automation-class="read-only-value">{{ (property.displayValue | async) | translate }}
<div [ngSwitch]="templateType">
<div *ngSwitchDefault>
<div
[attr.data-automation-id]="'card-select-label-' + property.key"
class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty
}"
>{{ property.label | translate }}
</div>
<div class="adf-property-field">
<div
*ngIf="!isEditable"
class="adf-property-value adf-property-read-only"
[attr.data-automation-id]="'select-readonly-value-' + property.key"
data-automation-class="read-only-value">{{ (property.displayValue | async) | translate }}
</div>
<div *ngIf="isEditable">
<mat-form-field class="adf-property-value" [ngClass]="{'adf-property-value-editable': isEditable}">
<mat-select
[(value)]="value"
[ngClass]="{ 'adf-property-readonly-value': isReadonlyProperty }"
panelClass="adf-select-filter"
(selectionChange)="onChange($event)"
data-automation-class="select-box"
[aria-label]="property.label | translate">
<adf-select-filter-input *ngIf="showInputFilter" (change)="onFilterInputChange($event)"/>
<mat-option *ngIf="displayNoneOption">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option
*ngFor="let option of list$ | async"
[value]="option.key">
{{ option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div *ngIf="isEditable">
<mat-form-field class="adf-property-value" [ngClass]="{'adf-property-value-editable': isEditable}">
<mat-select
[(value)]="value"
[ngClass]="{ 'adf-property-readonly-value': isReadonlyProperty }"
panelClass="adf-select-filter"
(selectionChange)="onChange($event)"
data-automation-class="select-box"
[aria-label]="property.label | translate">
<adf-select-filter-input *ngIf="showInputFilter" (change)="onFilterInputChange($event)" />
<mat-option *ngIf="displayNoneOption">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option
*ngFor="let option of list$ | async"
[value]="option.key">
{{ option.label | translate }}
<div *ngSwitchCase="'autocompleteBased'">
<mat-form-field
class="adf-property-field adf-card-selectitem-autocomplete"
[ngClass]="{ 'adf-property-read-only': !isEditable }"
[floatLabel]="'always'">
<mat-label
*ngIf="showProperty || isEditable"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-label-' + property.key"
class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty
}">
{{ property.label | translate }}
</mat-label>
<input
matInput
[matAutocomplete]="auto"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty
}"
title="{{ property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="autocompleteControl"
[title]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-value-' + property.key"
/>
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete"
(optionSelected)="onOptionSelected($event)">
<mat-option *ngFor="let option of property.options$ | async" [value]="option.key"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-option-' + property.key">
{{ option.label }}
</mat-option>
</mat-select>
</mat-autocomplete>
</mat-form-field>
</div>
</div>

View File

@ -39,6 +39,10 @@
}
}
.adf-card-selectitem-autocomplete .adf-property-value-editable {
padding-left: 10px;
}
#{$mat-form-field-subscript-wrapper} {
display: none;
}

View File

@ -25,14 +25,17 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing';
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
import { NoopTranslateModule } from '@alfresco/adf-core';
import { CardViewUpdateService, NoopTranslateModule } from '@alfresco/adf-core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
describe('CardViewSelectItemComponent', () => {
let loader: HarnessLoader;
let fixture: ComponentFixture<CardViewSelectItemComponent>;
let component: CardViewSelectItemComponent;
let appConfig: AppConfigService;
let cardViewUpdateService: CardViewUpdateService;
const mockData = [
{ key: 'one', label: 'One' },
{ key: 'two', label: 'Two' },
@ -65,6 +68,7 @@ describe('CardViewSelectItemComponent', () => {
fixture = TestBed.createComponent(CardViewSelectItemComponent);
component = fixture.componentInstance;
appConfig = TestBed.inject(AppConfigService);
cardViewUpdateService = TestBed.inject(CardViewUpdateService);
component.property = new CardViewSelectItemModel(mockDefaultProps);
loader = TestbedHarnessEnvironment.loader(fixture);
});
@ -91,7 +95,7 @@ describe('CardViewSelectItemComponent', () => {
editable: false
});
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
const readOnly = fixture.debugElement.query(By.css('[data-automation-class="read-only-value"]'));
@ -108,7 +112,7 @@ describe('CardViewSelectItemComponent', () => {
});
component.editable = true;
component.displayNoneOption = true;
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
expect(component.value).toEqual('two');
@ -131,7 +135,7 @@ describe('CardViewSelectItemComponent', () => {
});
component.editable = true;
component.displayNoneOption = true;
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
expect(component.value).toEqual(2);
@ -155,7 +159,7 @@ describe('CardViewSelectItemComponent', () => {
});
component.editable = true;
component.displayNoneOption = true;
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
expect(component.isEditable).toBe(true);
@ -168,7 +172,7 @@ describe('CardViewSelectItemComponent', () => {
});
it('should render select box if editable property is TRUE', async () => {
component.ngOnChanges();
component.ngOnChanges({});
component.editable = true;
fixture.detectChanges();
@ -176,7 +180,7 @@ describe('CardViewSelectItemComponent', () => {
});
it('should not have label twice', async () => {
component.ngOnChanges();
component.ngOnChanges({});
component.editable = true;
fixture.detectChanges();
@ -197,7 +201,7 @@ describe('CardViewSelectItemComponent', () => {
});
component.editable = true;
component.displayNoneOption = false;
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
const select = await loader.getHarness(MatSelectHarness);
@ -225,7 +229,7 @@ describe('CardViewSelectItemComponent', () => {
});
component.editable = true;
component.displayNoneOption = false;
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
const select = await loader.getHarness(MatSelectHarness);
@ -245,7 +249,7 @@ describe('CardViewSelectItemComponent', () => {
});
component.editable = true;
component.displayNoneOption = false;
component.ngOnChanges();
component.ngOnChanges({});
fixture.detectChanges();
const select = await loader.getHarness(MatSelectHarness);
@ -255,4 +259,94 @@ describe('CardViewSelectItemComponent', () => {
expect(filterInput).not.toBe(null);
});
});
describe('Autocomplete based', () => {
beforeEach(() => {
component.property = new CardViewSelectItemModel({
label: 'Test Label',
value: 'initial value',
key: 'test-key',
default: 'Placeholder',
editable: true,
autocompleteBased: true,
options$: of([
{ key: '1', label: 'Option 1' },
{ key: '2', label: 'Option 2' }
])
});
});
it('should set templateType to autocompleteBased', () => {
component.property.autocompleteBased = true;
fixture.detectChanges();
expect(component.templateType).toBe('autocompleteBased');
});
it('should set initial value to autocompleteControl', () => {
component.ngOnChanges({});
fixture.detectChanges();
expect(component.autocompleteControl.value).toBe('initial value');
});
it('should emit autocompleteInputValue$ with new value on autocompleteControl change', async () => {
const autocompleteValueSpy = spyOn(cardViewUpdateService.autocompleteInputValue$, 'next');
component.editedValue = '';
component.editable = true;
component.ngOnChanges({ property: { firstChange: true } } as any);
fixture.detectChanges();
component.autocompleteControl.setValue('new value');
fixture.detectChanges();
await fixture.whenStable();
expect(autocompleteValueSpy).toHaveBeenCalledWith('new value');
});
it('should update value correctly on option selected', () => {
cardViewUpdateService.update = jasmine.createSpy('update');
const event: MatAutocompleteSelectedEvent = {
option: {
value: '1'
}
} as MatAutocompleteSelectedEvent;
component.ngOnChanges({});
fixture.detectChanges();
component.onOptionSelected(event);
fixture.detectChanges();
expect(component.autocompleteControl.value).toBe('Option 1');
expect(cardViewUpdateService.update).toHaveBeenCalledWith(jasmine.objectContaining(component.property), '1');
});
it('should disable the autocomplete control', () => {
component.editable = false;
component.ngOnChanges({ editable: { currentValue: false, previousValue: true, firstChange: false, isFirstChange: () => false } });
fixture.detectChanges();
expect(component.autocompleteControl.disabled).toBeTrue();
});
it('should enable the autocomplete control', () => {
component.editable = true;
component.ngOnChanges({ editable: { currentValue: true, previousValue: false, firstChange: false, isFirstChange: () => false } });
fixture.detectChanges();
expect(component.autocompleteControl.enabled).toBeTrue();
});
it('should populate options for autocomplete', async () => {
component.ngOnChanges({});
fixture.detectChanges();
const autocomplete = await loader.getHarness(MatAutocompleteHarness);
await autocomplete.enterText('Op');
fixture.detectChanges();
const options = await autocomplete.getOptions();
expect(options.length).toBe(2);
expect(await options[0].getText()).toContain('Option 1');
expect(await options[1].getText()).toContain('Option 2');
});
});
});

View File

@ -15,30 +15,44 @@
* limitations under the License.
*/
import { Component, Input, OnChanges, OnDestroy, OnInit, inject, ViewEncapsulation } from '@angular/core';
import { Component, Input, OnChanges, OnInit, inject, ViewEncapsulation, SimpleChanges, DestroyRef } from '@angular/core';
import { CardViewSelectItemModel } from '../../models/card-view-selectitem.model';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { CardViewSelectItemOption } from '../../interfaces/card-view.interfaces';
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
import { BaseCardView } from '../base-card-view';
import { AppConfigService } from '../../../app-config/app-config.service';
import { takeUntil, map } from 'rxjs/operators';
import { map, debounceTime, filter, first } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { SelectFilterInputComponent } from './select-filter-input/select-filter-input.component';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'adf-card-view-selectitem',
standalone: true,
imports: [CommonModule, TranslateModule, MatFormFieldModule, MatSelectModule, SelectFilterInputComponent],
imports: [
CommonModule,
TranslateModule,
MatFormFieldModule,
MatSelectModule,
SelectFilterInputComponent,
MatAutocompleteModule,
MatInputModule,
ReactiveFormsModule
],
templateUrl: './card-view-selectitem.component.html',
styleUrls: ['./card-view-selectitem.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-card-view-selectitem' }
})
export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItemModel<string | number>> implements OnInit, OnChanges, OnDestroy {
export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItemModel<string | number>> implements OnInit, OnChanges {
private appConfig = inject(AppConfigService);
private readonly destroyRef = inject(DestroyRef);
static HIDE_FILTER_LIMIT = 5;
@Input() options$: Observable<CardViewSelectItemOption<string | number>[]>;
@ -53,19 +67,47 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
filter$ = new BehaviorSubject<string>('');
showInputFilter: boolean = false;
list$: Observable<CardViewSelectItemOption<string | number>[]> = null;
templateType = '';
autocompleteControl = new UntypedFormControl();
editedValue: string | number;
ngOnChanges(): void {
ngOnChanges(changes: SimpleChanges): void {
this.value = this.property.value;
if (changes.property?.firstChange) {
this.autocompleteControl.valueChanges
.pipe(
filter((textInputValue) => textInputValue !== this.editedValue && textInputValue !== null),
debounceTime(50),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((textInputValue) => {
this.editedValue = textInputValue;
this.cardViewUpdateService.autocompleteInputValue$.next(textInputValue);
});
}
if (changes.editable) {
if (this.isEditable) {
this.autocompleteControl.enable();
} else {
this.autocompleteControl.disable();
}
}
}
ngOnInit() {
if (this.property.autocompleteBased) {
this.templateType = 'autocompleteBased';
}
this.getOptions()
.pipe(takeUntil(this.destroy$))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((options) => {
this.showInputFilter = options.length > this.optionsLimit;
});
this.list$ = this.getList();
this.autocompleteControl.setValue(this.property.value);
}
onFilterInputChange(value: string) {
@ -78,11 +120,22 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
getList(): Observable<CardViewSelectItemOption<string | number>[]> {
return combineLatest([this.getOptions(), this.filter$]).pipe(
map(([items, filter]) => items.filter((item) => (filter ? item.label.toLowerCase().includes(filter.toLowerCase()) : true))),
takeUntil(this.destroy$)
map(([items, searchTerm]) => items.filter((item) => (filter ? item.label.toLowerCase().includes(searchTerm.toLowerCase()) : true)))
);
}
onOptionSelected(event: MatAutocompleteSelectedEvent) {
this.getOptions()
.pipe(first())
.subscribe((options) => {
const selectedOption = options.find((option) => option.key === event.option.value);
if (selectedOption) {
this.autocompleteControl.setValue(selectedOption.label);
this.cardViewUpdateService.update({ ...this.property } as CardViewSelectItemModel<string>, selectedOption.key);
}
});
}
onChange(event: MatSelectChange): void {
const selectedOption = event.value !== undefined ? event.value : null;
this.cardViewUpdateService.update({ ...this.property } as CardViewSelectItemModel<string>, selectedOption);
@ -93,10 +146,6 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
return this.displayEmpty || !this.property.isEmpty();
}
ngOnDestroy() {
super.ngOnDestroy();
}
private get optionsLimit(): number {
return this.appConfig.get<number>('content-metadata.selectFilterLimit', CardViewSelectItemComponent.HIDE_FILTER_LIMIT);
}

View File

@ -27,4 +27,5 @@ export interface CardViewSelectItemProperties<T> extends CardViewItemProperties
value: string | number;
options$: Observable<CardViewSelectItemOption<T>[]>;
displayNoneOption?: boolean;
autocompleteBased?: boolean;
}

View File

@ -22,7 +22,11 @@ import { of } from 'rxjs';
describe('CardViewSelectItemModel', () => {
let properties: CardViewSelectItemProperties<string>;
const mockData = [{ key: 'one', label: 'One' }, { key: 'two', label: 'Two' }, { key: 'three', label: 'Three' }];
const mockData = [
{ key: 'one', label: 'One' },
{ key: 'two', label: 'Two' },
{ key: 'three', label: 'Three' }
];
beforeEach(() => {
properties = {
@ -57,5 +61,16 @@ describe('CardViewSelectItemModel', () => {
expect(itemModel.displayNoneOption).toBe(false);
}));
it('should set autocompleteBased to false by default', fakeAsync(() => {
const itemModel = new CardViewSelectItemModel(properties);
expect(itemModel.autocompleteBased).toBe(false);
}));
it('should set autocompleteBased to true when it passed through the properties', fakeAsync(() => {
properties.autocompleteBased = true;
const itemModel = new CardViewSelectItemModel(properties);
expect(itemModel.autocompleteBased).toBe(true);
}));
});
});

View File

@ -26,6 +26,7 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
type = 'select';
options$: Observable<CardViewSelectItemOption<T>[]>;
displayNoneOption: boolean;
autocompleteBased = false;
valueFetch$: Observable<string> = null;
@ -35,6 +36,7 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
this.displayNoneOption = cardViewSelectItemProperties.displayNoneOption !== undefined ? cardViewSelectItemProperties.displayNoneOption : true;
this.options$ = cardViewSelectItemProperties.options$;
this.autocompleteBased = cardViewSelectItemProperties.autocompleteBased || false;
this.valueFetch$ = this.options$.pipe(
switchMap((options) => {

View File

@ -25,17 +25,17 @@ import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
export const transformKeyToObject = (key: string, value): any => {
const objectLevels: string[] = key.split('.').reverse();
return objectLevels.reduce<any>((previousValue, currentValue) => ({ [currentValue]: previousValue}), value);
return objectLevels.reduce<any>((previousValue, currentValue) => ({ [currentValue]: previousValue }), value);
};
@Injectable({
providedIn: 'root'
})
export class CardViewUpdateService implements BaseCardViewUpdate {
itemUpdated$ = new Subject<UpdateNotification>();
itemClicked$ = new Subject<ClickNotification>();
updateItem$ = new Subject<CardViewBaseItemModel>();
autocompleteInputValue$ = new Subject<string>();
update(property: CardViewBaseItemModel, newValue: any) {
this.itemUpdated$.next({
@ -58,5 +58,4 @@ export class CardViewUpdateService implements BaseCardViewUpdate {
updateElement(notification: CardViewBaseItemModel) {
this.updateItem$.next(notification);
}
}

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