[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,28 @@
<mat-card *ngIf="node">
<mat-card-content>
<adf-content-metadata
[node]="node"
[displayEmpty]="displayEmpty"
[editable]="editable"
[expanded]="expanded"
[preset]="preset">
</adf-content-metadata>
</mat-card-content>
<mat-card-footer class="adf-content-metadata-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
<div>
<button mat-icon-button (click)="toggleEdit()" data-automation-id="mata-data-card-toggle-edit">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button mat-button (click)="toggleExpanded()" data-automation-id="mata-data-card-toggle-expand">
<ng-container *ngIf="!expanded">
<span data-automation-id="mata-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</ng-container>
<ng-container *ngIf="expanded">
<span data-automation-id="mata-data-card-toggle-expand-label">{{ '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

@@ -3,9 +3,18 @@
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.adf-viewer-default-sidebar {
.adf-content-metadata-card {
&-card-footer.mat-card-footer {
.mat-card {
padding: 0;
.mat-card-content {
margin-bottom: 0;
}
}
&-footer.mat-card-footer {
margin: 0;
padding: 8px 12px;
border-top: 1px solid mat-color($foreground, text, 0.07);

View File

@@ -0,0 +1,172 @@
/*!
* @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.
*/
/*tslint:disable: ban*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentMetadataCardComponent } from './content-metadata-card.component';
import { ContentMetadataComponent } from '../content-metadata/content-metadata.component';
import { MatExpansionModule, MatCardModule, MatButtonModule, MatIconModule } from '@angular/material';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { BasicPropertiesService } from '../../services/basic-properties.service';
import { PropertyDescriptorLoaderService } from '../../services/properties-loader.service';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { AspectWhiteListService } from '../../services/aspect-whitelist.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
describe('ContentMetadataCardComponent', () => {
let component: ContentMetadataCardComponent,
fixture: ComponentFixture<ContentMetadataCardComponent>,
node: MinimalNodeEntryEntity,
preset = 'custom-preset';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatExpansionModule,
MatCardModule,
MatButtonModule,
MatIconModule
],
declarations: [
ContentMetadataCardComponent,
ContentMetadataComponent
],
providers: [
ContentMetadataService,
BasicPropertiesService,
PropertyDescriptorLoaderService,
PropertyDescriptorsService,
AspectWhiteListService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataCardComponent);
component = fixture.componentInstance;
node = <MinimalNodeEntryEntity> {
aspectNames: [],
content: {},
properties: {},
createdByUser: {},
modifiedByUser: {}
};
component.node = node;
component.preset = preset;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should have displayEmpty input param as false by default', () => {
expect(component.displayEmpty).toBe(false);
});
it('should pass through the node to the underlying component', () => {
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.node).toBe(node);
});
it('should pass through the preset to the underlying component', () => {
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.preset).toBe(preset);
});
it('should pass through the preset to the underlying component', () => {
component.displayEmpty = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.displayEmpty).toBe(true);
});
it('should pass through the editable to the underlying component', () => {
component.editable = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.editable).toBe(true);
});
it('should pass through the expanded to the underlying component', () => {
component.expanded = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.expanded).toBe(true);
});
it('should not show anything if node is not present', () => {
component.node = undefined;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent));
expect(contentMetadataComponent).toBeNull();
});
it('should toggle editable by clicking on the button', () => {
component.editable = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-edit"]'));
button.triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.editable).toBe(false);
});
it('should toggle expanded by clicking on the button', () => {
component.expanded = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-expand"]'));
button.triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.expanded).toBe(false);
});
it('should have the proper text on button while collapsed', () => {
component.expanded = false;
fixture.detectChanges();
const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION');
});
it('should have the proper text on button while collapsed', () => {
component.expanded = true;
fixture.detectChanges();
const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION');
});
});

View File

@@ -18,19 +18,23 @@
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' }
host: { 'class': 'adf-content-metadata-card' }
})
export class ContentMetadataCardComponent {
@Input()
node: MinimalNodeEntryEntity;
@Input()
displayEmpty: boolean = false;
@Input()
preset: string;
editable: boolean = false;
expanded: boolean = false;
@@ -41,8 +45,4 @@ export class ContentMetadataCardComponent {
toggleExpanded(): void {
this.expanded = !this.expanded;
}
get maxPropertiesToShow(): number {
return this.expanded ? Infinity : PROPERTY_COUNTER_WHILE_COLLAPSED;
}
}

View File

@@ -0,0 +1,43 @@
<div class="adf-metadata-properties">
<mat-accordion displayMode="flat">
<mat-expansion-panel [expanded]="!expanded" [hideToggle]="!expanded">
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'CORE.METADATA.BASIC.HEADER' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
class="adf-metadata-properties-basic"
data-automation-id="adf-metadata-properties-basic"
[properties]="basicProperties$ | async"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="expanded">
<ng-container *ngIf="aspects$ | async; else loading; let aspects">
<div *ngFor="let aspect of aspects" class="adf-metadata-properties-aspect">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{aspect.title}}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
class="adf-node-aspect-properties"
[properties]="aspect.properties"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>
</mat-expansion-panel>
</div>
</ng-container>
<ng-template #loading>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</ng-container>
</mat-accordion>
</div>

View File

@@ -0,0 +1,9 @@
@mixin adf-content-metadata-theme($theme) {
.adf {
&-metadata-properties {
.mat-expansion-panel:not([class*=mat-elevation-z]) {
box-shadow: none;
}
}
}
}

View File

@@ -0,0 +1,241 @@
/*!
* @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.
*/
/*tslint:disable: ban*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { MatExpansionModule, MatButtonModule, MatIconModule } from '@angular/material';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { BasicPropertiesService } from '../../services/basic-properties.service';
import { PropertyDescriptorLoaderService } from '../../services/properties-loader.service';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { AspectWhiteListService } from '../../services/aspect-whitelist.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { CardViewBaseItemModel, CardViewComponent, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Observable } from 'rxjs/Observable';
describe('ContentMetadataComponent', () => {
let component: ContentMetadataComponent,
fixture: ComponentFixture<ContentMetadataComponent>,
node: MinimalNodeEntryEntity,
preset = 'custom-preset';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatExpansionModule,
MatButtonModule,
MatIconModule
],
declarations: [
ContentMetadataComponent
],
providers: [
ContentMetadataService,
BasicPropertiesService,
PropertyDescriptorLoaderService,
PropertyDescriptorsService,
AspectWhiteListService,
AlfrescoApiService,
NodesApiService,
{ provide: LogService, useValue: { error: jasmine.createSpy('error') } }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataComponent);
component = fixture.componentInstance;
node = <MinimalNodeEntryEntity> {
id: 'node-id',
aspectNames: [],
content: {},
properties: {},
createdByUser: {},
modifiedByUser: {}
};
component.node = node;
component.preset = preset;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Default input param values', () => {
it('should have editable input param as false by default', () => {
expect(component.editable).toBe(false);
});
it('should have displayEmpty input param as false by default', () => {
expect(component.displayEmpty).toBe(false);
});
it('should have expanded input param as false by default', () => {
expect(component.expanded).toBe(false);
});
});
describe('Saving', () => {
it('should save the node on itemUpdate', () => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' },
updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService),
nodesApiService: NodesApiService = TestBed.get(NodesApiService);
spyOn(nodesApiService, 'updateNode');
updateService.update(property, 'updated-value');
expect(nodesApiService.updateNode).toHaveBeenCalledWith('node-id', {
'property-key': 'updated-value'
});
});
it('should update the node on successful save', async(() => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' },
updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService),
nodesApiService: NodesApiService = TestBed.get(NodesApiService),
expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return Observable.of(expectedNode);
});
updateService.update(property, 'updated-value');
fixture.whenStable().then(() => {
expect(component.node).toBe(expectedNode);
});
}));
it('should throw error on unsuccessful save', () => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' },
updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService),
nodesApiService: NodesApiService = TestBed.get(NodesApiService),
logService: LogService = TestBed.get(LogService);
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return ErrorObservable.create(new Error('My bad'));
});
updateService.update(property, 'updated-value');
expect(logService.error).toHaveBeenCalledWith(new Error('My bad'));
});
});
describe('Properties loading', () => {
let expectedNode,
contentMetadataService: ContentMetadataService;
beforeEach(() => {
expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
contentMetadataService = TestBed.get(ContentMetadataService);
fixture.detectChanges();
});
it('should load the basic properties on node change', () => {
spyOn(contentMetadataService, 'getBasicProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode);
});
it('should pass through the loaded basic properties to the card view', async(() => {
const expectedProperties = [];
component.expanded = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getBasicProperties').and.callFake(() => {
return Observable.of(expectedProperties);
});
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance;
expect(basicPropertiesComponent.properties).toBe(expectedProperties);
});
}));
it('should pass through the displayEmpty to the card view of basic properties', async(() => {
component.displayEmpty = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(Observable.of([]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance;
expect(basicPropertiesComponent.displayEmpty).toBe(false);
});
}));
it('should load the aspect properties on node change', () => {
spyOn(contentMetadataService, 'getAspectProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getAspectProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
it('should pass through the loaded aspect properties to the card view', async(() => {
const expectedProperties = [];
component.expanded = true;
fixture.detectChanges();
spyOn(contentMetadataService, 'getAspectProperties').and.callFake(() => {
return Observable.of([{ properties: expectedProperties }]);
});
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const firstAspectPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-properties-aspect adf-card-view')).componentInstance;
expect(firstAspectPropertiesComponent.properties).toBe(expectedProperties);
});
}));
it('should pass through the displayEmpty to the card view of aspect properties', async(() => {
component.expanded = true;
component.displayEmpty = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getAspectProperties').and.returnValue(Observable.of([{ properties: [] }]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-properties-aspect adf-card-view')).componentInstance;
expect(basicPropertiesComponent.displayEmpty).toBe(false);
});
}));
});
});

View File

@@ -15,21 +15,20 @@
* limitations under the License.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, Input, OnChanges, OnInit, SimpleChanges, SimpleChange, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { CardViewItem, CardViewUpdateService, FileSizePipe, NodesApiService } from '@alfresco/adf-core';
import { ContentMetadataService } from './content-metadata.service';
import { CardViewItem, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewAspect } from '../../interfaces/content-metadata.interfaces';
@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 ]
providers: [ CardViewUpdateService ]
})
export class ContentMetadataComponent implements OnChanges, OnInit {
@@ -40,43 +39,41 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
editable: boolean = false;
@Input()
maxPropertiesToShow: number = Infinity;
displayEmpty: boolean = false;
properties: CardViewItem[] = [];
@Input()
expanded: boolean = false;
@Input()
preset: string;
basicProperties$: Observable<CardViewItem[]>;
aspects$: Observable<CardViewAspect[]>;
constructor(private contentMetadataService: ContentMetadataService,
private cardViewUpdateService: CardViewUpdateService,
private nodesApi: NodesApiService) {}
private nodesApi: NodesApiService,
private logService: LogService) {}
ngOnInit(): void {
ngOnInit() {
this.cardViewUpdateService.itemUpdated$
.switchMap(this.saveNode.bind(this))
.subscribe(
node => this.node = node,
error => this.handleError(error)
error => this.logService.error(error)
);
}
ngOnChanges(): void {
this.recalculateProperties();
ngOnChanges(changes: SimpleChanges) {
const nodeChange: SimpleChange = changes['node'];
if (nodeChange) {
const node = nodeChange.currentValue;
this.basicProperties$ = this.contentMetadataService.getBasicProperties(node);
this.aspects$ = this.contentMetadataService.getAspectProperties(node, this.preset);
}
}
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

@@ -1,25 +0,0 @@
<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

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

View File

@@ -1,29 +0,0 @@
@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,7 @@
@import './components/content-metadata/content-metadata.component';
@import './components/content-metadata-card/content-metadata-card.component';
@mixin adf-content-metadata-module-theme($theme) {
@include adf-content-metadata-theme($theme);
@include adf-content-metadata-card-theme($theme);
}

View File

@@ -20,10 +20,14 @@ 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';
import { CardViewModule , FileSizePipe } from '@alfresco/adf-core';
import { ContentMetadataComponent } from './components/content-metadata/content-metadata.component';
import { ContentMetadataCardComponent } from './components/content-metadata-card/content-metadata-card.component';
import { PropertyDescriptorsService } from './services/property-descriptors.service';
import { PropertyDescriptorLoaderService } from './services/properties-loader.service';
import { AspectWhiteListService } from './services/aspect-whitelist.service';
import { BasicPropertiesService } from './services/basic-properties.service';
import { ContentMetadataService } from './services/content-metadata.service';
@NgModule({
imports: [
@@ -40,6 +44,14 @@ import { ContentMetadataCardComponent } from './content-metadata-card.component'
declarations: [
ContentMetadataComponent,
ContentMetadataCardComponent
],
providers: [
ContentMetadataService,
PropertyDescriptorsService,
PropertyDescriptorLoaderService,
AspectWhiteListService,
BasicPropertiesService,
FileSizePipe
]
})
export class ContentMetadataModule {}

View File

@@ -0,0 +1,26 @@
/*!
* @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 interface AspectProperty {
name: string;
title: string;
description?: string;
dataType: string;
defaultValue?: any;
mandatory: boolean;
multiValued: boolean;
}

View File

@@ -0,0 +1,25 @@
/*!
* @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 { AspectProperty } from "./aspect-property.interface";
export interface Aspect {
name: string;
title: string;
description: string;
properties: AspectProperty[]
}

View File

@@ -0,0 +1,23 @@
/*!
* @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 { CardViewItem} from '@alfresco/adf-core';
export interface CardViewAspect {
name: string;
properties: CardViewItem[]
}

View File

@@ -0,0 +1,20 @@
/*!
* @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 './aspect-property.interface';
export * from './aspect.interface';
export * from './card-view-aspect.interface';

View File

@@ -15,5 +15,4 @@
* limitations under the License.
*/
export * from './content-metadata.component';
export * from './content-metadata.service';
export * from './components/content-metadata-card/content-metadata-card.component';

View File

@@ -0,0 +1,75 @@
/*!
* @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 { AppConfigService, LogService } from '@alfresco/adf-core';
@Injectable()
export class AspectWhiteListService {
static readonly DEFAULT_PRESET = '*';
static readonly DEFAULT_PRESET_NAME = 'default';
preset: object | string = AspectWhiteListService.DEFAULT_PRESET;
constructor(private appConfigService: AppConfigService,
private logService: LogService) {}
public choosePreset(presetName: string) {
try {
const preset = this.appConfigService.config['content-metadata'].presets[presetName];
if (preset) {
this.preset = preset;
} else if (presetName !== AspectWhiteListService.DEFAULT_PRESET_NAME) {
this.logService.error(`No content-metadata preset for: ${presetName}`);
}
} catch (e) {
this.preset = AspectWhiteListService.DEFAULT_PRESET;
}
}
public isAspectAllowed(aspectName) {
if (this.isEveryAspectAllowed) {
return true;
}
const aspectNames = Object.keys(this.preset);
return aspectNames.indexOf(aspectName) !== -1;
}
public isPropertyAllowed(aspectName, propertyName) {
if (this.isEveryAspectAllowed || this.isEveryPropertyAllowedFor(aspectName)) {
return true;
}
if (this.preset[aspectName]) {
return this.preset[aspectName].indexOf(propertyName) !== -1;
}
return false;
}
private get isEveryAspectAllowed(): boolean {
return typeof this.preset === 'string' && this.preset === AspectWhiteListService.DEFAULT_PRESET;
}
private isEveryPropertyAllowedFor(aspectName): boolean {
const whitedListedProperties = this.preset[aspectName];
return typeof whitedListedProperties === 'string' && whitedListedProperties === AspectWhiteListService.DEFAULT_PRESET;
}
}

View File

@@ -20,7 +20,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { CardViewDateItemModel, CardViewTextItemModel, FileSizePipe } from '@alfresco/adf-core';
@Injectable()
export class ContentMetadataService {
export class BasicPropertiesService {
constructor(private fileSizePipe: FileSizePipe) {}

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 { async, TestBed } from '@angular/core/testing';
import { ContentMetadataService } from './content-metadata.service';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { BasicPropertiesService } from './basic-properties.service';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import {
CardViewTextItemModel,
CardViewDateItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
CardViewBoolItemModel,
CardViewDatetimeItemModel
} from '@alfresco/adf-core';
describe('ContentMetadataService', () => {
let service: ContentMetadataService,
descriptorsService: PropertyDescriptorsService,
aspects: Aspect[],
aspect: Aspect,
aspectProperty: AspectProperty,
node: MinimalNodeEntryEntity;
const dummyPreset = 'default';
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
ContentMetadataService,
BasicPropertiesService,
AspectWhiteListService,
PropertyDescriptorLoaderService,
AlfrescoApiService,
{ provide: LogService, useValue: { error: () => {} }},
PropertyDescriptorsService
]
}).compileComponents();
}));
beforeEach(() => {
service = TestBed.get(ContentMetadataService);
descriptorsService = TestBed.get(PropertyDescriptorsService);
node = <MinimalNodeEntryEntity> { properties: {} };
aspectProperty = {
name: 'FAS:PLAGUE',
title: 'The Faro Plague',
dataType: '',
defaultValue: '',
mandatory: false,
multiValued: false
};
aspect = {
name: 'FAS:FAS',
title: 'Faro Automated Solutions',
description: 'Faro Automated Solutions is an old Earth corporation that manufactured robots in the mid-21st century.',
properties: [aspectProperty]
};
aspects = [];
spyOn(descriptorsService, 'getAspects').and.returnValue(Observable.of(aspects));
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('General transformation', () => {
it('should translate more properties in an aspect properly', () => {
aspect.properties = [{
name: 'FAS:PLAGUE',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultValue',
mandatory: false,
multiValued: false
},
{
name: 'FAS:ALOY',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultValue',
mandatory: false,
multiValued: false
}];
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
expect(cardViewAspect[0].properties.length).toBe(2);
expect(cardViewAspect[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First property should be instance of CardViewTextItemModel');
expect(cardViewAspect[0].properties[1] instanceof CardViewTextItemModel).toBeTruthy('Second property should be instance of CardViewTextItemModel');
});
});
it('should translate every property in every aspect properly', () => {
aspects.push(
Object.assign({}, aspect, {
properties: [{
name: 'FAS:PLAGUE',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
}),
Object.assign({}, aspect, {
properties: [{
name: 'FAS:ALOY',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
})
);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
expect(cardViewAspect.length).toBe(2);
expect(cardViewAspect[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First aspect\'s property should be instance of CardViewTextItemModel');
expect(cardViewAspect[1].properties[0] instanceof CardViewTextItemModel).toBeTruthy('Second aspect\'s property should be instance of CardViewTextItemModel');
});
});
it('should log an error if unrecognised type is found', () => {
const logService = TestBed.get(LogService);
spyOn(logService, 'error').and.stub();
aspectProperty.name = 'FAS:PLAGUE';
aspectProperty.title = 'The Faro Plague';
aspectProperty.dataType = 'daemonic:scorcher';
aspectProperty.defaultValue = 'Daemonic beast';
aspects.push(aspect);
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
});
});
it('should fall back to singleline property type if unrecognised type is found', () => {
aspectProperty.name = 'FAS:PLAGUE';
aspectProperty.title = 'The Faro Plague';
aspectProperty.dataType = 'daemonic:scorcher';
aspectProperty.defaultValue = 'Daemonic beast';
aspects.push(aspect);
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewTextItemModel = <CardViewTextItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
});
});
});
describe('Different types\'s attributes', () => {
ContentMetadataService.RECOGNISED_ECM_TYPES.forEach((dataType) => {
it(`should translate properly the basic attributes of a property for ${dataType}`, () => {
aspectProperty.name = 'prefix:name';
aspectProperty.title = 'title';
aspectProperty.defaultValue = 'default value';
aspectProperty.dataType = dataType;
aspects.push(aspect);
node.properties = { 'prefix:name': null };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property = cardViewAspect[0].properties[0];
expect(property.label).toBe(aspectProperty.title);
expect(property.key).toBe('properties.prefix:name');
expect(property.default).toBe(aspectProperty.defaultValue);
expect(property.editable).toBeTruthy('Property should be editable');
});
});
});
it('should translate properly the multiline and value attributes for d:text', () => {
aspectProperty.dataType = 'd:text';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewTextItemModel = <CardViewTextItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(property.value).toBe('The Chariot Line');
expect(property.multiline).toBeFalsy('Property should be singleline');
});
});
it('should translate properly the multiline and value attributes for d:mltext', () => {
aspectProperty.dataType = 'd:mltext';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewTextItemModel = <CardViewTextItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(property.value).toBe('The Chariot Line');
expect(property.multiline).toBeTruthy('Property should be multiline');
});
});
it('should translate properly the value attribute for d:date', () => {
const expectedValue = new Date().toISOString();
aspectProperty.dataType = 'd:date';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': expectedValue };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewDateItemModel = <CardViewDateItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewDateItemModel).toBeTruthy('Property should be instance of CardViewDateItemModel');
expect(property.value).toBe(expectedValue);
});
});
it('should translate properly the value attribute for d:date', () => {
const expectedValue = new Date().toISOString();
aspectProperty.dataType = 'd:datetime';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': expectedValue };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewDatetimeItemModel = <CardViewDatetimeItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewDatetimeItemModel).toBeTruthy('Property should be instance of CardViewDatetimeItemModel');
expect(property.value).toBe(expectedValue);
});
});
it('should translate properly the value attribute for d:int', () => {
aspectProperty.dataType = 'd:int';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewIntItemModel = <CardViewIntItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(property.value).toBe(1024);
});
});
it('should translate properly the value attribute for d:long', () => {
aspectProperty.dataType = 'd:long';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewIntItemModel = <CardViewIntItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(property.value).toBe(1024);
});
});
it('should translate properly the value attribute for d:float', () => {
aspectProperty.dataType = 'd:float';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024.24' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(property.value).toBe(1024.24);
});
});
it('should translate properly the value attribute for d:double', () => {
aspectProperty.dataType = 'd:double';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024.24' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(property.value).toBe(1024.24);
});
});
it('should translate properly the value attribute for d:boolean', () => {
aspectProperty.dataType = 'd:boolean';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': true };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewBoolItemModel = <CardViewBoolItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewBoolItemModel).toBeTruthy('Property should be instance of CardViewBoolItemModel');
expect(property.value).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,139 @@
/*!
* @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 { PropertyDescriptorsService } from './property-descriptors.service';
import { BasicPropertiesService } from './basic-properties.service';
import {
CardViewItemProperties,
CardViewItem,
CardViewTextItemModel,
CardViewBoolItemModel,
CardViewDateItemModel,
CardViewDatetimeItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService
} from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { AspectProperty, CardViewAspect, Aspect } from '../interfaces/content-metadata.interfaces';
const D_TEXT = 'd:text';
const D_MLTEXT = 'd:mltext';
const D_DATE = 'd:date';
const D_DATETIME = 'd:datetime';
const D_INT = 'd:int';
const D_LONG = 'd:long';
const D_FLOAT = 'd:float';
const D_DOUBLE = 'd:double';
const D_BOOLEAN = 'd:boolean';
@Injectable()
export class ContentMetadataService {
static readonly RECOGNISED_ECM_TYPES = [ D_TEXT, D_MLTEXT, D_DATE, D_DATETIME, D_INT, D_LONG , D_FLOAT, D_DOUBLE, D_BOOLEAN ];
constructor(private basicPropertiesService: BasicPropertiesService,
private propertyDescriptorsService: PropertyDescriptorsService,
private logService: LogService) {}
getBasicProperties(node: MinimalNodeEntryEntity): Observable<CardViewItem[]> {
return Observable.of(this.basicPropertiesService.getBasicProperties(node));
}
getAspectProperties(node: MinimalNodeEntryEntity, preset: string): Observable<CardViewAspect[]> {
return this.propertyDescriptorsService.getAspects(node, preset)
.map(aspects => this.translateAspects(aspects, node.properties));
}
private translateAspects(aspects: Aspect[], nodeProperties): CardViewAspect[] {
return aspects.map(aspect => {
const translatedAspect: any = Object.assign({}, aspect);
translatedAspect.properties = this.translateProperties(aspect.properties, nodeProperties);
return translatedAspect;
});
}
private translateProperties(aspectProperties: AspectProperty[], nodeProperties: any): CardViewItem[] {
return aspectProperties.map(aspectProperty => {
return this.translateProperty(aspectProperty, nodeProperties[aspectProperty.name]);
});
}
private translateProperty(aspectProperty: AspectProperty, nodeProperty: any): CardViewItem {
this.checkECMTypeValidity(aspectProperty.dataType);
let propertyDefinition: CardViewItemProperties = {
label: aspectProperty.title,
value: nodeProperty,
key: this.getAspectPropertyKey(aspectProperty.name),
default: aspectProperty.defaultValue,
editable: true
};
let property;
switch (aspectProperty.dataType) {
case D_MLTEXT:
property = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: true
}));
break;
case D_INT:
case D_LONG:
property = new CardViewIntItemModel(propertyDefinition);
break;
case D_FLOAT:
case D_DOUBLE:
property = new CardViewFloatItemModel(propertyDefinition);
break;
case D_DATE:
property = new CardViewDateItemModel(propertyDefinition);
break;
case D_DATETIME:
property = new CardViewDatetimeItemModel(propertyDefinition);
break;
case D_BOOLEAN:
property = new CardViewBoolItemModel(propertyDefinition);
break;
case D_TEXT:
default:
property = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: false
}));
}
return property;
}
private checkECMTypeValidity(ecmPropertyType) {
if (ContentMetadataService.RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) {
this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`);
}
}
private getAspectPropertyKey(propertyName) {
return `properties.${propertyName}`;
}
}

View File

@@ -0,0 +1,95 @@
/*!
* @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 { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { ClassesApi } from 'alfresco-js-api';
describe('PropertyDescriptorLoaderService', () => {
let aspectProperties: PropertyDescriptorLoaderService,
classesApi: ClassesApi;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
PropertyDescriptorLoaderService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
aspectProperties = TestBed.get(PropertyDescriptorLoaderService);
const alfrescoApiService = TestBed.get(AlfrescoApiService);
classesApi = alfrescoApiService.classesApi;
});
afterEach(() => {
TestBed.resetTestingModule();
});
it('should load the aspects passed by paramter', () => {
spyOn(classesApi, 'getClass');
aspectProperties.load(['exif:exif', 'cm:content', 'custom:custom'])
.subscribe(() => {});
expect(classesApi.getClass).toHaveBeenCalledTimes(3);
expect(classesApi.getClass).toHaveBeenCalledWith('exif_exif');
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
expect(classesApi.getClass).toHaveBeenCalledWith('custom_custom');
});
it('should merge the forked values', (done) => {
const exifResponse = {
name: 'exif:exif',
id: 1,
properties: {
'exif:1': { id: 'exif:1:id', name: 'exif:1' },
'exif:2': { id: 'exif:2:id', name: 'exif:2' }
}
};
const contentResponse = {
name: 'cm:content',
id: 2,
properties: {
'cm:content': { id: 'cm:content:id', name: 'cm:content' }
}
};
const apiResponses = [ exifResponse, contentResponse ];
let counter = 0;
spyOn(classesApi, 'getClass').and.callFake(() => {
return Observable.of(apiResponses[counter++]);
});
aspectProperties.load(['exif:exif', 'cm:content'])
.subscribe({
next: (data) => {
expect(data[0]).toBe(exifResponse);
expect(data[1]).toBe(contentResponse);
},
complete: done
});
});
});

View File

@@ -0,0 +1,36 @@
/*!
* @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 { AlfrescoApiService } from '@alfresco/adf-core';
import { forkJoin } from 'rxjs/observable/forkJoin';
import { Observable } from 'rxjs/Observable';
import { defer } from 'rxjs/observable/defer';
@Injectable()
export class PropertyDescriptorLoaderService {
constructor(private alfrescoApiService: AlfrescoApiService) {}
load(aspects: string[]): Observable<any> {
const aspectFetchStreams = aspects
.map(aspectName => aspectName.replace(':', '_'))
.map(aspectName => defer( () => this.alfrescoApiService.classesApi.getClass(aspectName)) );
return forkJoin(aspectFetchStreams);
}
}

View File

@@ -0,0 +1,209 @@
/*!
* @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 { PropertyDescriptorsService } from './property-descriptors.service';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { AppConfigService, LogService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { AspectProperty } from '../interfaces/content-metadata.interfaces';
describe('PropertyDescriptorsService', () => {
let contentMetadataService: PropertyDescriptorsService,
aspectProperties: PropertyDescriptorLoaderService,
appConfigService: AppConfigService,
logService: LogService,
node: MinimalNodeEntryEntity,
testPresets: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
PropertyDescriptorsService,
PropertyDescriptorLoaderService,
AppConfigService,
AspectWhiteListService,
{ provide: LogService, useValue: { error: () => {} }},
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
contentMetadataService = TestBed.get(PropertyDescriptorsService);
aspectProperties = TestBed.get(PropertyDescriptorLoaderService);
appConfigService = TestBed.get(AppConfigService);
logService = TestBed.get(LogService);
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('getAspects', () => {
beforeEach(() => {
node = <MinimalNodeEntryEntity> { aspectNames: [ 'exif:exif', 'cm:content', 'custom:custom' ] };
testPresets = {};
appConfigService.config['content-metadata'] = {
presets: testPresets
};
});
it('should call the aspect properties loading for the default aspects related to the given node and defined in the app config', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
testPresets.default = { 'exif:exif': [], 'custom:custom': [], 'banana:banana': [] };
contentMetadataService.getAspects(node);
expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'custom:custom']);
});
it('should call the aspect properties loading for the defined aspects related to the given node and defined in the app config', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
testPresets.pink = { 'cm:content': [], 'custom:custom': [] };
contentMetadataService.getAspects(node, 'pink');
expect(aspectProperties.load).toHaveBeenCalledWith(['cm:content', 'custom:custom']);
});
it('should call the aspect properties loading for all the node aspectNames if the "*" widecard is used for the preset', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
testPresets.default = '*';
contentMetadataService.getAspects(node);
expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'cm:content', 'custom:custom']);
});
it('should call the aspect properties loading for all the node aspectNames if there is no preset data defined in the app config', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
spyOn(logService, 'error').and.stub();
appConfigService.config['content-metadata'] = undefined;
contentMetadataService.getAspects(node);
expect(logService.error).not.toHaveBeenCalled();
expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'cm:content', 'custom:custom']);
});
it('should show meaningful error when invalid preset are given', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
spyOn(logService, 'error').and.stub();
testPresets.pink = { 'cm:content': {}, 'custom:custom': {} };
contentMetadataService.getAspects(node, 'blue');
expect(logService.error).toHaveBeenCalledWith('No content-metadata preset for: blue');
});
it('should filter out properties which are not defined in the particular aspect', () => {
spyOn(aspectProperties, 'load').and.callFake(() => {
return Observable.of([
{
name: 'exif:exif',
properties: [
{ name: 'exif:1' },
{ name: 'exif:2' }
]
}
]);
});
testPresets.default = { 'exif:exif': ['exif:2'] };
contentMetadataService.getAspects(node).subscribe({
next: (aspects) => {
expect(aspects[0].name).toBe('exif:exif');
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:2' });
expect(aspects[0].properties).not.toContain(<AspectProperty> { name: 'exif:1' });
}
});
});
it('should accept "*" wildcard for aspect properties', () => {
spyOn(aspectProperties, 'load').and.callFake(() => {
return Observable.of([
{
name: 'exif:exif',
properties: [
{ name: 'exif:1' },
{ name: 'exif:2' }
]
},
{
name: 'custom:custom',
properties: [
{ name: 'custom:1' },
{ name: 'custom:2' }
]
}
]);
});
testPresets.default = {
'exif:exif': '*',
'custom:custom': ['custom:1']
};
contentMetadataService.getAspects(node).subscribe({
next: (aspects) => {
expect(aspects.length).toBe(2);
expect(aspects[0].name).toBe('exif:exif');
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:1' });
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:2' });
expect(aspects[1].name).toBe('custom:custom');
expect(aspects[1].properties).toContain(<AspectProperty> { name: 'custom:1' });
expect(aspects[1].properties).not.toContain(<AspectProperty> { name: 'custom:2' });
}
});
});
it('should filter out aspects which are not present in app config preset', () => {
spyOn(aspectProperties, 'load').and.callFake(() => {
return Observable.of([
{
name: 'exif:exif',
properties: [
{ name: 'exif:1' }
]
}
]);
});
testPresets.default = {
'exif:exif': ['exif:1'],
'banana:banana': ['banana:1']
};
contentMetadataService.getAspects(node).subscribe({
next: (aspects) => {
expect(aspects.length).toBe(1);
expect(aspects[0].name).toBe('exif:exif');
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:1' });
}
});
});
});
});

View File

@@ -0,0 +1,60 @@
/*!
* @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 { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { Observable } from 'rxjs/Observable';
import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces';
@Injectable()
export class PropertyDescriptorsService {
constructor(private aspectWhiteListService: AspectWhiteListService,
private aspectPropertiesService: PropertyDescriptorLoaderService) {}
getAspects(node: MinimalNodeEntryEntity, presetName: string = 'default'): Observable<Aspect[]> {
this.aspectWhiteListService.choosePreset(presetName);
return this.loadAspectDescriptors(node.aspectNames)
.map(this.filterPropertiesByWhitelist.bind(this));
}
private loadAspectDescriptors(aspectsAssignedToNode: string[]): Observable<any> {
const aspectsToLoad = aspectsAssignedToNode
.filter(nodeAspectName => this.aspectWhiteListService.isAspectAllowed(nodeAspectName));
return this.aspectPropertiesService.load(aspectsToLoad);
}
private filterPropertiesByWhitelist(aspectDescriptors): Aspect[] {
return aspectDescriptors.map((aspectDescriptor) => {
return Object.assign({}, aspectDescriptor, {
properties: this.getFilteredPropertiesArray(aspectDescriptor)
});
});
}
private getFilteredPropertiesArray(aspectDescriptor): AspectProperty[] {
const aspectName = aspectDescriptor.name;
return Object.keys(aspectDescriptor.properties)
.map(propertyName => aspectDescriptor.properties[propertyName])
.filter(property => this.aspectWhiteListService.isPropertyAllowed(aspectName, property.name));
}
}

View File

@@ -188,6 +188,7 @@
},
"METADATA": {
"BASIC": {
"HEADER": "Properties",
"NAME": "Name",
"TITLE": "Title",
"DESCRIPTION": "Description",

View File

@@ -29,6 +29,7 @@ import {
MatProgressBarModule,
MatProgressSpinnerModule,
MatRippleModule,
MatExpansionModule,
MatSelectModule
} from '@angular/material';
@@ -46,6 +47,7 @@ export function modules() {
MatRippleModule,
MatMenuModule,
MatOptionModule,
MatExpansionModule,
MatSelectModule
];
}

View File

@@ -12,9 +12,8 @@
@import '../dialogs/folder.dialog';
@import '../content-metadata/content-metadata.component';
@import '../content-metadata/content-metadata-card.component';
@import '../content-node-selector/content-node-selector.component';
@import '../content-metadata/content-metadata.module';
@mixin adf-content-services-theme($theme) {
@include adf-breadcrumb-theme($theme);
@@ -27,7 +26,6 @@
@include adf-search-control-theme($theme);
@include adf-search-autocomplete-theme($theme);
@include adf-dialog-theme($theme);
@include adf-content-metadata-theme($theme);
@include adf-content-metadata-card-theme($theme);
@include adf-content-node-selector-dialog-theme($theme) ;
@include adf-content-metadata-module-theme($theme);
}