[ADF-2232] Content metadata layout oriented configuration (#2918)

* Small refactoring I.

* Small refactoring II.

* On the way of implementing the soultion

* Refactoring aspect to groups and supporting different type of configs

* Fixed linter errors

* Fix debug project runner

* Fix linting errors

* Fix and align tests

* Config factory tests

* Layout oriented config parser

* Adding layout oriented config to the config factory

* Update config schema

* Layout oriented config

* Aspect oriented and indifferent configs alignment to the new propertyGroups structure

* Remove dead codes

* Fixinfinite loading error and custom type properties

* Add documentation

* Fix tests
This commit is contained in:
Popovics András
2018-02-13 13:57:45 +00:00
committed by Eugenio Romano
parent a863631f0d
commit c109b9f6f3
36 changed files with 1760 additions and 891 deletions

View File

@@ -25,10 +25,10 @@ import { ContentMetadataComponent } from '../content-metadata/content-metadata.c
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 { PropertyGroupTranslatorService } from '../../services/property-groups-translator.service';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { AspectWhiteListService } from '../../services/aspect-whitelist.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { ContentMetadataConfigFactory } from '../../services/config/content-metadata-config.factory';
describe('ContentMetadataCardComponent', () => {
@@ -52,9 +52,9 @@ describe('ContentMetadataCardComponent', () => {
providers: [
ContentMetadataService,
BasicPropertiesService,
PropertyDescriptorLoaderService,
PropertyGroupTranslatorService,
ContentMetadataConfigFactory,
PropertyDescriptorsService,
AspectWhiteListService,
AlfrescoApiService
]
}).compileComponents();
@@ -65,6 +65,7 @@ describe('ContentMetadataCardComponent', () => {
component = fixture.componentInstance;
node = <MinimalNodeEntryEntity> {
aspectNames: [],
nodeType: '',
content: {},
properties: {},
createdByUser: {},

View File

@@ -8,8 +8,6 @@
</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">
@@ -17,18 +15,17 @@
</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">
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties" class="adf-metadata-grouped-properties-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{aspect.title}}
{{group.title}}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
class="adf-node-aspect-properties"
[properties]="aspect.properties"
[properties]="group.properties"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>

View File

@@ -25,13 +25,13 @@ 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 { PropertyGroupTranslatorService } from '../../services/property-groups-translator.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';
import { ContentMetadataConfigFactory } from '../../services/config/content-metadata-config.factory';
describe('ContentMetadataComponent', () => {
@@ -53,9 +53,9 @@ describe('ContentMetadataComponent', () => {
providers: [
ContentMetadataService,
BasicPropertiesService,
PropertyDescriptorLoaderService,
PropertyGroupTranslatorService,
PropertyDescriptorsService,
AspectWhiteListService,
ContentMetadataConfigFactory,
AlfrescoApiService,
NodesApiService,
{ provide: LogService, useValue: { error: jasmine.createSpy('error') } }
@@ -69,6 +69,7 @@ describe('ContentMetadataComponent', () => {
node = <MinimalNodeEntryEntity> {
id: 'node-id',
aspectNames: [],
nodeType: '',
content: {},
properties: {},
createdByUser: {},
@@ -198,19 +199,19 @@ describe('ContentMetadataComponent', () => {
});
}));
it('should load the aspect properties on node change', () => {
spyOn(contentMetadataService, 'getAspectProperties');
it('should load the group properties on node change', () => {
spyOn(contentMetadataService, 'getGroupedProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getAspectProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
it('should pass through the loaded aspect properties to the card view', async(() => {
it('should pass through the loaded group properties to the card view', async(() => {
const expectedProperties = [];
component.expanded = true;
fixture.detectChanges();
spyOn(contentMetadataService, 'getAspectProperties').and.callFake(() => {
spyOn(contentMetadataService, 'getGroupedProperties').and.callFake(() => {
return Observable.of([{ properties: expectedProperties }]);
});
@@ -218,22 +219,22 @@ describe('ContentMetadataComponent', () => {
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);
const firstGroupedPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container adf-card-view')).componentInstance;
expect(firstGroupedPropertiesComponent.properties).toBe(expectedProperties);
});
}));
it('should pass through the displayEmpty to the card view of aspect properties', async(() => {
it('should pass through the displayEmpty to the card view of grouped properties', async(() => {
component.expanded = true;
component.displayEmpty = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getAspectProperties').and.returnValue(Observable.of([{ properties: [] }]));
spyOn(contentMetadataService, 'getGroupedProperties').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;
const basicPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container adf-card-view')).componentInstance;
expect(basicPropertiesComponent.displayEmpty).toBe(false);
});
}));

View File

@@ -20,7 +20,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { CardViewItem, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewAspect } from '../../interfaces/content-metadata.interfaces';
import { CardViewGroup } from '../../interfaces/content-metadata.interfaces';
@Component({
selector: 'adf-content-metadata',
@@ -49,7 +49,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
nodeHasBeenUpdated: boolean = false;
basicProperties$: Observable<CardViewItem[]>;
aspects$: Observable<CardViewAspect[]>;
groupedProperties$: Observable<CardViewGroup[]>;
constructor(private contentMetadataService: ContentMetadataService,
private cardViewUpdateService: CardViewUpdateService,
@@ -75,7 +75,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
this.nodeHasBeenUpdated = false;
this.basicProperties$ = this.contentMetadataService.getBasicProperties(node);
this.aspects$ = this.contentMetadataService.getAspectProperties(node, this.preset);
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
}
}

View File

@@ -24,10 +24,10 @@ 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 { ContentMetadataConfigFactory } from './services/config/content-metadata-config.factory';
import { BasicPropertiesService } from './services/basic-properties.service';
import { ContentMetadataService } from './services/content-metadata.service';
import { PropertyGroupTranslatorService } from './services/property-groups-translator.service';
@NgModule({
imports: [
@@ -48,9 +48,9 @@ import { ContentMetadataService } from './services/content-metadata.service';
providers: [
ContentMetadataService,
PropertyDescriptorsService,
PropertyDescriptorLoaderService,
AspectWhiteListService,
ContentMetadataConfigFactory,
BasicPropertiesService,
PropertyGroupTranslatorService,
FileSizePipe
]
})

View File

@@ -15,9 +15,6 @@
* limitations under the License.
*/
import { CardViewItem} from '@alfresco/adf-core';
export interface CardViewAspect {
name: string;
properties: CardViewItem[]
export declare interface AspectOrientedConfig {
[key: string]: string | string[];
}

View File

@@ -15,11 +15,9 @@
* limitations under the License.
*/
import { AspectProperty } from "./aspect-property.interface";
import { CardViewItem } from '@alfresco/adf-core';
export interface Aspect {
name: string;
export interface CardViewGroup {
title: string;
description: string;
properties: AspectProperty[]
properties: CardViewItem[];
}

View File

@@ -0,0 +1,24 @@
/*!
* @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 { PropertyGroupContainer } from './property-group.interface';
import { OrganisedPropertyGroup } from './organised-property-group.interface';
export interface ContentMetadataConfig {
isGroupAllowed(groupname: string): boolean;
reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[];
}

View File

@@ -15,6 +15,12 @@
* limitations under the License.
*/
export * from './aspect-property.interface';
export * from './aspect.interface';
export * from './card-view-aspect.interface';
export * from './aspect-oriented-config.interface';
export * from './property.interface';
export * from './property-group.interface';
export * from './organised-property-group.interface';
export * from './card-view-group.interface';
export * from './content-metadata-config.interface';
export * from './indifferent-config.interface';
export * from './layout-oriented-config.interface';
export * from './preset-config.interface';

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 declare type InDifferentConfig = '*';

View File

@@ -0,0 +1,29 @@
/*!
* @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 LayoutOrientedConfigItem {
aspect?: string;
type?: string;
properties: string | string[];
}
export interface LayoutOrientedConfigLayoutBlock {
title: string;
items: LayoutOrientedConfigItem[];
}
export interface LayoutOrientedConfig extends Array<LayoutOrientedConfigLayoutBlock> {}

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 { Property } from './property.interface';
export interface OrganisedPropertyGroup {
title: string;
properties: Property[];
}

View File

@@ -0,0 +1,22 @@
/*!
* @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 { InDifferentConfig } from './indifferent-config.interface';
import { AspectOrientedConfig } from './aspect-oriented-config.interface';
import { LayoutOrientedConfig } from './layout-oriented-config.interface';
export declare type PresetConfig = InDifferentConfig | AspectOrientedConfig | LayoutOrientedConfig;

View File

@@ -0,0 +1,31 @@
/*!
* @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 { Property } from './property.interface';
export interface PropertyGroup {
name: string;
title: string;
description?: string;
properties: {
[key: string]: Property
};
}
export interface PropertyGroupContainer {
[key: string]: PropertyGroup;
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
export interface AspectProperty {
export interface Property {
name: string;
title: string;
description?: string;

View File

@@ -1,75 +0,0 @@
/*!
* @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

@@ -24,7 +24,7 @@ export class BasicPropertiesService {
constructor(private fileSizePipe: FileSizePipe) {}
getBasicProperties(node: MinimalNodeEntryEntity) {
getProperties(node: MinimalNodeEntryEntity) {
return [
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.NAME',

View File

@@ -0,0 +1,178 @@
/*!
* @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 { AspectOrientedConfigService } from './aspect-oriented-config.service';
import { AspectOrientedConfig, Property, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
describe('AspectOrientedConfigService', () => {
let configService: AspectOrientedConfigService;
function createConfigService(configObj: AspectOrientedConfig) {
return new AspectOrientedConfigService(configObj);
}
describe('reorganiseByConfig', () => {
interface TestCase {
name: string;
config: AspectOrientedConfig;
expectations: OrganisedPropertyGroup[];
}
const property1 = <Property> { name: 'property1' },
property2 = <Property> { name: 'property2' },
property3 = <Property> { name: 'property3' },
property4 = <Property> { name: 'property4' };
const propertyGroups: PropertyGroupContainer = {
berseria: { title: 'Berseria', description: '', name: 'berseria', properties: { property1, property2 } },
zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } }
};
const testCases: TestCase[] = [
{
name: 'Empty config',
config: {},
expectations: []
},
{
name: 'One property from One group',
config: {
'berseria': [ 'property1' ]
},
expectations: [{
title: 'Berseria',
properties: [ property1 ]
}]
},
{
name: 'More properties from One group',
config: {
'berseria': [ 'property1', 'property2' ]
},
expectations: [{
title: 'Berseria',
properties: [ property1, property2 ]
}]
},
{
name: 'One-one properties from More group',
config: {
'berseria': [ 'property1' ],
'zestiria': [ 'property3' ]
},
expectations: [
{
title: 'Berseria',
properties: [ property1 ]
},
{
title: 'Zestiria',
properties: [ property3 ]
}
]
},
{
name: 'More properties from More groups',
config: {
'zestiria': [ 'property4', 'property3' ],
'berseria': [ 'property2', 'property1' ]
},
expectations: [
{
title: 'Zestiria',
properties: [ property4, property3 ]
},
{
title: 'Berseria',
properties: [ property2, property1 ]
}
]
},
{
name: 'Wildcard',
config: {
'berseria': '*',
'zestiria': [ 'property4' ]
},
expectations: [
{
title: 'Berseria',
properties: [ property1, property2 ]
},
{
title: 'Zestiria',
properties: [ property4 ]
}
]
},
{
name: 'Not existing group',
config: {
'berseria': '*',
'not-existing-group': '*',
'zestiria': [ 'property4' ]
},
expectations: [
{
title: 'Berseria',
properties: [ property1, property2 ]
},
{
title: 'Zestiria',
properties: [ property4 ]
}
]
},
{
name: 'Not existing property',
config: {
'berseria': [ 'not-existing-property' ],
'zestiria': [ 'property4' ]
},
expectations: [
{
title: 'Zestiria',
properties: [ property4 ]
}
]
}
];
testCases.forEach((testCase) => {
it(`should pass for: ${testCase.name}`, () => {
configService = createConfigService(testCase.config);
const organisedPropertyGroups = configService.reorganiseByConfig(propertyGroups);
expect(organisedPropertyGroups.length).toBe(testCase.expectations.length, 'Group count should match');
testCase.expectations.forEach((expectation, i) => {
expect(organisedPropertyGroups[i].title).toBe(expectation.title, 'Group\'s title should match' );
expect(organisedPropertyGroups[i].properties.length).toBe(
expectation.properties.length,
`Property count for "${organisedPropertyGroups[i].title}" group should match.`
);
expectation.properties.forEach((property, j) => {
expect(organisedPropertyGroups[i].properties[j]).toBe(property, `Property should match ${property.name}`);
});
});
});
});
});
});

View File

@@ -0,0 +1,65 @@
/*!
* @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 { ContentMetadataConfig, AspectOrientedConfig, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
import { getGroup, getProperty } from './property-group-reader';
@Injectable()
export class AspectOrientedConfigService implements ContentMetadataConfig {
constructor(private config: AspectOrientedConfig) {}
public isGroupAllowed(groupName: string): boolean {
const groupNames = Object.keys(this.config);
return groupNames.indexOf(groupName) !== -1;
}
public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const aspects = this.config,
aspectNames = Object.keys(aspects);
return aspectNames
.reduce((groupAccumulator, aspectName) => {
const newGroup = this.getOrganisedPropertyGroup(propertyGroups, aspectName);
return groupAccumulator.concat(newGroup);
}, [])
.filter(organisedPropertyGroup => organisedPropertyGroup.properties.length > 0);
}
private getOrganisedPropertyGroup(propertyGroups, aspectName) {
const group = getGroup(propertyGroups, aspectName);
let newGroup = [];
if (group) {
const aspectProperties = this.config[aspectName];
let properties;
if (aspectProperties === '*') {
properties = getProperty(propertyGroups, aspectName, aspectProperties);
} else {
properties = (<string[]> aspectProperties)
.map((propertyName) => getProperty(propertyGroups, aspectName, propertyName))
.filter(props => props !== undefined);
}
newGroup = [ { title: group.title, properties } ];
}
return newGroup;
}
}

View File

@@ -0,0 +1,117 @@
/*!
* @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 { AppConfigService, LogService } from '@alfresco/adf-core';
import { IndifferentConfigService } from './indifferent-config.service';
import { AspectOrientedConfigService } from './aspect-oriented-config.service';
import { LayoutOrientedConfigService } from './layout-oriented-config.service';
import { ContentMetadataConfigFactory } from './content-metadata-config.factory';
import { ContentMetadataConfig } from '../../interfaces/content-metadata.interfaces';
describe('ContentMetadataConfigFactory', () => {
let factory: ContentMetadataConfigFactory,
appConfig: AppConfigService,
config: ContentMetadataConfig;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
ContentMetadataConfigFactory,
AppConfigService,
{ provide: LogService, useValue: { error: () => {} }}
]
}).compileComponents();
}));
beforeEach(() => {
factory = TestBed.get(ContentMetadataConfigFactory);
appConfig = TestBed.get(AppConfigService);
});
function setConfig(presetName, presetConfig) {
appConfig.config['content-metadata'] = {
presets: {
[presetName]: presetConfig
}
};
}
afterEach(() => {
TestBed.resetTestingModule();
});
describe('get', () => {
let logService;
beforeEach(() => {
logService = TestBed.get(LogService);
spyOn(logService, 'error').and.stub();
});
it('should get back to default preset if no preset is provided as parameter', () => {
config = factory.get();
expect(config).toEqual(jasmine.any(IndifferentConfigService));
});
it('should get back to default preset if no preset is set', () => {
config = factory.get('default');
expect(config).toEqual(jasmine.any(IndifferentConfigService));
expect(logService.error).not.toHaveBeenCalled();
});
it('should get back to the default preset if the requested preset does not exist', () => {
config = factory.get('not-existing-preset');
expect(config).toEqual(jasmine.any(IndifferentConfigService));
});
it('should log an error message if the requested preset does not exist', () => {
config = factory.get('not-existing-preset');
expect(logService.error).toHaveBeenCalledWith('No content-metadata preset for: not-existing-preset');
});
it('should get back the IndifferentConfigService preset if the preset config is indifferent', () => {
setConfig('default', '*');
config = factory.get('default');
expect(config).toEqual(jasmine.any(IndifferentConfigService));
});
it('should get back the AspectOrientedConfigService preset if the preset config is aspect oriented', () => {
setConfig('default', { 'exif:exif' : '*'});
config = factory.get('default');
expect(config).toEqual(jasmine.any(AspectOrientedConfigService));
});
it('should get back the LayoutOrientedConfigService preset if the preset config is layout oriented', () => {
setConfig('default', []);
config = factory.get('default');
expect(config).toEqual(jasmine.any(LayoutOrientedConfigService));
});
});
});

View File

@@ -0,0 +1,79 @@
/*!
* @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';
import { AspectOrientedConfigService } from './aspect-oriented-config.service';
import { IndifferentConfigService } from './indifferent-config.service';
import { LayoutOrientedConfigService } from './layout-oriented-config.service';
import {
PresetConfig,
ContentMetadataConfig,
AspectOrientedConfig,
InDifferentConfig,
LayoutOrientedConfig
} from '../../interfaces/content-metadata.interfaces';
@Injectable()
export class ContentMetadataConfigFactory {
static readonly INDIFFERENT_PRESET = '*';
static readonly DEFAULT_PRESET_NAME = 'default';
constructor(private appConfigService: AppConfigService, private logService: LogService) {}
public get(presetName: string = 'default'): ContentMetadataConfig {
let presetConfig;
try {
presetConfig = this.appConfigService.config['content-metadata'].presets[presetName];
} catch {
if (presetName !== ContentMetadataConfigFactory.DEFAULT_PRESET_NAME) {
this.logService.error(`No content-metadata preset for: ${presetName}`);
}
presetConfig = ContentMetadataConfigFactory.INDIFFERENT_PRESET;
}
return this.createConfig(presetConfig);
}
private createConfig(presetConfig: PresetConfig): ContentMetadataConfig {
let config: ContentMetadataConfig;
if (this.isLayoutOrientedPreset(presetConfig)) {
config = new LayoutOrientedConfigService(<LayoutOrientedConfig> presetConfig);
} else if (this.isAspectOrientedPreset(presetConfig)) {
config = new AspectOrientedConfigService(<AspectOrientedConfig> presetConfig);
} else {
config = new IndifferentConfigService(<InDifferentConfig> presetConfig);
}
Object.freeze(config);
return config;
}
private isAspectOrientedPreset(presetConfig: PresetConfig): boolean {
return this.isObject(presetConfig);
}
private isLayoutOrientedPreset(presetConfig: PresetConfig): boolean {
return Array.isArray(presetConfig);
}
private isObject(x) {
return x != null && typeof x === 'object';
}
}

View File

@@ -0,0 +1,46 @@
/*!
* @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 {
ContentMetadataConfig,
InDifferentConfig,
OrganisedPropertyGroup,
PropertyGroupContainer
} from '../../interfaces/content-metadata.interfaces';
@Injectable()
export class IndifferentConfigService implements ContentMetadataConfig {
constructor(config: InDifferentConfig) {}
public isGroupAllowed(groupName: string): boolean {
return true;
}
public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
return Object.keys(propertyGroups)
.map((groupName) => {
const propertyGroup = propertyGroups[groupName],
properties = propertyGroup.properties;
return Object.assign({}, propertyGroup, {
properties: Object.keys(properties).map(propertyName => properties[propertyName])
});
});
}
}

View File

@@ -0,0 +1,247 @@
/*!
* @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 { LayoutOrientedConfigService } from './layout-oriented-config.service';
import { LayoutOrientedConfig, Property, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
describe('LayoutOrientedConfigService', () => {
let configService: LayoutOrientedConfigService;
function createConfigService(configObj: LayoutOrientedConfig) {
return new LayoutOrientedConfigService(configObj);
}
describe('isGroupAllowed', () => {
const testCases = [
{
config: [],
expectation: false,
groupNameToQuery: 'berseria'
},
{
config: [{ title: 'Deamons', items: [{ aspect: 'berseria', properties: '*' }] }],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [{ title: 'Deamons', items: [{ type: 'berseria', properties: '*' }] }],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [{ title: 'Deamons', items: [
{ aspect: 'zestiria', properties: '*' }, { aspect: 'berseria', properties: '*' }
]}],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [
{ title: 'Deamons', items: [{ aspect: 'zestiria', properties: '*' }] },
{ title: 'Malakhims', items: [{ aspect: 'berseria', properties: '*' }] }
],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [
{ title: 'Deamons', items: [{ aspect: 'zestiria', properties: '*' }] },
{ title: 'Malakhims', items: [{ type: 'berseria', properties: '*' }] }
],
expectation: false,
groupNameToQuery: 'phantasia'
}
];
testCases.forEach((testCase, index) => {
it(`should return ${testCase.expectation.toString()} for test case index #${index}`, () => {
configService = createConfigService(testCase.config);
const isAllowed = configService.isGroupAllowed(testCase.groupNameToQuery);
expect(isAllowed).toBe(testCase.expectation);
});
});
});
describe('reorganiseByConfig', () => {
interface TestCase {
name: string;
config: LayoutOrientedConfig;
expectations: OrganisedPropertyGroup[];
}
const property1 = <Property> { name: 'property1' },
property2 = <Property> { name: 'property2' },
property3 = <Property> { name: 'property3' },
property4 = <Property> { name: 'property4' };
const propertyGroups: PropertyGroupContainer = {
berseria: { title: 'Berseria', description: '', name: 'berseria', properties: { property1, property2 } },
zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } }
};
const testCases: TestCase[] = [
{
name: 'Empty config',
config: [],
expectations: []
},
{
name: 'First property of a group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'berseria', properties: [ 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property1 ] }
]
},
{
name: 'Second property of a group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'berseria', properties: [ 'property2' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property2 ] }
]
},
{
name: 'More properties from one group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'berseria', properties: [ 'property2', 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property2, property1 ] }
]
},
{
name: 'First property of the second group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: [ 'property4' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property4 ] }
]
},
{
name: 'One-one properties from multiple groups in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: [ 'property4' ] },
{ aspect: 'berseria', properties: [ 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property4, property1 ] }
]
},
{
name: 'Multiple properties mixed from multiple groups in multiple items',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: [ 'property4' ] },
{ type: 'berseria', properties: [ 'property1' ] }
]},
{ title: 'Second group', items: [
{ aspect: 'zestiria', properties: [ 'property3' ] },
{ type: 'berseria', properties: [ 'property2', 'property1' ] },
{ aspect: 'zestiria', properties: [ 'property4' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property4, property1 ] },
{ title: 'Second group', properties: [ property3, property2, property1, property4 ] }
]
},
{
name: 'Multiple properties mixed from multiple groups in multiple items with "*"',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: '*' },
{ type: 'berseria', properties: [ 'property1' ] }
]},
{ title: 'Second group', items: [
{ type: 'berseria', properties: [ 'property2', 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property3, property4, property1 ] },
{ title: 'Second group', properties: [ property2, property1 ] }
]
},
{
name: 'Not existing property',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: '*' },
{ type: 'berseria', properties: [ 'not-existing-property' ] },
{ type: 'berseria', properties: [ 'property2' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property3, property4, property2 ] }
]
},
{
name: 'Not existing group',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: '*' },
{ type: 'not-existing-group', properties: '*' },
{ type: 'berseria', properties: [ 'property2' ] },
{ type: 'not-existing-group', properties: 'not-existing-property' }
]}
],
expectations: [
{ title: 'First group', properties: [ property3, property4, property2 ] }
]
}
];
testCases.forEach((testCase) => {
it(`should pass for: ${testCase.name}`, () => {
configService = createConfigService(testCase.config);
const organisedPropertyGroups = configService.reorganiseByConfig(propertyGroups);
expect(organisedPropertyGroups.length).toBe(testCase.expectations.length, 'Group count should match');
testCase.expectations.forEach((expectation, i) => {
expect(organisedPropertyGroups[i].title).toBe(expectation.title, 'Group\'s title should match' );
expect(organisedPropertyGroups[i].properties.length).toBe(
expectation.properties.length,
`Property count for "${organisedPropertyGroups[i].title}" group should match.`
);
expectation.properties.forEach((property, j) => {
expect(organisedPropertyGroups[i].properties[j]).toBe(property, `Property should match ${property.name}`);
});
});
});
});
});
});

View File

@@ -0,0 +1,74 @@
/*!
* @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 {
ContentMetadataConfig,
LayoutOrientedConfig,
LayoutOrientedConfigItem,
OrganisedPropertyGroup,
PropertyGroupContainer
} from '../../interfaces/content-metadata.interfaces';
import { getProperty } from './property-group-reader';
@Injectable()
export class LayoutOrientedConfigService implements ContentMetadataConfig {
constructor(private config: LayoutOrientedConfig) {}
public isGroupAllowed(groupName: string): boolean {
return this.getMatchingGroups(groupName).length > 0;
}
public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const layoutBlocks = this.config;
return layoutBlocks.map((layoutBlock) => {
const flattenedItems = this.flattenItems(layoutBlock.items),
properties = flattenedItems.reduce((props, explodedItem) => {
const property = getProperty(propertyGroups, explodedItem.groupName, explodedItem.propertyName) || [];
return props.concat(property);
}, []);
return {
title: layoutBlock.title,
properties
};
});
}
private flattenItems(items) {
return items.reduce((accumulator, item) => {
const properties = Array.isArray(item.properties) ? item.properties : [item.properties];
const flattenedProperties = properties.map(propertyName => {
return {
groupName: item.aspect || item.type,
propertyName
};
});
return accumulator.concat(flattenedProperties);
}, []);
}
private getMatchingGroups(groupName: string): LayoutOrientedConfigItem[] {
return this.config
.map(layoutBlock => layoutBlock.items)
.reduce((accumulator, items) => accumulator.concat(items), [])
.filter((item) => item.aspect === groupName || item.type === groupName);
}
}

View File

@@ -0,0 +1,43 @@
/*!
* @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 { PropertyGroup, Property, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
const emptyGroup = {
properties: {}
};
function convertObjectToArray(object: any): Property[] {
return Object.keys(object).map(key => object[key]);
}
export function getGroup(propertyGroups: PropertyGroupContainer, groupName: string): PropertyGroup | undefined {
return propertyGroups[groupName];
}
export function getProperty(propertyGroups: PropertyGroupContainer, groupName: string, propertyName: string): Property | Property[] | undefined {
const groupDefinition = getGroup(propertyGroups, groupName) || emptyGroup;
let propertyDefinitions;
if (propertyName === '*') {
propertyDefinitions = convertObjectToArray(groupDefinition.properties);
} else {
propertyDefinitions = groupDefinition.properties[propertyName];
}
return propertyDefinitions;
}

View File

@@ -1,325 +0,0 @@
/*!
* @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

@@ -17,123 +17,44 @@
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';
import { PropertyGroupTranslatorService } from './property-groups-translator.service';
import { CardViewItem } from '@alfresco/adf-core';
import { CardViewGroup } from '../interfaces/content-metadata.interfaces';
import { ContentMetadataConfigFactory } from './config/content-metadata-config.factory';
import { PropertyDescriptorsService } from './property-descriptors.service';
@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) {}
constructor(
private basicPropertiesService: BasicPropertiesService,
private contentMetadataConfigFactory: ContentMetadataConfigFactory,
private propertyGroupTranslatorService: PropertyGroupTranslatorService,
private propertyDescriptorsService: PropertyDescriptorsService
) {}
getBasicProperties(node: MinimalNodeEntryEntity): Observable<CardViewItem[]> {
return Observable.of(this.basicPropertiesService.getBasicProperties(node));
return Observable.of(this.basicPropertiesService.getProperties(node));
}
getAspectProperties(node: MinimalNodeEntryEntity, preset: string): Observable<CardViewAspect[]> {
return this.propertyDescriptorsService.getAspects(node, preset)
.map(aspects => this.translateAspects(aspects, node.properties));
}
getGroupedProperties(node: MinimalNodeEntryEntity, presetName: string = 'default'): Observable<CardViewGroup[]> {
const config = this.contentMetadataConfigFactory.get(presetName),
groupNames = node.aspectNames
.concat(node.nodeType)
.filter(groupName => config.isGroupAllowed(groupName));
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;
});
}
let groupedProperties;
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
}));
if (groupNames.length > 0) {
groupedProperties = this.propertyDescriptorsService.load(groupNames)
.map(groups => config.reorganiseByConfig(groups))
.map(groups => this.propertyGroupTranslatorService.translateToCardViewGroups(groups, node.properties));
} else {
groupedProperties = Observable.of([]);
}
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}`;
return groupedProperties;
}
}

View File

@@ -1,95 +0,0 @@
/*!
* @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

@@ -1,36 +0,0 @@
/*!
* @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

@@ -17,193 +17,80 @@
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';
import { ClassesApi } from 'alfresco-js-api';
import { PropertyGroup } from '../interfaces/content-metadata.interfaces';
describe('PropertyDescriptorsService', () => {
describe('PropertyDescriptorLoaderService', () => {
let contentMetadataService: PropertyDescriptorsService,
aspectProperties: PropertyDescriptorLoaderService,
appConfigService: AppConfigService,
logService: LogService,
node: MinimalNodeEntryEntity,
testPresets: any;
let service: PropertyDescriptorsService,
classesApi: ClassesApi;
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);
service = TestBed.get(PropertyDescriptorsService);
const alfrescoApiService = TestBed.get(AlfrescoApiService);
classesApi = alfrescoApiService.classesApi;
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('getAspects', () => {
it('should load the groups passed by paramter', () => {
spyOn(classesApi, 'getClass');
beforeEach(() => {
node = <MinimalNodeEntryEntity> { aspectNames: [ 'exif:exif', 'cm:content', 'custom:custom' ] };
service.load(['exif:exif', 'cm:content', 'custom:custom'])
.subscribe(() => {});
testPresets = {};
appConfigService.config['content-metadata'] = {
presets: testPresets
};
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: PropertyGroup = {
name: 'exif:exif',
title: '',
properties: {
'exif:1': { title: 'exif:1:id', name: 'exif:1', dataType: '', mandatory: false, multiValued: false },
'exif:2': { title: 'exif:2:id', name: 'exif:2', dataType: '', mandatory: false, multiValued: false }
}
};
const contentResponse: PropertyGroup = {
name: 'cm:content',
title: '',
properties: {
'cm:content': { title: 'cm:content:id', name: 'cm:content', dataType: '', mandatory: false, multiValued: false }
}
};
const apiResponses = [ exifResponse, contentResponse ];
let counter = 0;
spyOn(classesApi, 'getClass').and.callFake(() => {
return Observable.of(apiResponses[counter++]);
});
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' }
]
}
]);
service.load(['exif:exif', 'cm:content'])
.subscribe({
next: (data) => {
expect(data['exif:exif']).toBe(exifResponse);
expect(data['cm:content']).toBe(contentResponse);
},
complete: done
});
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

@@ -16,45 +16,31 @@
*/
import { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { forkJoin } from 'rxjs/observable/forkJoin';
import { Observable } from 'rxjs/Observable';
import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces';
import { defer } from 'rxjs/observable/defer';
import { PropertyGroup, PropertyGroupContainer } from '../interfaces/content-metadata.interfaces';
@Injectable()
export class PropertyDescriptorsService {
constructor(private aspectWhiteListService: AspectWhiteListService,
private aspectPropertiesService: PropertyDescriptorLoaderService) {}
constructor(private alfrescoApiService: AlfrescoApiService) {}
getAspects(node: MinimalNodeEntryEntity, presetName: string = 'default'): Observable<Aspect[]> {
this.aspectWhiteListService.choosePreset(presetName);
load(groupNames: string[]): Observable<PropertyGroupContainer> {
const groupFetchStreams = groupNames
.map(groupName => groupName.replace(':', '_'))
.map(groupName => defer( () => this.alfrescoApiService.classesApi.getClass(groupName)) );
return this.loadAspectDescriptors(node.aspectNames)
.map(this.filterPropertiesByWhitelist.bind(this));
return forkJoin(groupFetchStreams)
.map(this.convertToObject);
}
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 convertToObject(propertyGroupsArray: PropertyGroup[]): PropertyGroupContainer {
return propertyGroupsArray.reduce((propertyGroups, propertyGroup) => {
return Object.assign({}, propertyGroups, {
[propertyGroup.name]: propertyGroup
});
});
}
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

@@ -0,0 +1,285 @@
/*!
* @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 { PropertyGroupTranslatorService } from './property-groups-translator.service';
import { Property, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces';
import {
CardViewTextItemModel,
CardViewDateItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
CardViewBoolItemModel,
CardViewDatetimeItemModel
} from '@alfresco/adf-core';
describe('PropertyGroupTranslatorService', () => {
let service: PropertyGroupTranslatorService,
propertyGroups: OrganisedPropertyGroup[],
propertyGroup: OrganisedPropertyGroup,
property: Property,
propertyValues: { [key: string]: any };
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
PropertyGroupTranslatorService,
{ provide: LogService, useValue: { error: () => {} }}
]
}).compileComponents();
}));
beforeEach(() => {
service = TestBed.get(PropertyGroupTranslatorService);
property = {
name: 'FAS:PLAGUE',
title: 'The Faro Plague',
dataType: '',
defaultValue: '',
mandatory: false,
multiValued: false
};
propertyGroup = {
title: 'Faro Automated Solutions',
properties: [property]
};
propertyGroups = [];
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('General transformation', () => {
it('should translate EVERY properties in ONE group properly', () => {
propertyGroup.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
}];
propertyGroups.push(propertyGroup);
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
expect(cardViewGroup[0].properties.length).toBe(2);
expect(cardViewGroup[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First property should be instance of CardViewTextItemModel');
expect(cardViewGroup[0].properties[1] instanceof CardViewTextItemModel).toBeTruthy('Second property should be instance of CardViewTextItemModel');
});
it('should translate EVERY property in EVERY group properly', () => {
propertyGroups.push(
Object.assign({}, propertyGroup, {
properties: [{
name: 'FAS:PLAGUE',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
}),
Object.assign({}, propertyGroup, {
properties: [{
name: 'FAS:ALOY',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
})
);
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
expect(cardViewGroup.length).toBe(2);
expect(cardViewGroup[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First group\'s property should be instance of CardViewTextItemModel');
expect(cardViewGroup[1].properties[0] instanceof CardViewTextItemModel).toBeTruthy('Second group\'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();
property.name = 'FAS:PLAGUE';
property.title = 'The Faro Plague';
property.dataType = 'daemonic:scorcher';
property.defaultValue = 'Daemonic beast';
propertyGroups.push(propertyGroup);
service.translateToCardViewGroups(propertyGroups, propertyValues);
expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
});
it('should fall back to singleline property type if unrecognised type is found', () => {
property.name = 'FAS:PLAGUE';
property.title = 'The Faro Plague';
property.dataType = 'daemonic:scorcher';
property.defaultValue = 'Daemonic beast';
propertyGroups.push(propertyGroup);
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewTextItemModel = <CardViewTextItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
});
});
describe('Different types\'s attributes', () => {
beforeEach(() => {
propertyGroups.push(propertyGroup);
});
PropertyGroupTranslatorService.RECOGNISED_ECM_TYPES.forEach((dataType) => {
it(`should translate properly the basic attributes of a property for ${dataType}`, () => {
property.name = 'prefix:name';
property.title = 'title';
property.defaultValue = 'default value';
property.dataType = dataType;
propertyValues = { 'prefix:name': null };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty = cardViewGroup[0].properties[0];
expect(cardViewProperty.label).toBe(property.title);
expect(cardViewProperty.key).toBe('properties.prefix:name');
expect(cardViewProperty.default).toBe(property.defaultValue);
expect(cardViewProperty.editable).toBeTruthy('Property should be editable');
});
});
it('should translate properly the multiline and value attributes for d:text', () => {
property.dataType = 'd:text';
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewTextItemModel = <CardViewTextItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(cardViewProperty.value).toBe('The Chariot Line');
expect(cardViewProperty.multiline).toBeFalsy('Property should be singleline');
});
it('should translate properly the multiline and value attributes for d:mltext', () => {
property.dataType = 'd:mltext';
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewTextItemModel = <CardViewTextItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(cardViewProperty.value).toBe('The Chariot Line');
expect(cardViewProperty.multiline).toBeTruthy('Property should be multiline');
});
it('should translate properly the value attribute for d:date', () => {
const expectedValue = new Date().toISOString();
property.dataType = 'd:date';
propertyValues = { 'FAS:PLAGUE': expectedValue };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewDateItemModel = <CardViewDateItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewDateItemModel).toBeTruthy('Property should be instance of CardViewDateItemModel');
expect(cardViewProperty.value).toBe(expectedValue);
});
it('should translate properly the value attribute for d:datetime', () => {
const expectedValue = new Date().toISOString();
property.dataType = 'd:datetime';
propertyValues = { 'FAS:PLAGUE': expectedValue };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewDatetimeItemModel = <CardViewDatetimeItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewDatetimeItemModel).toBeTruthy('Property should be instance of CardViewDatetimeItemModel');
expect(cardViewProperty.value).toBe(expectedValue);
});
it('should translate properly the value attribute for d:int', () => {
property.dataType = 'd:int';
propertyValues = { 'FAS:PLAGUE': '1024' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewIntItemModel = <CardViewIntItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(cardViewProperty.value).toBe(1024);
});
it('should translate properly the value attribute for d:long', () => {
property.dataType = 'd:long';
propertyValues = { 'FAS:PLAGUE': '1024' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewIntItemModel = <CardViewIntItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(cardViewProperty.value).toBe(1024);
});
it('should translate properly the value attribute for d:float', () => {
property.dataType = 'd:float';
propertyValues = { 'FAS:PLAGUE': '1024.24' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(cardViewProperty.value).toBe(1024.24);
});
it('should translate properly the value attribute for d:double', () => {
property.dataType = 'd:double';
propertyValues = { 'FAS:PLAGUE': '1024.24' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(cardViewProperty.value).toBe(1024.24);
});
it('should translate properly the value attribute for d:boolean', () => {
property.dataType = 'd:boolean';
propertyValues = { 'FAS:PLAGUE': true };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewBoolItemModel = <CardViewBoolItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewBoolItemModel).toBeTruthy('Property should be instance of CardViewBoolItemModel');
expect(cardViewProperty.value).toBe(true);
});
});
});

View File

@@ -0,0 +1,122 @@
/*!
* @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 {
CardViewItemProperties,
CardViewItem,
CardViewTextItemModel,
CardViewBoolItemModel,
CardViewDateItemModel,
CardViewDatetimeItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService
} from '@alfresco/adf-core';
import { Property, CardViewGroup, OrganisedPropertyGroup } 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 PropertyGroupTranslatorService {
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 logService: LogService) {}
public translateToCardViewGroups(propertyGroups: OrganisedPropertyGroup[], propertyValues): CardViewGroup[] {
return propertyGroups.map(propertyGroup => {
const translatedPropertyGroup: any = Object.assign({}, propertyGroup);
translatedPropertyGroup.properties = this.translateArray(propertyGroup.properties, propertyValues);
return translatedPropertyGroup;
});
}
private translateArray(properties: Property[], propertyValues: any): CardViewItem[] {
return properties.map(property => {
return this.translate(property, propertyValues[property.name]);
});
}
private translate(property: Property, propertyValue: any): CardViewItem {
this.checkECMTypeValidity(property.dataType);
const prefix = 'properties.';
let propertyDefinition: CardViewItemProperties = {
label: property.title,
value: propertyValue,
key: `${prefix}${property.name}`,
default: property.defaultValue,
editable: true
};
let cardViewItemProperty;
switch (property.dataType) {
case D_MLTEXT:
cardViewItemProperty = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: true
}));
break;
case D_INT:
case D_LONG:
cardViewItemProperty = new CardViewIntItemModel(propertyDefinition);
break;
case D_FLOAT:
case D_DOUBLE:
cardViewItemProperty = new CardViewFloatItemModel(propertyDefinition);
break;
case D_DATE:
cardViewItemProperty = new CardViewDateItemModel(propertyDefinition);
break;
case D_DATETIME:
cardViewItemProperty = new CardViewDatetimeItemModel(propertyDefinition);
break;
case D_BOOLEAN:
cardViewItemProperty = new CardViewBoolItemModel(propertyDefinition);
break;
case D_TEXT:
default:
cardViewItemProperty = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: false
}));
}
return cardViewItemProperty;
}
private checkECMTypeValidity(ecmPropertyType) {
if (PropertyGroupTranslatorService.RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) {
this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`);
}
}
}

View File

@@ -158,7 +158,105 @@
]
}
}
}
},
"content-metadata-layout-group": {
"description": "Content metadata's layout groups definition",
"type": "array",
"items": [
{
"description": "Content metadata's one layout group definition",
"type": "object",
"required": [
"title",
"items"
],
"properties": {
"title": {
"type": "string",
"description": "Content metadata's one layout group definition's title"
},
"items": {
"type": "array",
"description": "Content metadata's one layout group definition's items",
"items": {
"oneOf": [
{
"type": "object",
"required": [
"aspect",
"properties"
],
"properties": {
"aspect": {
"description": "Aspect group",
"type": "string"
},
"properties": {
"description": "Wildcard character",
"type": "string",
"pattern": "^\\*$"
}
}
},
{
"type": "object",
"required": [
"aspect",
"properties"
],
"properties": {
"aspect": {
"description": "Aspect group",
"type": "string"
},
"properties": {
"description": "list of aspect properties",
"type": "array"
}
}
},
{
"type": "object",
"required": [
"type",
"properties"
],
"properties": {
"type": {
"description": "Type group",
"type": "string"
},
"properties": {
"description": "Wildcard character",
"type": "string",
"pattern": "^\\*$"
}
}
},
{
"type": "object",
"required": [
"type",
"properties"
],
"properties": {
"type": {
"description": "Type group",
"type": "string"
},
"properties": {
"description": "list of type properties",
"type": "array"
}
}
}
]
}
}
}
}
]
}
},
"type": "object",
@@ -326,7 +424,8 @@
".*": {
"oneOf": [
{ "type": "string", "pattern": "^\\*$", "description": "Wildcard for every aspect"},
{ "$ref": "#/definitions/content-metadata-aspect" }
{ "$ref": "#/definitions/content-metadata-aspect" },
{ "$ref": "#/definitions/content-metadata-layout-group" }
]
}
}