[ADF-1841] Content Metadata first iteration (#2666)

* First try

* Small layout changes

* Add pipe support for CardViewTextItemModel

* property service

* Additional stuff

* Make CardViewUpdateService smarter

* Content metadata saving

* Rebase fix

* CardView Style fixes

* Fix core and content-services tests

* Fix CardView text item update UX
This commit is contained in:
Popovics András
2017-11-18 10:43:39 +00:00
committed by Eugenio Romano
parent 15cbd3a316
commit 4b76e6b4a9
32 changed files with 822 additions and 128 deletions

View File

@@ -80,7 +80,7 @@ export class CardViewDateItemComponent implements OnInit {
let momentDate = moment(newDateValue.value, this.SHOW_FORMAT, true);
if (momentDate.isValid()) {
this.valueDate = momentDate;
this.cardViewUpdateService.update(this.property, {[this.property.key]: momentDate.toDate()});
this.cardViewUpdateService.update(this.property, momentDate.toDate());
}
}
}

View File

@@ -11,11 +11,11 @@
</ng-template>
</span>
<span *ngIf="isEditble()">
<div *ngIf="!inEdit" (click)="setEditMode(true)" class="adf-textitem-readonly" [attr.data-automation-id]="'card-textitem-edit-toggle-' + property.key">
<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="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
<mat-icon [attr.data-automation-id]="'card-textitem-edit-icon-' + property.key" class="adf-textitem-icon">create</mat-icon>
<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">
<mat-form-field floatPlaceholder="never" class="adf-input-container">

View File

@@ -1,4 +1,5 @@
@mixin adf-card-view-textitem-theme($theme) {
$foreground: map-get($theme, foreground);
.adf {
&-textitem-icon {
@@ -6,9 +7,9 @@
width: 16px;
height: 16px;
position: relative;
top: 3px;
top: 4px;
padding-left: 8px;
opacity: 0.5;
opacity: 0.3;
}
&-update-icon {
@@ -42,7 +43,7 @@
input:focus,
textarea:focus {
border: 1px solid #EEE;
border: 1px solid mat-color($foreground, text, 0.15);
}
}
@@ -72,13 +73,13 @@
&-textitem-editable .mat-input-element {
font-family: inherit;
position: relative;
padding-top: 3px;
padding-top: 6px;
}
&-textitem-editable .mat-input-element:focus {
padding: 5px;
left: -6px;
top: -6px;
top: 0;
}
&-textitem-editable input.mat-input-element {

View File

@@ -215,4 +215,19 @@ describe('CardViewTextItemComponent', () => {
let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`));
updateInput.triggerEventHandler('click', null);
});
it('should switch back to readonly mode after an update attempt', async(() => {
component.editable = true;
component.property.editable = true;
component.inEdit = true;
component.editedValue = 'updated-value';
fixture.detectChanges();
component.update();
fixture.whenStable().then(() => {
expect(component.property.value).toBe(component.editedValue);
expect(component.inEdit).toBeFalsy();
});
}));
});

View File

@@ -64,7 +64,9 @@ export class CardViewTextItemComponent implements OnChanges {
}
update(): void {
this.cardViewUpdateService.update(this.property, { [this.property.key]: this.editedValue });
this.cardViewUpdateService.update(this.property, this.editedValue );
this.property.value = this.editedValue;
this.setEditMode(false);
}
clicked(): void {

View File

@@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule, MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslateModule } from '@ngx-translate/core';
import { CardViewContentProxyDirective } from './card-view-content-proxy.directive';
@@ -37,6 +38,7 @@ import { CardViewComponent } from './card-view.component';
MatIconModule,
MatButtonModule,
FormsModule,
FlexLayoutModule,
TranslateModule
],
declarations: [

View File

@@ -71,6 +71,20 @@
"BACK": "Back",
"APPLY": "APPLY",
"NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL."
},
"METADATA": {
"BASIC": {
"NAME": "Name",
"TITLE": "Title",
"DESCRIPTION": "Description",
"AUTHOR": "Author",
"MIMETYPE": "Mimetype",
"SIZE": "Size",
"CREATOR": "Creator",
"CREATED_DATE": "Created Date",
"MODIFIER": "Modifier",
"MODIFIED_DATE": "Modified Date"
}
}
},
"LOGIN": {
@@ -146,6 +160,12 @@
"OF": "of"
},
"LOADING": "Loading",
"UNKNOWN_FORMAT": "Couldn't load preview"
"UNKNOWN_FORMAT": "Couldn't load preview",
"SIDEBAR": {
"METADATA": {
"MORE_INFORMATION": "More information",
"LESS_INFORMATION": "Less information"
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*!
* @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 { PipeTransform } from '@angular/core';
import { CardViewTextItemModel, CardViewTextItemProperties } from './card-view-textitem.model';
class TestPipe implements PipeTransform {
transform(value: string, pipeParam: string): string {
const paramPostFix = pipeParam ? `-${pipeParam}` : '';
return `testpiped-${value}${paramPostFix}`;
}
}
describe('CardViewTextItemModel', () => {
let properties: CardViewTextItemProperties;
beforeEach(() => {
properties = {
label: 'Tribe',
value: 'Banuk',
key: 'tribe'
};
});
describe('displayValue', () => {
it('should return the extension if file has it', () => {
const file = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('Banuk');
});
it('should apply a pipe on the value if it is present', () => {
properties.pipes = [
{ pipe: new TestPipe() }
];
const file = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('testpiped-Banuk');
});
it('should apply a pipe on the value with parameters if those are present', () => {
properties.pipes = [
{ pipe: new TestPipe(), params: ['withParams'] }
];
const file = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('testpiped-Banuk-withParams');
});
it('should apply more pipes on the value with parameters if those are present', () => {
const pipe: PipeTransform = new TestPipe();
properties.pipes = [
{ pipe, params: ['1'] },
{ pipe, params: ['2'] },
{ pipe, params: ['3'] }
];
const file = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3');
});
});
});

View File

@@ -23,24 +23,41 @@
* @returns {CardViewTextItemModel} .
*/
import { PipeTransform } from '@angular/core';
import { CardViewItem } from '../interface/card-view-item.interface';
import { DynamicComponentModel } from '../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
export interface CardViewTextItemPipeProperty {
pipe: PipeTransform;
params?: Array<any>;
}
export interface CardViewTextItemProperties extends CardViewItemProperties {
multiline?: boolean;
pipes?: Array<CardViewTextItemPipeProperty>;
}
export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'text';
multiline: boolean;
multiline?: boolean;
pipes?: Array<CardViewTextItemPipeProperty>;
constructor(obj: CardViewTextItemProperties) {
super(obj);
this.multiline = !!obj.multiline ;
this.pipes = obj.pipes || [];
}
get displayValue() {
return this.value;
return this.applyPipes(this.value);
}
private applyPipes(displayValue) {
if (this.pipes.length) {
displayValue = this.pipes.reduce((accumulator, { pipe, params }) => {
return pipe.transform(accumulator, ...params);
}, displayValue);
}
return displayValue;
}
}

View File

@@ -0,0 +1,94 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
import { CardViewUpdateService, transformKeyToObject } from './card-view-update.service';
describe('CardViewUpdateService', () => {
describe('transformKeyToObject', () => {
it('should return the proper constructed value object for "dotless" keys', () => {
const valueObject = transformKeyToObject('property-key', 'property-value');
expect(valueObject).toEqual({
'property-key': 'property-value'
});
});
it('should return the proper constructed value object for dot contained keys', () => {
const valueObject = transformKeyToObject('level:0.level:1.level:2.level:3', 'property-value');
expect(valueObject).toEqual({
'level:0': {
'level:1': {
'level:2': {
'level:3': 'property-value'
}
}
}
});
});
});
describe('Service', () => {
let cardViewUpdateService: CardViewUpdateService;
const property: CardViewBaseItemModel = <CardViewBaseItemModel> {
label: 'property-label',
value: 'property-value',
key: 'property-key',
default: 'property-default',
editable: false,
clickable: false
};
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
CardViewUpdateService
]
}).compileComponents();
}));
beforeEach(() => {
cardViewUpdateService = TestBed.get(CardViewUpdateService);
});
it('should send updated message with proper parameters', async(() => {
cardViewUpdateService.itemUpdated$.subscribe(
( { target, changed } ) => {
expect(target).toBe(property);
expect(changed).toEqual({ 'property-key': 'changed-property-value' });
}
);
cardViewUpdateService.update(property, 'changed-property-value');
}));
it('should send clicked message with proper parameters', async(() => {
cardViewUpdateService.itemClicked$.subscribe(
( { target } ) => {
expect(target).toBe(property);
}
);
cardViewUpdateService.clicked(property);
}));
});
});

View File

@@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable, Subject } from 'rxjs/Rx';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
export interface UpdateNotification {
@@ -28,26 +28,34 @@ export interface ClickNotification {
target: any;
}
export function transformKeyToObject(key: string, value): Object {
const objectLevels: string[] = key.split('.').reverse();
return objectLevels.reduce<{}>((previousValue, currentValue) => {
return { [currentValue]: previousValue};
}, value);
}
@Injectable()
export class CardViewUpdateService {
// Observable sources
private itemUpdatedSource = new Subject<UpdateNotification>();
private itemClickedSource = new Subject<ClickNotification>();
// Observable streams
public itemUpdated$ = this.itemUpdatedSource.asObservable();
public itemUpdated$ = <Observable<UpdateNotification>> this.itemUpdatedSource.asObservable();
public itemClicked$ = <Observable<ClickNotification>> this.itemClickedSource.asObservable();
public itemClicked$: Subject<ClickNotification> = new Subject<ClickNotification>();
update(property: CardViewBaseItemModel, changed: any) {
update(property: CardViewBaseItemModel, newValue: any) {
this.itemUpdatedSource.next({
target: property,
changed
changed: transformKeyToObject(property.key, newValue)
});
}
clicked(property: CardViewBaseItemModel) {
this.itemClicked$.next({
this.itemClickedSource.next({
target: property
});
}

View File

@@ -14,6 +14,7 @@
@import '../login/components/login.component';
@import '../datatable/components/datatable/datatable.component';
@import '../form/components/widgets/form';
@import '../viewer/components/viewer.component';
@mixin adf-core-theme($theme) {
@include adf-form-theme($theme);
@@ -30,6 +31,7 @@
@include adf-userinfo-theme($theme);
@include adf-login-theme($theme);
@include adf-datatable-theme($theme);
@include adf-viewer-theme($theme);
}

View File

@@ -150,10 +150,10 @@
<ng-container *ngIf="showSidebar && sidebarPosition !== 'left'">
<div class="adf-viewer__sidebar adf-viewer__sidebar-right">
<ng-content select="adf-viewer-sidebar"></ng-content>
<ng-container *ngIf="!sidebar">
<!-- todo: default info drawer -->
<ng-container *ngIf="sidebarTemplate">
<ng-container *ngTemplateOutlet="sidebarTemplate;context:sidebarTemplateContext"></ng-container>
</ng-container>
<ng-content *ngIf="!sidebarTemplate" select="adf-viewer-sidebar"></ng-content>
</div>
</ng-container>
</div>

View File

@@ -1,95 +1,99 @@
$adf-viewer-background-color: #f5f5f5;
@mixin adf-viewer-theme($theme) {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$adf-viewer-background-color: mat-color($background, card);
@mixin full-screen() {
width: 100%;
height: 100%;
background-color: $adf-viewer-background-color;
}
.adf-viewer {
&__mimeicon {
vertical-align: middle;
.full-screen {
width: 100%;
height: 100%;
background-color: $adf-viewer-background-color;
}
&-container {
.adf-viewer-layout-content {
@include full-screen();
position: relative;
overflow-y: auto;
overflow-x: hidden;
z-index: 1;
.adf-viewer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
&__mimeicon {
vertical-align: middle;
}
&-container {
.adf-viewer-layout-content {
@extend .full-screen;
position: relative;
overflow-y: auto;
overflow-x: hidden;
z-index: 1;
& > div {
display: flex;
flex-flow: row wrap;
margin: 0 auto;
align-items: stretch;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
& > div {
display: flex;
flex-flow: row wrap;
margin: 0 auto;
align-items: stretch;
}
}
.adf-viewer-layout {
@extend .full-screen;
display: flex;
flex-direction: row;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
.adf-viewer-content {
@extend .full-screen;
flex: 1;
}
}
.adf-viewer-layout {
@include full-screen();
&-overlay-container {
.adf-viewer-content {
position: fixed;
top: 0px;
left: 0px;
z-index: 1000;
}
}
&-inline-container {
@extend .full-screen;
}
&-content-container {
display: flex;
flex-direction: row;
overflow-y: auto;
overflow-x: hidden;
position: relative;
justify-content: center;
}
.adf-viewer-content {
@include full-screen();
flex: 1;
&-unknown-content {
align-items: center;
display: flex;
}
}
&-overlay-container {
.adf-viewer-content {
position: fixed;
top: 0px;
left: 0px;
z-index: 1000;
&__loading-screen {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 85vh;
.mat-spinner {
margin: 0 auto;
}
}
}
&-inline-container {
@include full-screen();
}
&-content-container {
display: flex;
justify-content: center;
}
&-unknown-content {
align-items: center;
display: flex;
}
&__loading-screen {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 85vh;
.mat-spinner {
margin: 0 auto;
&__sidebar {
width: 350px;
display: block;
padding: 0;
background-color: #fafafa;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27);
border-left: 1px solid mat-color($foreground, text, 0.07);
}
}
&__sidebar {
width: 350px;
display: block;
padding: 8px 0;
background-color: #fafafa;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27);
border-left: 1px solid rgba(0, 0, 0, 0.07);
}
}

View File

@@ -23,7 +23,6 @@ import {
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { BaseEvent } from '../../events';
import { AlfrescoApiService, LogService, RenditionsService } from '../../services';
import { ViewerMoreActionsComponent } from './viewer-more-actions.component';
import { ViewerOpenWithComponent } from './viewer-open-with.component';
import { ViewerSidebarComponent } from './viewer-sidebar.component';
@@ -92,6 +91,9 @@ export class ViewerComponent implements OnDestroy, OnChanges {
@Input()
sidebarPosition = 'right';
@Input()
sidebarTemplate: TemplateRef<any> = null;
@Output()
goBack = new EventEmitter<BaseEvent<any>>();
@@ -114,6 +116,7 @@ export class ViewerComponent implements OnDestroy, OnChanges {
downloadUrl: string = null;
fileName = 'document';
isLoading = false;
node: MinimalNodeEntryEntity;
extensionTemplates: { template: TemplateRef<any>, isVisible: boolean }[] = [];
externalExtensions: string[] = [];
@@ -121,6 +124,7 @@ export class ViewerComponent implements OnDestroy, OnChanges {
otherMenu: any;
extension: string;
mimeType: string;
sidebarTemplateContext: { node: MinimalNodeEntryEntity } = { node: null };
private extensions = {
image: ['png', 'jpg', 'jpeg', 'gif', 'bpm'],
@@ -203,6 +207,7 @@ export class ViewerComponent implements OnDestroy, OnChanges {
}
this.extensionChange.emit(this.extension);
this.sidebarTemplateContext.node = data;
this.scrollTop();
resolve();
},