[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": "*" "exif:exif": "*"
} }
}, },
"multi-value-pipe-separator": ", " "multi-value-pipe-separator": ", ",
"multi-value-chips": true
}, },
"sideNav": { "sideNav": {
"expandedSidenav": true, "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"] "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().setSortFilterDropDown('Priority');
await tasksCloudDemoPage.editTaskFilterCloudComponent().setOrderFilterDropDown('ASC'); 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 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 () => { it('[C307115] Should display tasks sorted by owner when owner is selected from sort dropdown', async () => {

View File

@ -899,26 +899,6 @@
"pattern": "^\\*$", "pattern": "^\\*$",
"description": "Wildcard for every aspect" "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" "$ref": "#/definitions/content-metadata-aspect"
}, },
@ -932,6 +912,10 @@
"multi-value-pipe-separator": { "multi-value-pipe-separator": {
"description": "Content metadata's separator for multi value properties", "description": "Content metadata's separator for multi value properties",
"type": "string" "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"> <div class="adf-property-value">
<span *ngIf="!isEditable()"> <span *ngIf="!isEditable()">
<span *ngIf="!isClickable(); else elseBlock" [attr.data-automation-id]="'card-textitem-value-' + property.key"> <span *ngIf="!isClickable(); else nonClickableTemplate"
<span *ngIf="showProperty()" [attr.data-automation-id]="'card-textitem-value-' + property.key">
[ngClass]="property.multiline?'adf-textitem-multiline':'adf-textitem-scroll'"> <span *ngIf="!isChipViewEnabled; else chipListTemplate">
{{ property.displayValue }}</span> <span *ngIf="showProperty()"
</span> [ngClass]="property.multiline?'adf-textitem-multiline':'adf-textitem-scroll'">
<ng-template #elseBlock> {{ property.displayValue }}
<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> </span>
<button mat-icon-button fxFlex="0 0 auto" *ngIf="showClickableIcon()" </span>
class="adf-textitem-action" </span>
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate" <ng-template #nonClickableTemplate>
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate" <div role="button"
[attr.data-automation-id]="'card-textitem-clickable-icon-' + property.key"> 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> <mat-icon class="adf-textitem-icon">{{property?.icon}}</mat-icon>
</button> </button>
</div> </div>
</ng-template> </ng-template>
</span> </span>
<span *ngIf="isEditable()"> <span *ngIf="isEditable()">
<div *ngIf="!inEdit" role="button" <div *ngIf="!inEdit"
tabindex="0" role="button"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate" tabindex="0"
(click)="setEditMode(true)" [attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate"
(keydown.enter)="setEditMode(true)" (click)="setEditMode(true)"
class="adf-textitem-readonly" (keydown.enter)="setEditMode(true)"
[attr.data-automation-id]="'card-textitem-toggle-' + property.key" class="adf-textitem-readonly"
fxLayout="row" fxLayoutAlign="space-between center"> [attr.data-automation-id]="'card-textitem-toggle-' + property.key"
<span [attr.data-automation-id]="'card-textitem-value-' + property.key"> fxLayout="row"
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span> 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> </span>
<button mat-icon-button fxFlex="0 0 auto" <button mat-icon-button
class="adf-textitem-action" fxFlex="0 0 auto"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate" class="adf-textitem-action"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate" [attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'card-textitem-edit-icon-' + property.key"> [attr.data-automation-id]="'card-textitem-edit-icon-' + property.key">
<mat-icon class="adf-textitem-icon"> create</mat-icon> <mat-icon class="adf-textitem-icon"> create</mat-icon>
</button> </button>
</div> </div>
<div *ngIf="inEdit" class="adf-textitem-editable"> <div *ngIf="inEdit"
class="adf-textitem-editable">
<div class="adf-textitem-editable-controls"> <div class="adf-textitem-editable-controls">
<mat-form-field floatPlaceholder="never" class="adf-input-container"> <mat-form-field floatPlaceholder="never"
<input *ngIf="!property.multiline" #editorInput class="adf-input-container">
<input *ngIf="!isChipViewEnabled && !property.multiline"
#editorInput
(keydown.escape)="reset($event)" (keydown.escape)="reset($event)"
(keydown.enter)="update($event)" (keydown.enter)="update($event)"
matInput matInput
class="adf-input" class="adf-input"
[placeholder]="property.default | translate" [placeholder]="property.default | translate"
[(ngModel)]="editedValue" [(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-editinput-' + property.key"> [attr.data-automation-id]="'card-textitem-editinput-' + property.key">
<textarea *ngIf="property.multiline" #editorInput <textarea *ngIf="!isChipViewEnabled && property.multiline"
matInput #editorInput
matTextareaAutosize matInput
matAutosizeMaxRows="1" matTextareaAutosize
matAutosizeMaxRows="5" matAutosizeMaxRows="1"
class="adf-textarea" matAutosizeMaxRows="5"
[placeholder]="property.default | translate" class="adf-textarea"
[(ngModel)]="editedValue" [placeholder]="property.default | translate"
(input)="onTextAreaInputChange()" [(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea> (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> </mat-form-field>
<button mat-icon-button class="adf-textitem-action" (click)="update($event)" <button mat-icon-button
[attr.aria-label]="'CORE.METADATA.ACTIONS.SAVE' | translate" class="adf-textitem-action"
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate" (click)="update($event)"
[attr.data-automation-id]="'card-textitem-update-' + property.key"> [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> <mat-icon class="adf-textitem-icon">done</mat-icon>
</button> </button>
<button mat-icon-button (click)="reset($event)" class="adf-textitem-action" <button mat-icon-button
[attr.aria-label]="'CORE.METADATA.ACTIONS.CANCEL' | translate" (click)="reset($event)"
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate" class="adf-textitem-action"
[attr.data-automation-id]="'card-textitem-reset-' + property.key"> [attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">
<mat-icon>clear</mat-icon> <mat-icon>clear</mat-icon>
</button> </button>
</div> </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> <ul>
<li *ngFor="let errorMessage of errorMessages">{{ errorMessage | translate }}</li> <li *ngFor="let errorMessage of errorMessages">{{ errorMessage | translate }}</li>
</ul> </ul>
</mat-error> </mat-error>
</div> </div>
</span> </span>
<ng-template #elseEmptyValueBlock> <ng-template #emptyValueTemplate>
<span class="adf-textitem-default-value">{{ property.default | translate }}</span> <span class="adf-textitem-default-value">{{ property.default | translate }}</span>
</ng-template> </ng-template>
<ng-template #chipListTemplate>
<mat-chip-list>
<mat-chip *ngFor="let propertyValue of editedValue">
{{ propertyValue }}
</mat-chip>
</mat-chip-list>
</ng-template>
</div> </div>

View File

@ -23,6 +23,7 @@ import { CardViewTextItemComponent } from './card-view-textitem.component';
import { setupTestBed } from '../../../testing/setup-test-bed'; import { setupTestBed } from '../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../testing/core.testing.module'; import { CoreTestingModule } from '../../../testing/core.testing.module';
import { CardViewItemFloatValidator, CardViewItemIntValidator } from '@alfresco/adf-core'; import { CardViewItemFloatValidator, CardViewItemIntValidator } from '@alfresco/adf-core';
import { MatChipsModule } from '@angular/material';
describe('CardViewTextItemComponent', () => { describe('CardViewTextItemComponent', () => {
@ -31,7 +32,10 @@ describe('CardViewTextItemComponent', () => {
const mouseEvent = new MouseEvent('click'); const mouseEvent = new MouseEvent('click');
setupTestBed({ setupTestBed({
imports: [CoreTestingModule] imports: [
CoreTestingModule,
MatChipsModule
]
}); });
beforeEach(() => { beforeEach(() => {
@ -146,6 +150,48 @@ describe('CardViewTextItemComponent', () => {
const editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); 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'); 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', () => { 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 { CardViewUpdateService } from '../../services/card-view-update.service';
import { AppConfigService } from '../../../app-config/app-config.service'; import { AppConfigService } from '../../../app-config/app-config.service';
import { BaseCardView } from '../base-card-view'; import { BaseCardView } from '../base-card-view';
import { MatChipInputEvent } from '@angular/material';
@Component({ @Component({
selector: 'adf-card-view-textitem', selector: 'adf-card-view-textitem',
@ -29,6 +30,7 @@ import { BaseCardView } from '../base-card-view';
export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemModel> implements OnChanges { export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemModel> implements OnChanges {
static DEFAULT_SEPARATOR = ', '; static DEFAULT_SEPARATOR = ', ';
static DEFAULT_USE_CHIPS = false;
@Input() @Input()
editable: boolean = false; editable: boolean = false;
@ -40,18 +42,20 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
private editorInput: any; private editorInput: any;
inEdit: boolean = false; inEdit: boolean = false;
editedValue: string; editedValue: string | string[];
errorMessages: string[]; errorMessages: string[];
valueSeparator: string; valueSeparator: string;
useChipsForMultiValueProperty: boolean;
constructor(cardViewUpdateService: CardViewUpdateService, constructor(cardViewUpdateService: CardViewUpdateService,
private appConfig: AppConfigService) { private appConfig: AppConfigService) {
super(cardViewUpdateService); super(cardViewUpdateService);
this.valueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator') || CardViewTextItemComponent.DEFAULT_SEPARATOR; 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 { ngOnChanges(): void {
this.editedValue = this.property.multiline ? this.property.displayValue : this.property.value; this.resetValue();
} }
showProperty(): boolean { showProperty(): boolean {
@ -87,19 +91,27 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
}, 0); }, 0);
} }
reset(event: MouseEvent|KeyboardEvent): void { reset(event: Event): void {
event.stopPropagation(); event.stopPropagation();
this.editedValue = this.property.multiline ? this.property.displayValue : this.property.value; this.resetValue();
this.setEditMode(false); this.setEditMode(false);
this.resetErrorMessages(); 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() { private resetErrorMessages() {
this.errorMessages = []; this.errorMessages = [];
} }
update(event: MouseEvent|KeyboardEvent): void { update(event: Event): void {
event.stopPropagation(); event.stopPropagation();
if (this.property.isValid(this.editedValue)) { if (this.property.isValid(this.editedValue)) {
@ -113,14 +125,35 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
} }
} }
prepareValueForUpload(property: CardViewTextItemModel, value: string): string | string [] { prepareValueForUpload(property: CardViewTextItemModel, value: string | string[]): string | string[] {
if (property.multivalued) { if (property.multivalued && typeof value === 'string') {
const listOfValues = value.split(this.valueSeparator.trim()).map((item) => item.trim()); const listOfValues = value.split(this.valueSeparator.trim()).map((item) => item.trim());
return listOfValues; return listOfValues;
} }
return value; 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() { onTextAreaInputChange() {
this.errorMessages = this.property.getValidationErrors(this.editedValue); this.errorMessages = this.property.getValidationErrors(this.editedValue);
} }
@ -132,4 +165,8 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
this.cardViewUpdateService.clicked(this.property); 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 sortOrder: 'ASC' if the list is await expected to be sorted ascending and 'DESC' for descending
* @param columnTitle: titleColumn column * @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 * @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`)); const column = element.all(by.css(`div.adf-datatable-cell[title='${columnTitle}'] span`));
await BrowserVisibility.waitUntilElementIsVisible(column.first()); await BrowserVisibility.waitUntilElementIsVisible(column.first());
const initialList = []; const initialList = [];
const length = await column.count(); const length = await column.count();
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const text = await BrowserActions.getText(column.get(i)); const text = await BrowserActions.getText(column.get(i));
@ -135,7 +136,12 @@ export class DataTableComponentPage {
} }
let sortedList = [...initialList]; 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') { if (sortOrder.toLocaleLowerCase() === 'desc') {
sortedList = sortedList.reverse(); sortedList = sortedList.reverse();
} }