[ADF-1986] Content matadata editing phase II. (#2796)

* Aspects collection

* Fetch only those metadata aspects which are defined in the application config

* Aspect property filtering first round

* Addig wildcard support for preset, default preset fallback to wildcard, and logging

* Add white list service

* Renaming services

* ContentMetadataService and CardViewItemDispatcherComponent update

* Observables... Observables everywhere...

* Propers CardViewAspect

* Defining more interfaces

* Dummy data and expansions panels

* Fix build attempt & proper panel expansion

* Folder restructuring

* Add different type mappings

* Restructuring Card view component

* Fix build

* More ECM types supported

* Validators first phase, extraction of interfaces, world domination preparation

* Validators phase II.

* Integer and float validators

* Hide empty text items and validation message foundation

* Validation error messages for text item view, small style changes

* Update date item

* bool item component

* Datetimepicker npm module

* Datetime model

* Add mapping for datetime

* Small fixes

* Passing down preset

* Adding forgotten package.json entry

* Adding some tests for wrapper card component

* content-metadata.component tests

* Covering some edge cases, adding some tests

* Fix cardview component's test

* Add datetimepicker to demoshell

* card view component show empty values by default

* displayEmpty dependency injection

* remove table like design from cardview

* Using alfresco-js-api instead of spike

* Remove spike folder and contents

* Fix test

* Cardview updated docs

* Content metadata md

* Fix review issues

* Fix the packagr issue
This commit is contained in:
Popovics András
2018-01-11 12:31:22 +00:00
committed by Eugenio Romano
parent 994041fb23
commit 783f7f0497
106 changed files with 3816 additions and 724 deletions

View File

@@ -0,0 +1,58 @@
<div 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()">{{ property.displayValue }}</span>
</span>
<ng-template #elseBlock>
<span class="adf-textitem-clickable-value" (click)="clicked()" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
</ng-template>
</span>
<span *ngIf="isEditable()">
<div *ngIf="!inEdit" (click)="setEditMode(true)" class="adf-textitem-readonly" [attr.data-automation-id]="'card-textitem-edit-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>
</span>
<mat-icon fxFlex="0 0 auto" [attr.data-automation-id]="'card-textitem-edit-icon-' + property.key" class="adf-textitem-icon">create</mat-icon>
</div>
<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
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"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea>
</mat-form-field>
<mat-icon
class="adf-textitem-icon adf-update-icon"
(click)="update()"
[attr.data-automation-id]="'card-textitem-update-' + property.key">done</mat-icon>
<mat-icon
class="adf-textitem-icon adf-reset-icon"
(click)="reset()"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">clear</mat-icon>
</div>
<mat-error 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>
<span class="adf-textitem-default-value">{{ property.default | translate }}</span>
</ng-template>
</div>

View File

@@ -0,0 +1,116 @@
@mixin adf-card-view-textitem-theme($theme) {
$foreground: map-get($theme, foreground);
.adf {
&-textitem-icon {
font-size: 16px;
width: 16px;
height: 16px;
position: relative;
top: 4px;
padding-left: 8px;
opacity: 0.3;
}
&-update-icon {
padding-left: 13px;
}
&-textitem-readonly {
cursor: pointer;
&:hover mat-icon {
opacity: 1;
}
}
&-textitem-clickable-value {
cursor: pointer;
color: mat-color($primary);
}
&-textitem-editable {
&-controls {
display: flex;
mat-icon:hover {
opacity: 1;
cursor: pointer;
}
mat-form-field {
width: 100%;
}
input:focus,
textarea:focus {
border: 1px solid mat-color($foreground, text, 0.15);
}
}
&-error {
font-size: 12px;
padding-top: 4px;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
margin: 0;
padding: 0;
}
}
}
}
&-textitem-default-value {
color: mat-color($foreground, text, 0.54);
}
&-textitem-editable .mat-input-wrapper {
margin: 0;
padding-bottom: 0;
}
&-textitem-editable .mat-input-underline {
display: none;
}
&-textitem-editable .mat-input-infix {
padding: 0;
border-top: none;
}
&-textitem-editable .mat-input-placeholder-wrapper {
padding-top: 2em;
position: static;
}
&-textitem-editable .mat-input-placeholder {
top: 4px;
}
&-textitem-editable .mat-input-element {
font-family: inherit;
position: relative;
padding-top: 6px;
}
&-textitem-editable .mat-input-element:focus {
padding: 5px;
left: -6px;
top: 0;
}
&-textitem-editable input.mat-input-element {
margin-bottom: 2px;
}
&-textitem-editable input.mat-input-element:focus {
margin-bottom: -8px;
}
}
}

View File

@@ -0,0 +1,325 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { AppConfigService } from '../../../app-config/app-config.service';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { LogService } from '../../../services/log.service';
import { TranslateLoaderService } from '../../../services/translate-loader.service';
import { CardViewTextItemComponent } from './card-view-textitem.component';
describe('CardViewTextItemComponent', () => {
let fixture: ComponentFixture<CardViewTextItemComponent>;
let component: CardViewTextItemComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
FormsModule,
NoopAnimationsModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatNativeDateModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderService
}
})
],
declarations: [
CardViewTextItemComponent
],
providers: [
AppConfigService,
CardViewUpdateService,
LogService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewTextItemComponent);
component = fixture.componentInstance;
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: 'Lorem ipsum',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Rendering', () => {
it('should render the label and value', () => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('Text label');
let 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('Lorem ipsum');
});
it('should NOT render the default as value if the value is empty, editable is false and displayEmpty is false', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
component.displayEmpty = false;
fixture.detectChanges();
let 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('');
});
it('should render the default as value if the value is empty, editable is false and displayEmpty is true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
component.displayEmpty = true;
fixture.detectChanges();
let 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('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and editable true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: true
});
component.editable = true;
fixture.detectChanges();
let 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('FAKE-DEFAULT-KEY');
});
it('should NOT render the default as value if the value is empty, clickable is false and displayEmpty is false', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: false
});
component.displayEmpty = false;
fixture.detectChanges();
let 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('');
});
it('should render the default as value if the value is empty, clickable is false and displayEmpty is true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: false
});
component.displayEmpty = true;
fixture.detectChanges();
let 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('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and clickable true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: true
});
fixture.detectChanges();
let 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('FAKE-DEFAULT-KEY');
});
it('should render value when editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let 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('Lorem ipsum');
});
it('should render the edit icon in case of editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
expect(editIcon).not.toBeNull('Edit icon should be shown');
});
it('should NOT render the edit icon in case of editable:false', () => {
component.editable = false;
fixture.detectChanges();
let 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 NOT render the picker and toggle in case of editable:true but (general) editable:false', () => {
component.editable = false;
component.property.editable = true;
fixture.detectChanges();
let 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');
});
});
describe('Update', () => {
beforeEach(() => {
component.editable = true;
component.property.editable = true;
component.inEdit = true;
component.editedValue = 'updated-value';
fixture.detectChanges();
});
it('should call the isValid method with the edited value', () => {
spyOn(component.property, 'isValid');
component.editedValue = 'updated-value';
component.update();
expect(component.property.isValid).toHaveBeenCalledWith('updated-value');
});
it('should trigger the update event if the editedValue is valid', () => {
component.property.isValid = () => true;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
spyOn(cardViewUpdateService, 'update');
component.editedValue = 'updated-value';
component.update();
expect(cardViewUpdateService.update).toHaveBeenCalledWith(component.property, 'updated-value');
});
it('should NOT trigger the update event if the editedValue is invalid', () => {
component.property.isValid = () => false;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
spyOn(cardViewUpdateService, 'update');
component.update();
expect(cardViewUpdateService.update).not.toHaveBeenCalled();
});
it('should set the errorMessages properly if the editedValue is invalid', () => {
const expectedErrorMessages = ['Something went wrong'];
component.property.isValid = () => false;
component.property.getValidationErrors = () => expectedErrorMessages;
component.update();
expect(component.errorMessages).toBe(expectedErrorMessages);
});
it('should update the propery\'s value after a succesful update attempt', async(() => {
component.property.isValid = () => true;
component.update();
fixture.whenStable().then(() => {
expect(component.property.value).toBe(component.editedValue);
});
}));
it('should switch back to readonly mode after an update attempt', async(() => {
component.property.isValid = () => true;
component.update();
fixture.whenStable().then(() => {
expect(component.inEdit).toBeFalsy();
});
}));
it('should trigger an update event on the CardViewUpdateService [integration]', (done) => {
component.inEdit = false;
component.property.isValid = () => true;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
const expectedText = 'changed text';
fixture.detectChanges();
cardViewUpdateService.itemUpdated$.subscribe(
(updateNotification) => {
expect(updateNotification.target).toBe(component.property);
expect(updateNotification.changed).toEqual({ textkey: expectedText });
done();
}
);
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-toggle-${component.property.key}"]`));
editIcon.triggerEventHandler('click', null);
fixture.detectChanges();
let editInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-editinput-${component.property.key}"]`));
editInput.nativeElement.value = expectedText;
editInput.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`));
updateInput.triggerEventHandler('click', null);
});
});
});

View File

@@ -0,0 +1,93 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, OnChanges, ViewChild } from '@angular/core';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
@Component({
selector: 'adf-card-view-textitem',
templateUrl: './card-view-textitem.component.html',
styleUrls: ['./card-view-textitem.component.scss']
})
export class CardViewTextItemComponent implements OnChanges {
@Input()
property: CardViewTextItemModel;
@Input()
editable: boolean = false;
@Input()
displayEmpty: boolean = true;
@ViewChild('editorInput')
private editorInput: any;
inEdit: boolean = false;
editedValue: string;
errorMessages: string[];
constructor(private cardViewUpdateService: CardViewUpdateService) {}
ngOnChanges(): void {
this.editedValue = this.property.value;
}
showProperty(): boolean {
return this.displayEmpty || !this.property.isEmpty();
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
isClickable(): boolean {
return this.property.clickable;
}
hasErrors(): number {
return this.errorMessages && this.errorMessages.length;
}
setEditMode(editStatus: boolean): void {
this.inEdit = editStatus;
setTimeout(() => {
if (this.editorInput) {
this.editorInput.nativeElement.click();
}
}, 0);
}
reset(): void {
this.editedValue = this.property.value;
this.setEditMode(false);
}
update(): void {
if (this.property.isValid(this.editedValue)) {
this.cardViewUpdateService.update(this.property, this.editedValue );
this.property.value = this.editedValue;
this.setEditMode(false);
} else {
this.errorMessages = this.property.getValidationErrors(this.editedValue);
}
}
clicked(): void {
this.cardViewUpdateService.clicked(this.property);
}
}