[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

@ -1,6 +1,22 @@
<ng-container *ngIf="nodeId"> <ng-container *ngIf="nodeId">
<adf-viewer [fileNodeId]="nodeId" [allowSidebar]="true">
<ng-template let-node="node" #sidebarTemplate>
<adf-info-drawer title="Details">
<adf-info-drawer-tab label="Properties">
<adf-content-metadata-card [node]="node"></adf-content-metadata-card>
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Versions">
<mat-card>
<mat-card-content>
Versions go here...
</mat-card-content>
</mat-card>
</adf-info-drawer-tab>
</adf-info-drawer>
</ng-template>
<adf-viewer [fileNodeId]="nodeId" [allowSidebar]="true" [sidebarTemplate]="sidebarTemplate">
<!-- <!--
<adf-viewer-extension [supportedExtensions]="['json']"> <adf-viewer-extension [supportedExtensions]="['json']">
<ng-template let-urlFileContent="urlFileContent" let-extension="extension"> <ng-template let-urlFileContent="urlFileContent" let-extension="extension">

View File

@ -129,6 +129,26 @@ const textItemProperty = new CardViewTextItemModel(options);
| editable | boolean | false | Whether the property editable or not | | editable | boolean | false | Whether the property editable or not |
| clickable | boolean | false | Whether the property clickable or not | | clickable | boolean | false | Whether the property clickable or not |
| multiline | string | false | Single or multiline text | | multiline | string | false | Single or multiline text |
| pipes | CardViewTextItemPipeProperty[] | [] | Pipes to be applied on the displayValue |
##### Using pipes in Card Text Item
You can use pipes for text items almost the same way as you would do it in your template. You can provide an array of pipes with additional pipeParameters like this:
```js
const myWonderfulPipe1: PipeTransform = <whatever PipeTransform implmentation>;
const myWonderfulPipe2: PipeTransform = <whatever PipeTransform implmentation>;
new CardViewTextItemModel({
label: 'some label',
value: someValue,
key: 'some-key',
pipes: [
{ pipe: myWonderfulPipe1, params: ['first-param', 'second-param'] },
{ pipe: myWonderfulPipe2, params: ['first-param', 'second-param'] }
]
});
```
#### Card Map Item #### Card Map Item

View File

@ -0,0 +1,25 @@
<mat-card *ngIf="node">
<mat-card-content>
<adf-content-metadata [node]="node" [editable]="editable" [maxPropertiesToShow]="maxPropertiesToShow"></adf-content-metadata>
</mat-card-content>
<mat-card-footer class="adf-viewer-default-sidebar-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
<div>
<button mat-icon-button>
<mat-icon>star_border</mat-icon>
</button>
<button mat-icon-button (click)="toggleEdit()">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button mat-button (click)="toggleExpanded()">
<ng-container *ngIf="!expanded">
<span>{{ 'ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</ng-container>
<ng-container *ngIf="expanded">
<span>{{ 'ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_up</mat-icon>
</ng-container>
</button>
</mat-card-footer>
</mat-card>

View File

@ -0,0 +1,21 @@
@mixin adf-content-metadata-card-theme($theme) {
$primary: map-get($theme, primary);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.adf-viewer-default-sidebar {
&-card-footer.mat-card-footer {
padding: 8px 12px;
border-top: 1px solid mat-color($foreground, text, 0.07);
button {
color: mat-color($foreground, text, 0.54);
&:hover {
color: mat-color($foreground, text, 0.87);
}
}
}
}
}

View File

@ -0,0 +1,48 @@
/*!
* @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, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
const PROPERTY_COUNTER_WHILE_COLLAPSED = 5;
@Component({
selector: 'adf-content-metadata-card',
templateUrl: './content-metadata-card.component.html',
styleUrls: ['./content-metadata-card.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { 'class': 'adf-viewer-default-sidebar' }
})
export class ContentMetadataCardComponent {
@Input()
node: MinimalNodeEntryEntity;
editable: boolean = false;
expanded: boolean = false;
toggleEdit(): void {
this.editable = !this.editable;
}
toggleExpanded(): void {
this.expanded = !this.expanded;
}
get maxPropertiesToShow(): number {
return this.expanded ? Infinity : PROPERTY_COUNTER_WHILE_COLLAPSED;
}
}

View File

@ -0,0 +1,3 @@
<div class="adf-metadata-properties">
<adf-card-view [properties]="properties" [editable]="editable"></adf-card-view>
</div>

View File

@ -0,0 +1,29 @@
@mixin adf-content-metadata-theme($theme) {
$primary: map-get($theme, primary);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.adf {
&-metadata-properties {
.adf-property {
display: block;
margin-bottom: 20px;
.adf-property-label {
display: block;
padding: 0;
font-size: 12px;
color: mat-color($foreground, text, 0.4);
}
.adf-property-value {
display: block;
padding: 0;
font-size: 14px;
color: mat-color($foreground, text, 0.87);
}
}
}
}
}

View File

@ -0,0 +1,82 @@
/*!
* @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 { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Rx';
import { CardViewItem, CardViewUpdateService, FileSizePipe, NodesApiService } from '@alfresco/adf-core';
import { ContentMetadataService } from './content-metadata.service';
@Component({
selector: 'adf-content-metadata',
templateUrl: './content-metadata.component.html',
styleUrls: ['./content-metadata.component.scss'],
host: { 'class': 'adf-content-metadata' },
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [ CardViewUpdateService ],
viewProviders: [ ContentMetadataService, FileSizePipe ]
})
export class ContentMetadataComponent implements OnChanges, OnInit {
@Input()
node: MinimalNodeEntryEntity;
@Input()
editable: boolean = false;
@Input()
maxPropertiesToShow: number = Infinity;
properties: CardViewItem[] = [];
constructor(private contentMetadataService: ContentMetadataService,
private cardViewUpdateService: CardViewUpdateService,
private nodesApi: NodesApiService) {}
ngOnInit(): void {
this.cardViewUpdateService.itemUpdated$
.switchMap(this.saveNode.bind(this))
.subscribe(
node => this.node = node,
error => this.handleError(error)
);
}
ngOnChanges(): void {
this.recalculateProperties();
}
private saveNode({ changed: nodeBody }): Observable<MinimalNodeEntryEntity> {
return this.nodesApi.updateNode(this.node.id, nodeBody);
}
private handleError(error): void {
/*tslint:disable-next-line*/
console.log(error);
}
private recalculateProperties(): void {
let basicProperties = this.contentMetadataService.getBasicProperties(this.node);
if (this.maxPropertiesToShow) {
basicProperties = basicProperties.slice(0, this.maxPropertiesToShow);
}
this.properties = [...basicProperties];
}
}

View File

@ -0,0 +1,45 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { MaterialModule } from '../material.module';
import { CardViewModule } from '@alfresco/adf-core';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataCardComponent } from './content-metadata-card.component';
@NgModule({
imports: [
CommonModule,
MaterialModule,
TranslateModule,
FlexLayoutModule,
CardViewModule
],
exports: [
ContentMetadataComponent,
ContentMetadataCardComponent
],
declarations: [
ContentMetadataComponent,
ContentMetadataCardComponent
]
})
export class ContentMetadataModule {}

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 { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { CardViewDateItemModel, CardViewTextItemModel, FileSizePipe } from '@alfresco/adf-core';
@Injectable()
export class ContentMetadataService {
constructor(private fileSizePipe: FileSizePipe) {}
getBasicProperties(node: MinimalNodeEntryEntity) {
return [
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.NAME',
value: node.name,
key: 'name',
editable: true
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.TITLE',
value: node.properties['cm:title'],
key: 'properties.cm:title',
editable: true
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.CREATOR',
value: node.createdByUser.displayName,
key: 'createdByUser.displayName',
editable: false
}),
new CardViewDateItemModel({
label: 'CORE.METADATA.BASIC.CREATED_DATE',
value: node.createdAt,
key: 'createdAt',
editable: false
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.SIZE',
value: node.content.sizeInBytes,
key: 'content.sizeInBytes',
pipes: [{ pipe: this.fileSizePipe }],
editable: false
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.MODIFIER',
value: node.modifiedByUser.displayName,
key: 'modifiedByUser.displayName',
editable: false
}),
new CardViewDateItemModel({
label: 'CORE.METADATA.BASIC.MODIFIED_DATE',
value: node.modifiedAt,
key: 'modifiedAt',
editable: false
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.MIMETYPE',
value: node.content.mimeTypeName,
key: 'content.mimeTypeName',
editable: false
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.AUTHOR',
value: node.properties['cm:author'],
key: 'properties.cm:author',
editable: true
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.DESCRIPTION',
value: node.properties['cm:description'],
key: 'properties.cm:description',
multiline: true,
editable: true
})
];
}
}

View File

@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@ -0,0 +1,21 @@
/*!
* @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.
*/
export * from './content-metadata.component';
export * from './content-metadata.service';
export * from './content-metadata.module';

View File

@ -35,6 +35,7 @@ import { VersionManagerModule } from './version-manager';
import { ContentNodeSelectorModule } from './content-node-selector'; import { ContentNodeSelectorModule } from './content-node-selector';
import { DialogModule } from './dialogs'; import { DialogModule } from './dialogs';
import { DirectiveModule } from './directive'; import { DirectiveModule } from './directive';
import { ContentMetadataModule } from './content-metadata';
@NgModule({ @NgModule({
imports: [ imports: [
@ -54,6 +55,7 @@ import { DirectiveModule } from './directive';
BreadcrumbModule, BreadcrumbModule,
VersionManagerModule, VersionManagerModule,
ContentNodeSelectorModule, ContentNodeSelectorModule,
ContentMetadataModule,
DialogModule, DialogModule,
DirectiveModule DirectiveModule
], ],
@ -79,6 +81,7 @@ import { DirectiveModule } from './directive';
BreadcrumbModule, BreadcrumbModule,
VersionManagerModule, VersionManagerModule,
ContentNodeSelectorModule, ContentNodeSelectorModule,
ContentMetadataModule,
DialogModule, DialogModule,
DirectiveModule DirectiveModule
] ]

View File

@ -182,5 +182,19 @@
}, },
"PERMISSON": { "PERMISSON": {
"LACKOF": "You don't have the {{permission}} permission to {{action}} the {{type}}" "LACKOF": "You don't have the {{permission}} permission to {{action}} the {{type}}"
},
"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"
}
} }
} }

View File

@ -27,6 +27,7 @@ export * from './version-manager';
export * from './content-node-selector'; export * from './content-node-selector';
export * from './dialogs'; export * from './dialogs';
export * from './directive'; export * from './directive';
export * from './content-metadata';
export * from './mock'; export * from './mock';

View File

@ -21,6 +21,7 @@ import {
MatChipsModule, MatChipsModule,
MatDialogModule, MatDialogModule,
MatIconModule, MatIconModule,
MatCardModule,
MatInputModule, MatInputModule,
MatListModule, MatListModule,
MatMenuModule, MatMenuModule,
@ -37,6 +38,7 @@ export function modules() {
MatChipsModule, MatChipsModule,
MatDialogModule, MatDialogModule,
MatIconModule, MatIconModule,
MatCardModule,
MatInputModule, MatInputModule,
MatListModule, MatListModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,

View File

@ -11,6 +11,9 @@
@import '../dialogs/folder.dialog'; @import '../dialogs/folder.dialog';
@import '../content-metadata/content-metadata.component';
@import '../content-metadata/content-metadata-card.component';
@mixin adf-content-services-theme($theme) { @mixin adf-content-services-theme($theme) {
@include adf-breadcrumb-theme($theme); @include adf-breadcrumb-theme($theme);
@include adf-breadcrumb-dropdown-theme($theme); @include adf-breadcrumb-dropdown-theme($theme);
@ -21,4 +24,6 @@
@include adf-search-control-theme($theme); @include adf-search-control-theme($theme);
@include adf-search-autocomplete-theme($theme); @include adf-search-autocomplete-theme($theme);
@include adf-dialog-theme($theme); @include adf-dialog-theme($theme);
@include adf-content-metadata-theme($theme);
@include adf-content-metadata-card-theme($theme);
} }

View File

@ -80,7 +80,7 @@ export class CardViewDateItemComponent implements OnInit {
let momentDate = moment(newDateValue.value, this.SHOW_FORMAT, true); let momentDate = moment(newDateValue.value, this.SHOW_FORMAT, true);
if (momentDate.isValid()) { if (momentDate.isValid()) {
this.valueDate = momentDate; 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> </ng-template>
</span> </span>
<span *ngIf="isEditble()"> <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 [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span> <span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</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>
<div *ngIf="inEdit" class="adf-textitem-editable"> <div *ngIf="inEdit" class="adf-textitem-editable">
<mat-form-field floatPlaceholder="never" class="adf-input-container"> <mat-form-field floatPlaceholder="never" class="adf-input-container">

View File

@ -1,4 +1,5 @@
@mixin adf-card-view-textitem-theme($theme) { @mixin adf-card-view-textitem-theme($theme) {
$foreground: map-get($theme, foreground);
.adf { .adf {
&-textitem-icon { &-textitem-icon {
@ -6,9 +7,9 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
position: relative; position: relative;
top: 3px; top: 4px;
padding-left: 8px; padding-left: 8px;
opacity: 0.5; opacity: 0.3;
} }
&-update-icon { &-update-icon {
@ -42,7 +43,7 @@
input:focus, input:focus,
textarea: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 { &-textitem-editable .mat-input-element {
font-family: inherit; font-family: inherit;
position: relative; position: relative;
padding-top: 3px; padding-top: 6px;
} }
&-textitem-editable .mat-input-element:focus { &-textitem-editable .mat-input-element:focus {
padding: 5px; padding: 5px;
left: -6px; left: -6px;
top: -6px; top: 0;
} }
&-textitem-editable input.mat-input-element { &-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}"]`)); let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`));
updateInput.triggerEventHandler('click', null); 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 { 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 { clicked(): void {

View File

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

View File

@ -71,6 +71,20 @@
"BACK": "Back", "BACK": "Back",
"APPLY": "APPLY", "APPLY": "APPLY",
"NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL." "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": { "LOGIN": {
@ -146,6 +160,12 @@
"OF": "of" "OF": "of"
}, },
"LOADING": "Loading", "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} . * @returns {CardViewTextItemModel} .
*/ */
import { PipeTransform } from '@angular/core';
import { CardViewItem } from '../interface/card-view-item.interface'; import { CardViewItem } from '../interface/card-view-item.interface';
import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; import { DynamicComponentModel } from '../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
export interface CardViewTextItemPipeProperty {
pipe: PipeTransform;
params?: Array<any>;
}
export interface CardViewTextItemProperties extends CardViewItemProperties { export interface CardViewTextItemProperties extends CardViewItemProperties {
multiline?: boolean; multiline?: boolean;
pipes?: Array<CardViewTextItemPipeProperty>;
} }
export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'text'; type: string = 'text';
multiline: boolean; multiline?: boolean;
pipes?: Array<CardViewTextItemPipeProperty>;
constructor(obj: CardViewTextItemProperties) { constructor(obj: CardViewTextItemProperties) {
super(obj); super(obj);
this.multiline = !!obj.multiline ; this.multiline = !!obj.multiline ;
this.pipes = obj.pipes || [];
} }
get displayValue() { 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 { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject'; import { Observable, Subject } from 'rxjs/Rx';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model'; import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
export interface UpdateNotification { export interface UpdateNotification {
@ -28,26 +28,34 @@ export interface ClickNotification {
target: any; 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() @Injectable()
export class CardViewUpdateService { export class CardViewUpdateService {
// Observable sources // Observable sources
private itemUpdatedSource = new Subject<UpdateNotification>(); private itemUpdatedSource = new Subject<UpdateNotification>();
private itemClickedSource = new Subject<ClickNotification>();
// Observable streams // 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, newValue: any) {
update(property: CardViewBaseItemModel, changed: any) {
this.itemUpdatedSource.next({ this.itemUpdatedSource.next({
target: property, target: property,
changed changed: transformKeyToObject(property.key, newValue)
}); });
} }
clicked(property: CardViewBaseItemModel) { clicked(property: CardViewBaseItemModel) {
this.itemClicked$.next({ this.itemClickedSource.next({
target: property target: property
}); });
} }

View File

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

View File

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

View File

@ -1,12 +1,15 @@
$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() { .full-screen {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: $adf-viewer-background-color; background-color: $adf-viewer-background-color;
} }
.adf-viewer { .adf-viewer {
&__mimeicon { &__mimeicon {
vertical-align: middle; vertical-align: middle;
@ -14,7 +17,7 @@ $adf-viewer-background-color: #f5f5f5;
&-container { &-container {
.adf-viewer-layout-content { .adf-viewer-layout-content {
@include full-screen(); @extend .full-screen;
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@ -34,7 +37,7 @@ $adf-viewer-background-color: #f5f5f5;
} }
.adf-viewer-layout { .adf-viewer-layout {
@include full-screen(); @extend .full-screen;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -44,7 +47,7 @@ $adf-viewer-background-color: #f5f5f5;
} }
.adf-viewer-content { .adf-viewer-content {
@include full-screen(); @extend .full-screen;
flex: 1; flex: 1;
} }
} }
@ -59,7 +62,7 @@ $adf-viewer-background-color: #f5f5f5;
} }
&-inline-container { &-inline-container {
@include full-screen(); @extend .full-screen;
} }
&-content-container { &-content-container {
@ -87,9 +90,10 @@ $adf-viewer-background-color: #f5f5f5;
&__sidebar { &__sidebar {
width: 350px; width: 350px;
display: block; display: block;
padding: 8px 0; padding: 0;
background-color: #fafafa; background-color: #fafafa;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27); box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27);
border-left: 1px solid rgba(0, 0, 0, 0.07); border-left: 1px solid mat-color($foreground, text, 0.07);
}
} }
} }

View File

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