Skip to content

Commit

Permalink
feat(tree-view): seleção única e customizada
Browse files Browse the repository at this point in the history
Permite a função de seleção única utilizando po-radio
e a customizar quais itens devem ou não receber seleção.

Fixes DTHFUI-7624
  • Loading branch information
guilnorth authored and rafaellmarques committed Oct 6, 2023
1 parent 90e9362 commit 7e9c9da
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 14 deletions.
1 change: 0 additions & 1 deletion projects/portal/src/assets/json/version.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,25 @@ describe('PoTreeViewBaseComponent:', () => {

expectPropertiesValues(component, 'maxLevel', invalidValues, 4);
});

it('p-single-select: should update property with `true` if valid values', () => {
const validValues = [true, 'true', 1, ''];

expectPropertiesValues(component, 'singleSelect', validValues, true);
});

it('p-single-select: should update property with `false` if invalid values', () => {
const invalidValues = [10, 0.5, 'test', undefined];

expectPropertiesValues(component, 'singleSelect', invalidValues, false);
});
});

describe('Methods: ', () => {
beforeEach(() => {
component.singleSelect = false;
});

it('emitExpanded: should call collapsed.emit with tree view item if treeViewItem.expanded is false', () => {
const treeViewItem = { label: 'Nível 01', value: 1, expanded: false };

Expand Down Expand Up @@ -100,6 +116,21 @@ describe('PoTreeViewBaseComponent:', () => {
expect(spyUpdateItemsOnSelect).toHaveBeenCalledWith(treeViewItem);
});

it('emitSelected: should emit without treeViewItem.subItems if is `singleSelect`', () => {
const treeViewItem = { label: 'Nível 01', value: 1, selected: true, subItems: [{ label: 'Nivel 02', value: 2 }] };
const expected = { label: 'Nível 01', value: 1, selected: true };

const spyUpdateItemsOnSelect = spyOn(component, <any>'updateItemsOnSelect');
const spySelectedEmit = spyOn(component['selected'], 'emit');

component.singleSelect = true;
component['emitSelected'](treeViewItem);

expect(component.singleSelect).toEqual(true);
expect(spySelectedEmit).toHaveBeenCalledWith(expected);
expect(spyUpdateItemsOnSelect).toHaveBeenCalledWith(expected);
});

it('getItemsByMaxLevel: should return and not call addItem if level is 4', () => {
const items = [];

Expand Down Expand Up @@ -216,6 +247,23 @@ describe('PoTreeViewBaseComponent:', () => {
expect(spyExpandParentItem).not.toHaveBeenCalledWith(childItem, parentItem);
});

it('addItem: shouldn`t call selectItemBySubItems if is `singleSelect`', () => {
const childItem = { label: 'Nível 02', value: 2 };
const parentItem = { label: 'Nível 01', value: 1 };
const items = [];

const expectedValue = [parentItem];

const spySelectItemBySubItems = spyOn(component, <any>'selectItemBySubItems');

component.singleSelect = true;
component['addItem'](items, childItem, parentItem);

expect(items.length).toBe(1);
expect(items).toEqual(expectedValue);
expect(spySelectItemBySubItems).not.toHaveBeenCalled();
});

it('addItem: should add parentItem in items and call expandParentItem, addChildItemInParent and selectItemBySubItems', () => {
const childItem = { label: 'Nível 02', value: 2 };
const parentItem = { label: 'Nível 01', value: 1 };
Expand Down Expand Up @@ -297,6 +345,26 @@ describe('PoTreeViewBaseComponent:', () => {
expect(spyGetItemsWithParentSelected).toHaveBeenCalledWith(component.items);
});

it('updateItemsOnSelect: shouldn`t call selectAllItems if is singleSelect', () => {
const selectedItem = {
label: 'Label 01',
value: '01',
selected: true,
subItems: [{ label: 'Label 01.1', value: '01.1' }]
};
const items = [selectedItem];
component.items = items;
component.singleSelect = true;

const spyGetItemsWithParentSelected = spyOn(component, <any>'getItemsWithParentSelected').and.returnValue(items);
const spySelect = spyOn(component, <any>'selectAllItems');

component['updateItemsOnSelect'](selectedItem);

expect(spySelect).not.toHaveBeenCalled();
expect(spyGetItemsWithParentSelected).toHaveBeenCalledWith(component.items);
});

it('updateItemsOnSelect: should call selectAllItems if selectedItem has subItems and call getItemsWithParentSelected', () => {
const selectedItem = {
label: 'Label 01',
Expand All @@ -305,6 +373,7 @@ describe('PoTreeViewBaseComponent:', () => {
subItems: [{ label: 'Label 01.1', value: '01.1' }]
};
const items = [selectedItem];

component.items = items;

const spyGetItemsWithParentSelected = spyOn(component, <any>'getItemsWithParentSelected').and.returnValue(items);
Expand Down Expand Up @@ -370,6 +439,62 @@ describe('PoTreeViewBaseComponent:', () => {
expect(items).toEqual(expectedItems);
});

it('selectAllItems: shouldn`t set `selected` on item if isSelectable is false', () => {
const items = [
{
label: 'Nivel 01',
value: 1,
selected: true,
subItems: [
{
label: 'Nivel 02',
value: 2,
selected: false,
isSelectable: false,
subItems: [
{
label: 'Nivel 03',
value: 3,
selected: false,
subItems: [{ label: 'Nivel 04', value: 4, selected: false }]
}
]
}
]
}
];

const expectedItems = [
{
label: 'Nivel 01',
value: 1,
selected: true,
subItems: [
{
label: 'Nivel 02',
value: 2,
isSelectable: false,
selected: false,
subItems: [
{
label: 'Nivel 03',
value: 3,
selected: true,
subItems: [{ label: 'Nivel 04', value: 4, selected: true }]
}
]
}
]
}
];

const isSelected = true;

component['selectAllItems'](items, isSelected);

expect(items).toEqual(expectedItems);
});

it('selectAllItems: should unselect all items if isSelected is false', () => {
const items = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export class PoTreeViewBaseComponent {
private _items: Array<PoTreeViewItem> = [];
private _selectable: boolean = false;
private _maxLevel = poTreeViewMaxLevel;
private _singleSelect: boolean = false;

// armazena o value do item selecionado
selectedValue: string | number;

/**
* Lista de itens do tipo `PoTreeViewItem` que será renderizada pelo componente.
Expand Down Expand Up @@ -99,6 +103,23 @@ export class PoTreeViewBaseComponent {
return this._selectable;
}

/**
* @optional
*
* @description
*
* Habilita a seleção para item único atráves de po-radio.
*
* @default false
*/
@Input('p-single-select') set singleSelect(value: boolean) {
this._singleSelect = convertToBoolean(value);
}

get singleSelect() {
return this._singleSelect;
}

/**
* @optional
*
Expand Down Expand Up @@ -127,9 +148,15 @@ export class PoTreeViewBaseComponent {
protected emitSelected(treeViewItem: PoTreeViewItem) {
const event = treeViewItem.selected ? 'selected' : 'unselected';

this.updateItemsOnSelect(treeViewItem);
this.selectedValue = treeViewItem.value;

this[event].emit({ ...treeViewItem });
// Não emitir subItems quando for singleSelect
const { subItems, ...rest } = treeViewItem;
const treeViewToEmit = this.singleSelect ? { ...rest } : treeViewItem;

this.updateItemsOnSelect(treeViewToEmit);

this[event].emit({ ...treeViewToEmit });
}

private addChildItemInParent(childItem: PoTreeViewItem, parentItem: PoTreeViewItem) {
Expand All @@ -150,8 +177,12 @@ export class PoTreeViewBaseComponent {
if (isNewItem) {
this.expandParentItem(childItem, parentItem);
}

this.addChildItemInParent(childItem, parentItem);
this.selectItemBySubItems(parentItem);

if (!this.singleSelect) {
this.selectItemBySubItems(parentItem);
}

items.push(parentItem);
} else {
Expand All @@ -165,7 +196,7 @@ export class PoTreeViewBaseComponent {
this.selectAllItems(item.subItems, isSelected);
}

item.selected = isSelected;
item.selected = item.isSelectable !== false ? isSelected : false;
});
}

Expand Down Expand Up @@ -225,6 +256,10 @@ export class PoTreeViewBaseComponent {
--level;
}

if (item.selected) {
this.selectedValue = currentItem.value;
}

this.addItem(newItems, currentItem, parentItem, true);
});

Expand All @@ -246,7 +281,7 @@ export class PoTreeViewBaseComponent {
}

private updateItemsOnSelect(selectedItem: PoTreeViewItem) {
if (selectedItem.subItems) {
if (selectedItem.subItems && !this.singleSelect) {
this.selectAllItems(selectedItem.subItems, selectedItem.selected);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</span>
</button>

<ng-container *ngIf="selectable; then checkboxTemplate; else labelTemplate"></ng-container>
<ng-container *ngIf="selectable; then selectionTemplate; else labelTemplate"></ng-container>
</div>

<ng-template #labelTemplate>
Expand All @@ -16,13 +16,34 @@
</span>
</ng-template>

<ng-template #selectionTemplate>
<ng-container *ngIf="singleSelect; then radioTemplate; else checkboxTemplate"></ng-container>
</ng-template>

<ng-template #checkboxTemplate>
<po-checkbox
class="po-tree-view-item-header-checkbox"
[class.po-tree-view-item-header-padding]="!hasSubItems"
[p-label]="item.label"
[(ngModel)]="item.selected"
(p-change)="selected.emit(item)"
[p-disabled]="item.isSelectable === false"
>
</po-checkbox>
</ng-template>

<ng-template #radioTemplate>
<po-radio
class="po-tree-view-item-header-checkbox"
[class.po-tree-view-item-header-padding]="!hasSubItems"
#inputRadio
[name]="idRadio"
[(ngModel)]="item.selected"
[p-label]="item.label"
[p-value]="item.value"
[p-checked]="item.value === selectedValue"
(p-change-selected)="selected.emit(item)"
[p-disabled]="item.isSelectable === false"
>
</po-radio>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { uuid } from '../../../utils/util';

import { PoTreeViewItem } from '../po-tree-view-item/po-tree-view-item.interface';

Expand All @@ -14,10 +15,16 @@ export class PoTreeViewItemHeaderComponent {

@Input('p-selectable') selectable: boolean = false;

@Input('p-single-select') singleSelect: boolean;

@Output('p-expanded') expanded = new EventEmitter<MouseEvent>();

@Output('p-selected') selected = new EventEmitter<any>();

@Input('p-selected-value') selectedValue: string | number;

idRadio = `po-radio[${uuid()}]`;

get hasSubItems() {
return !!(this.item.subItems && this.item.subItems.length);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<po-tree-view-item-header
[p-item]="item"
[p-selectable]="selectable"
[p-single-select]="singleSelect"
[p-selected-value]="selectedValue"
(p-expanded)="onClick($event)"
(p-selected)="onSelect(item)"
>
Expand All @@ -12,6 +14,8 @@
*ngFor="let subItem of item.subItems; trackBy: trackByFunction"
[p-item]="subItem"
[p-selectable]="selectable"
[p-single-select]="singleSelect"
[p-selected-value]="selectedValue"
>
</po-tree-view-item>
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class PoTreeViewItemComponent {

@Input('p-selectable') selectable: boolean;

@Input('p-single-select') singleSelect: boolean;

@Input('p-selected-value') selectedValue: string | number;

get hasSubItems() {
return !!(this.item.subItems && this.item.subItems.length);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export interface PoTreeViewItem {
*/
selected?: boolean | null;

/**
* Permite ativar/desativar a seleção do item
*/
isSelectable?: boolean | null;

/** Lista de itens do próximo nível, e assim consecutivamente até que se atinja o quarto nível. */
subItems?: Array<PoTreeViewItem>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<po-container *ngIf="hasItems" p-no-padding>
<ul class="po-tree-view">
<po-tree-view-item *ngFor="let item of items; trackBy: trackByFunction" [p-item]="item" [p-selectable]="selectable">
<po-tree-view-item
*ngFor="let item of items; trackBy: trackByFunction"
[p-item]="item"
[p-selectable]="selectable"
[p-single-select]="singleSelect"
[p-selected-value]="selectedValue"
>
</po-tree-view-item>
</ul>
</po-container>
Loading

0 comments on commit 7e9c9da

Please sign in to comment.