[ADF-4559] Add chips to multivalue metadata properties (#5552)

* [ADF-4559] Add chips to multivalue metadata properties

* Fix app config schema

* Restore app config

* Fix checkListIsSorted method

* Fix e2e datatable sorting

* Fix e2e tests

* Improve chips input
This commit is contained in:
davidcanonieto 2020-03-23 13:02:01 +00:00 committed by GitHub
parent 666dd45fa2
commit 144da83d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 223 additions and 95 deletions

View File

@ -820,7 +820,8 @@
"exif:exif": "*"
}
},
"multi-value-pipe-separator": ", "
"multi-value-pipe-separator": ", ",
"multi-value-chips": true
},
"sideNav": {
"expandedSidenav": true,

View File

@ -347,6 +347,13 @@ To customize the separator used by this card you can set it in your `app.config.
"exif:exif": [ "exif:pixelXDimension", "exif:pixelYDimension"]
}
},
"multi-value-pipe-separator" : " - "
"multi-value-pipe-separator" : " - ",
"multi-value-chips" : false
},
```
### Use chips for multi value properties
If you want to display chips fo each value instead of a composed string you just need to enable it in the content-metadata config.
![Chips for multi value properties](../../docassets/images/metadata-chips.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -204,10 +204,10 @@ describe('Edit task filters and task list properties', () => {
await tasksCloudDemoPage.editTaskFilterCloudComponent().setSortFilterDropDown('Priority');
await tasksCloudDemoPage.editTaskFilterCloudComponent().setOrderFilterDropDown('ASC');
await expect(await tasksCloudDemoPage.taskListCloudComponent().getDataTable().checkListIsSorted('ASC', 'Priority')).toBe(true);
await expect(await tasksCloudDemoPage.taskListCloudComponent().getDataTable().checkListIsSorted('ASC', 'Priority', 'NUMBER')).toBe(true);
await tasksCloudDemoPage.editTaskFilterCloudComponent().setOrderFilterDropDown('DESC');
await expect(await tasksCloudDemoPage.taskListCloudComponent().getDataTable().checkListIsSorted('DESC', 'Priority')).toBe(true);
await expect(await tasksCloudDemoPage.taskListCloudComponent().getDataTable().checkListIsSorted('DESC', 'Priority', 'NUMBER')).toBe(true);
});
it('[C307115] Should display tasks sorted by owner when owner is selected from sort dropdown', async () => {

View File

@ -899,26 +899,6 @@
"pattern": "^\\*$",
"description": "Wildcard for every aspect"
},
{
"type": "object",
"description": "",
"required": [
"includeAll"
],
"properties": {
"includeAll": {
"description": "includeAll all property",
"type": "boolean"
},
"postfix": {
"description": "exclude",
"type": [
"string",
"array"
]
}
}
},
{
"$ref": "#/definitions/content-metadata-aspect"
},
@ -932,6 +912,10 @@
"multi-value-pipe-separator": {
"description": "Content metadata's separator for multi value properties",
"type": "string"
},
"multi-value-chips": {
"description": "Use chips for multi value properties",
"type": "boolean"
}
}
},

View File

@ -1,95 +1,142 @@
<div [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label" *ngIf="showProperty() || isEditable()">{{ property.label | translate }}</div>
<div [attr.data-automation-id]="'card-textitem-label-' + property.key"
class="adf-property-label"
*ngIf="showProperty() || isEditable()">{{ property.label | translate }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditable()">
<span *ngIf="!isClickable(); else elseBlock" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty()"
[ngClass]="property.multiline?'adf-textitem-multiline':'adf-textitem-scroll'">
{{ property.displayValue }}</span>
</span>
<ng-template #elseBlock>
<div role="button" class="adf-textitem-clickable" [attr.data-automation-id]="'card-textitem-toggle-' + property.key" (click)="clicked()" fxLayout="row" fxLayoutAlign="space-between center">
<span class="adf-textitem-clickable-value" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
<span *ngIf="!isClickable(); else nonClickableTemplate"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="!isChipViewEnabled; else chipListTemplate">
<span *ngIf="showProperty()"
[ngClass]="property.multiline?'adf-textitem-multiline':'adf-textitem-scroll'">
{{ property.displayValue }}
</span>
<button mat-icon-button fxFlex="0 0 auto" *ngIf="showClickableIcon()"
class="adf-textitem-action"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'card-textitem-clickable-icon-' + property.key">
</span>
</span>
<ng-template #nonClickableTemplate>
<div role="button"
class="adf-textitem-clickable"
[attr.data-automation-id]="'card-textitem-toggle-' + property.key"
(click)="clicked()"
fxLayout="row"
fxLayoutAlign="space-between center">
<span class="adf-textitem-clickable-value"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else emptyValueTemplate">{{ property.displayValue }}</span>
</span>
<button mat-icon-button
fxFlex="0 0 auto"
*ngIf="showClickableIcon()"
class="adf-textitem-action"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'card-textitem-clickable-icon-' + property.key">
<mat-icon class="adf-textitem-icon">{{property?.icon}}</mat-icon>
</button>
</div>
</ng-template>
</span>
<span *ngIf="isEditable()">
<div *ngIf="!inEdit" role="button"
tabindex="0"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate"
(click)="setEditMode(true)"
(keydown.enter)="setEditMode(true)"
class="adf-textitem-readonly"
[attr.data-automation-id]="'card-textitem-toggle-' + property.key"
fxLayout="row" fxLayoutAlign="space-between center">
<span [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
<div *ngIf="!inEdit"
role="button"
tabindex="0"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate"
(click)="setEditMode(true)"
(keydown.enter)="setEditMode(true)"
class="adf-textitem-readonly"
[attr.data-automation-id]="'card-textitem-toggle-' + property.key"
fxLayout="row"
fxLayoutAlign="space-between center">
<span *ngIf="!isChipViewEnabled; else chipListTemplate"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else emptyValueTemplate">{{ property.displayValue }}</span>
</span>
<button mat-icon-button fxFlex="0 0 auto"
class="adf-textitem-action"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'card-textitem-edit-icon-' + property.key">
<button mat-icon-button
fxFlex="0 0 auto"
class="adf-textitem-action"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'card-textitem-edit-icon-' + property.key">
<mat-icon class="adf-textitem-icon"> create</mat-icon>
</button>
</div>
<div *ngIf="inEdit" class="adf-textitem-editable">
<div *ngIf="inEdit"
class="adf-textitem-editable">
<div class="adf-textitem-editable-controls">
<mat-form-field floatPlaceholder="never" class="adf-input-container">
<input *ngIf="!property.multiline" #editorInput
<mat-form-field floatPlaceholder="never"
class="adf-input-container">
<input *ngIf="!isChipViewEnabled && !property.multiline"
#editorInput
(keydown.escape)="reset($event)"
(keydown.enter)="update($event)"
matInput
class="adf-input"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-editinput-' + property.key">
<textarea *ngIf="property.multiline" #editorInput
matInput
matTextareaAutosize
matAutosizeMaxRows="1"
matAutosizeMaxRows="5"
class="adf-textarea"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
(input)="onTextAreaInputChange()"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea>
matInput
class="adf-input"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-editinput-' + property.key">
<textarea *ngIf="!isChipViewEnabled && property.multiline"
#editorInput
matInput
matTextareaAutosize
matAutosizeMaxRows="1"
matAutosizeMaxRows="5"
class="adf-textarea"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
(input)="onTextAreaInputChange()"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea>
<div *ngIf="isChipViewEnabled">
<mat-chip-list class="adf-input"
#chipList>
<mat-chip *ngFor="let propertyValue of editedValue; let idx = index"
[removable]="true"
(removed)="removeValueFromList(idx)">
{{ propertyValue }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input #editorInput
[placeholder]="property.default | translate"
[matChipInputFor]="chipList"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addValueToList($event)"
[attr.data-automation-id]="'card-textitem-editchipinput-' + property.key">
</mat-chip-list>
</div>
</mat-form-field>
<button mat-icon-button class="adf-textitem-action" (click)="update($event)"
[attr.aria-label]="'CORE.METADATA.ACTIONS.SAVE' | translate"
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
[attr.data-automation-id]="'card-textitem-update-' + property.key">
<button mat-icon-button
class="adf-textitem-action"
(click)="update($event)"
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
[attr.data-automation-id]="'card-textitem-update-' + property.key">
<mat-icon class="adf-textitem-icon">done</mat-icon>
</button>
<button mat-icon-button (click)="reset($event)" class="adf-textitem-action"
[attr.aria-label]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">
<button mat-icon-button
(click)="reset($event)"
class="adf-textitem-action"
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">
<mat-icon>clear</mat-icon>
</button>
</div>
<mat-error [attr.data-automation-id]="'card-textitem-error-' + property.key" class="adf-textitem-editable-error" *ngIf="hasErrors()">
<mat-error [attr.data-automation-id]="'card-textitem-error-' + property.key"
class="adf-textitem-editable-error"
*ngIf="hasErrors()">
<ul>
<li *ngFor="let errorMessage of errorMessages">{{ errorMessage | translate }}</li>
</ul>
</mat-error>
</div>
</span>
<ng-template #elseEmptyValueBlock>
<ng-template #emptyValueTemplate>
<span class="adf-textitem-default-value">{{ property.default | translate }}</span>
</ng-template>
<ng-template #chipListTemplate>
<mat-chip-list>
<mat-chip *ngFor="let propertyValue of editedValue">
{{ propertyValue }}
</mat-chip>
</mat-chip-list>
</ng-template>
</div>

View File

@ -23,6 +23,7 @@ import { CardViewTextItemComponent } from './card-view-textitem.component';
import { setupTestBed } from '../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../testing/core.testing.module';
import { CardViewItemFloatValidator, CardViewItemIntValidator } from '@alfresco/adf-core';
import { MatChipsModule } from '@angular/material';
describe('CardViewTextItemComponent', () => {
@ -31,7 +32,10 @@ describe('CardViewTextItemComponent', () => {
const mouseEvent = new MouseEvent('click');
setupTestBed({
imports: [CoreTestingModule]
imports: [
CoreTestingModule,
MatChipsModule
]
});
beforeEach(() => {
@ -146,6 +150,48 @@ describe('CardViewTextItemComponent', () => {
const editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
expect(editIcon).toBeNull('Edit icon should NOT be shown');
});
it('should render chips for multivalue properties when chips are enabled', () => {
component.property = new CardViewTextItemModel({
label: 'Text label',
value: ['item1', 'item2', 'item3'],
key: 'textkey',
default: ['FAKE-DEFAULT-KEY'],
editable: true,
multivalued: true
});
component.useChipsForMultiValueProperty = true;
component.ngOnChanges();
fixture.detectChanges();
const valueChips = fixture.debugElement.queryAll(By.css(`mat-chip`));
expect(valueChips).not.toBeNull();
expect(valueChips.length).toBe(3);
expect(valueChips[0].nativeElement.innerText.trim()).toBe('item1');
expect(valueChips[1].nativeElement.innerText.trim()).toBe('item2');
expect(valueChips[2].nativeElement.innerText.trim()).toBe('item3');
});
it('should render string for multivalue properties when chips are disabled', () => {
component.property = new CardViewTextItemModel({
label: 'Text label',
value: ['item1', 'item2', 'item3'],
key: 'textkey',
default: ['FAKE-DEFAULT-KEY'],
editable: true,
multivalued: true
});
component.useChipsForMultiValueProperty = false;
component.ngOnChanges();
fixture.detectChanges();
const valueChips = fixture.debugElement.query(By.css(`mat-chip-list`));
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('item1,item2,item3');
expect(valueChips).toBeNull();
});
});
describe('clickable', () => {

View File

@ -20,6 +20,7 @@ import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { AppConfigService } from '../../../app-config/app-config.service';
import { BaseCardView } from '../base-card-view';
import { MatChipInputEvent } from '@angular/material';
@Component({
selector: 'adf-card-view-textitem',
@ -29,6 +30,7 @@ import { BaseCardView } from '../base-card-view';
export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemModel> implements OnChanges {
static DEFAULT_SEPARATOR = ', ';
static DEFAULT_USE_CHIPS = false;
@Input()
editable: boolean = false;
@ -40,18 +42,20 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
private editorInput: any;
inEdit: boolean = false;
editedValue: string;
editedValue: string | string[];
errorMessages: string[];
valueSeparator: string;
useChipsForMultiValueProperty: boolean;
constructor(cardViewUpdateService: CardViewUpdateService,
private appConfig: AppConfigService) {
super(cardViewUpdateService);
this.valueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator') || CardViewTextItemComponent.DEFAULT_SEPARATOR;
this.useChipsForMultiValueProperty = this.appConfig.get<boolean>('content-metadata.multi-value-chips') || CardViewTextItemComponent.DEFAULT_USE_CHIPS;
}
ngOnChanges(): void {
this.editedValue = this.property.multiline ? this.property.displayValue : this.property.value;
this.resetValue();
}
showProperty(): boolean {
@ -87,19 +91,27 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
}, 0);
}
reset(event: MouseEvent|KeyboardEvent): void {
reset(event: Event): void {
event.stopPropagation();
this.editedValue = this.property.multiline ? this.property.displayValue : this.property.value;
this.resetValue();
this.setEditMode(false);
this.resetErrorMessages();
}
resetValue() {
if (this.isChipViewEnabled) {
this.editedValue = this.property.value ? Array.from(this.property.value) : [];
} else {
this.editedValue = this.property.multiline ? this.property.displayValue : this.property.value;
}
}
private resetErrorMessages() {
this.errorMessages = [];
}
update(event: MouseEvent|KeyboardEvent): void {
update(event: Event): void {
event.stopPropagation();
if (this.property.isValid(this.editedValue)) {
@ -113,14 +125,35 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
}
}
prepareValueForUpload(property: CardViewTextItemModel, value: string): string | string [] {
if (property.multivalued) {
prepareValueForUpload(property: CardViewTextItemModel, value: string | string[]): string | string[] {
if (property.multivalued && typeof value === 'string') {
const listOfValues = value.split(this.valueSeparator.trim()).map((item) => item.trim());
return listOfValues;
}
return value;
}
removeValueFromList(itemIndex: number) {
if (typeof this.editedValue !== 'string') {
this.editedValue.splice(itemIndex, 1);
}
}
addValueToList(newListItem: MatChipInputEvent) {
const chipInput = newListItem.input;
const chipValue = newListItem.value.trim() || '';
if (typeof this.editedValue !== 'string') {
if (chipValue) {
this.editedValue.push(chipValue);
}
if (chipInput) {
chipInput.value = '';
}
}
}
onTextAreaInputChange() {
this.errorMessages = this.property.getValidationErrors(this.editedValue);
}
@ -132,4 +165,8 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
this.cardViewUpdateService.clicked(this.property);
}
}
get isChipViewEnabled(): boolean {
return this.property.multivalued && this.useChipsForMultiValueProperty;
}
}

View File

@ -118,14 +118,15 @@ export class DataTableComponentPage {
*
* @param sortOrder: 'ASC' if the list is await expected to be sorted ascending and 'DESC' for descending
* @param columnTitle: titleColumn column
* @param listType: 'string' for string typed lists and 'number' for number typed (int, float) lists
* @return 'true' if the list is sorted as await expected and 'false' if it isn't
*/
async checkListIsSorted(sortOrder: string, columnTitle: string): Promise<any> {
async checkListIsSorted(sortOrder: string, columnTitle: string, listType: string = 'STRING'): Promise<any> {
const column = element.all(by.css(`div.adf-datatable-cell[title='${columnTitle}'] span`));
await BrowserVisibility.waitUntilElementIsVisible(column.first());
const initialList = [];
const length = await column.count();
const length = await column.count();
for (let i = 0; i < length; i++) {
const text = await BrowserActions.getText(column.get(i));
@ -135,7 +136,12 @@ export class DataTableComponentPage {
}
let sortedList = [...initialList];
sortedList = sortedList.sort();
if (listType.toLocaleLowerCase() === 'string') {
sortedList = sortedList.sort();
} else if (listType.toLocaleLowerCase() === 'number') {
sortedList = sortedList.sort((a, b) => a - b);
}
if (sortOrder.toLocaleLowerCase() === 'desc') {
sortedList = sortedList.reverse();
}