[ADF-4162] Add includeAll/exclude capabilities to metadata config (#4436)

* [ADF-4162] Add includeAll/exclude capabilities to metadata config

* Revert app config

* Update documentation

* fix schema change
This commit is contained in:
davidcanonieto 2019-03-18 12:45:08 +00:00 committed by Eugenio Romano
parent 791051edee
commit 3e29c7cd9d
17 changed files with 365 additions and 22 deletions

View File

@ -600,7 +600,7 @@
"content-metadata": { "content-metadata": {
"presets": { "presets": {
"default": { "default": {
"exif:exif": "*" "exif:exif": "*"
} }
} }
}, },

View File

@ -130,8 +130,7 @@ A final example shows the same process applied to a custom preset called "kitten
### Layout oriented config ### Layout oriented config
You can also go beyond the aspect oriented configuration if you need to configure the groups and properties in a more detailed way. With this type of configuration any property of any aspect/type You can also go beyond the aspect oriented configuration if you need to configure the groups and properties in a more detailed way. With this type of configuration any property of any aspect/type
can be "cherry picked" and grouped into an accordion drawer, along with a translatable title can be "cherry picked" and grouped into an accordion drawer, along with a translatable title defined in the preset configuration.
defined in the preset configuration.
#### Basic elements #### Basic elements
@ -217,6 +216,80 @@ The result of this config would be two accordion groups with the following prope
| kitten:favourite-food | | kitten:favourite-food |
| kitten:recommended-food | | kitten:recommended-food |
### Displaying all properties
You can list all the properties by simply adding the `includeAll: boolean` to your config. This config will display all the aspects and properties available for that specific file.
```json
"content-metadata": {
"presets": {
"default": {
"includeAll": true
}
}
},
```
Futhermore, you can also exclude specific aspects by adding the `exclude` property. It can be either a string if it's only one aspect or an array if you want to exclude multiple aspects at once:
```json
"content-metadata": {
"presets": {
"default": {
"includeAll": true,
"exclude": "exif:exif"
}
}
},
```
```json
"content-metadata": {
"presets": {
"default": {
"includeAll": true,
"exclude": ["exif:exif", "owner:parameters"]
}
}
},
```
When using this configuration you can still whitelist aspects and properties as you desire. Let's see more complex examples for each of the config layouts:
##### Aspect oriented config
```json
"content-metadata": {
"presets": {
"default": {
"includeAll": true,
"exclude": "exif:exif",
"exif:exif": [ "exif:pixelXDimension", "exif:pixelYDimension"]
}
}
},
```
##### Layout oriented config
```json
"content-metadata": {
"presets": {
"robot-images": [
{
"includeAll": true,
"exclude": ["cm:content", "exif:exif"]
},
{
"title": "Robot Group",
"items": [
{ "aspect": "exif:exif", "properties": [ "exif:pixelXDimension", "exif:pixelYDimension"] }
]
}
]
}
},
```
## What happens when there is a whitelisted aspect in the config but the given node doesn't relate to that aspect ## What happens when there is a whitelisted aspect in the config but the given node doesn't relate to that aspect
Nothing - since this aspect is not related to the node, it will simply be ignored and not Nothing - since this aspect is not related to the node, it will simply be ignored and not

View File

@ -21,7 +21,7 @@
<ng-container *ngIf="expanded"> <ng-container *ngIf="expanded">
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties"> <ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;" class="adf-metadata-grouped-properties-container"> <div *ngFor="let group of groupedProperties; let first = first;" class="adf-metadata-grouped-properties-container">
<mat-expansion-panel <mat-expansion-panel *ngIf="showGroup(group) || editable"
[attr.data-automation-id]="'adf-metadata-group-' + group.title" [attr.data-automation-id]="'adf-metadata-group-' + group.title"
[expanded]="!displayDefaultProperties && first"> [expanded]="!displayDefaultProperties && first">
<mat-expansion-panel-header> <mat-expansion-panel-header>

View File

@ -21,7 +21,10 @@ import { By } from '@angular/platform-browser';
import { Node } from '@alfresco/js-api'; import { Node } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component'; import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service'; import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewBaseItemModel, CardViewComponent, CardViewUpdateService, NodesApiService, LogService, setupTestBed } from '@alfresco/adf-core'; import {
CardViewBaseItemModel, CardViewComponent, CardViewUpdateService, NodesApiService,
LogService, setupTestBed
} from '@alfresco/adf-core';
import { throwError, of } from 'rxjs'; import { throwError, of } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module'; import { ContentTestingModule } from '../../../testing/content.testing.module';
@ -206,6 +209,7 @@ describe('ContentMetadataComponent', () => {
spyOn(contentMetadataService, 'getGroupedProperties').and.callFake(() => { spyOn(contentMetadataService, 'getGroupedProperties').and.callFake(() => {
return of([{ properties: expectedProperties }]); return of([{ properties: expectedProperties }]);
}); });
spyOn(component, 'showGroup').and.returnValue(true);
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@ -221,6 +225,7 @@ describe('ContentMetadataComponent', () => {
component.displayEmpty = false; component.displayEmpty = false;
fixture.detectChanges(); fixture.detectChanges();
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] }])); spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] }]));
spyOn(component, 'showGroup').and.returnValue(true);
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@ -230,6 +235,42 @@ describe('ContentMetadataComponent', () => {
expect(basicPropertiesComponent.displayEmpty).toBe(false); expect(basicPropertiesComponent.displayEmpty).toBe(false);
}); });
})); }));
it('should hide card views group when the grouped properties are empty', async(() => {
component.expanded = true;
fixture.detectChanges();
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] }]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesGroup = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container mat-expansion-panel'));
expect(basicPropertiesGroup).toBeNull();
});
}));
it('should display card views group when there is at least one property that is not empty', async(() => {
component.expanded = true;
fixture.detectChanges();
const cardViewGroup = {title: 'Group 1', properties: [{
data: null,
default: null,
displayValue: 'DefaultName',
icon: '',
key: 'properties.cm:default',
label: 'To'
}]};
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [cardViewGroup] }]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesGroup = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container mat-expansion-panel'));
expect(basicPropertiesGroup).toBeDefined();
});
}));
}); });
describe('Properties displaying', () => { describe('Properties displaying', () => {

View File

@ -106,6 +106,14 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
return this.nodesApiService.updateNode(this.node.id, nodeBody); return this.nodesApiService.updateNode(this.node.id, nodeBody);
} }
showGroup(group: CardViewGroup) {
const properties = group.properties.filter((property) => {
return !!property.displayValue;
});
return properties.length;
}
ngOnDestroy() { ngOnDestroy() {
this.disposableNodeUpdate.unsubscribe(); this.disposableNodeUpdate.unsubscribe();
} }

View File

@ -16,5 +16,5 @@
*/ */
export declare interface AspectOrientedConfig { export declare interface AspectOrientedConfig {
[key: string]: string | string[]; [key: string]: string | string[] | boolean;
} }

View File

@ -21,4 +21,7 @@ import { OrganisedPropertyGroup } from './organised-property-group.interface';
export interface ContentMetadataConfig { export interface ContentMetadataConfig {
isGroupAllowed(groupName: string): boolean; isGroupAllowed(groupName: string): boolean;
reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[]; reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[];
filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[];
appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[];
isIncludeAllEnabled(): boolean;
} }

View File

@ -19,6 +19,8 @@ export interface LayoutOrientedConfigItem {
aspect?: string; aspect?: string;
type?: string; type?: string;
properties: string | string[]; properties: string | string[];
includeAll?: boolean;
exclude?: string | string[];
} }
export interface LayoutOrientedConfigLayoutBlock { export interface LayoutOrientedConfigLayoutBlock {

View File

@ -175,4 +175,52 @@ describe('AspectOrientedConfigService', () => {
}); });
}); });
}); });
describe('appendAllPreset', () => {
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 } }
};
it(`should return all the propertyGorups`, () => {
const testCase = {
name: 'Not existing property',
config: {
includeAll: true
},
expectations: [
{
title: 'Berseria',
properties: [ property1, property2 ]
},
{
title: 'Zestiria',
properties: [ property3, property4 ]
}
]
};
configService = createConfigService(testCase.config);
const organisedPropertyGroups = configService.appendAllPreset(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

@ -20,9 +20,12 @@ import { getGroup, getProperty } from './property-group-reader';
export class AspectOrientedConfigService implements ContentMetadataConfig { export class AspectOrientedConfigService implements ContentMetadataConfig {
constructor(private config: any) {} constructor(private config: any) { }
public isGroupAllowed(groupName: string): boolean { public isGroupAllowed(groupName: string): boolean {
if (this.isIncludeAllEnabled()) {
return true;
}
const groupNames = Object.keys(this.config); const groupNames = Object.keys(this.config);
return groupNames.indexOf(groupName) !== -1; return groupNames.indexOf(groupName) !== -1;
} }
@ -39,6 +42,33 @@ export class AspectOrientedConfigService implements ContentMetadataConfig {
.filter((organisedPropertyGroup) => organisedPropertyGroup.properties.length > 0); .filter((organisedPropertyGroup) => organisedPropertyGroup.properties.length > 0);
} }
public appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const groups = Object.keys(propertyGroups)
.map((groupName) => {
const propertyGroup = propertyGroups[groupName],
properties = propertyGroup.properties;
return Object.assign({}, propertyGroup, {
properties: Object.keys(properties).map((propertyName) => properties[propertyName])
});
});
return groups;
}
public filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
if (this.config.exclude) {
return propertyGroups.filter((preset) => {
return !this.config.exclude.includes(preset.name);
});
}
return propertyGroups;
}
public isIncludeAllEnabled() {
return this.config.includeAll;
}
private getOrganisedPropertyGroup(propertyGroups, aspectName) { private getOrganisedPropertyGroup(propertyGroups, aspectName) {
const group = getGroup(propertyGroups, aspectName); const group = getGroup(propertyGroups, aspectName);
let newGroup = []; let newGroup = [];
@ -55,7 +85,7 @@ export class AspectOrientedConfigService implements ContentMetadataConfig {
.filter((props) => props !== undefined); .filter((props) => props !== undefined);
} }
newGroup = [ { title: group.title, properties } ]; newGroup = [{ title: group.title, properties }];
} }
return newGroup; return newGroup;

View File

@ -92,7 +92,7 @@ describe('ContentMetadataConfigFactory', () => {
})); }));
}); });
xdescribe('set', () => { describe('set', () => {
function setConfig(presetName, presetConfig) { function setConfig(presetName, presetConfig) {
appConfig.config['content-metadata'] = { appConfig.config['content-metadata'] = {

View File

@ -38,4 +38,16 @@ export class IndifferentConfigService implements ContentMetadataConfig {
}); });
}); });
} }
public filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
return propertyGroups;
}
public appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
return[];
}
public isIncludeAllEnabled(): boolean {
return true;
}
} }

View File

@ -66,6 +66,13 @@ describe('LayoutOrientedConfigService', () => {
], ],
expectation: false, expectation: false,
groupNameToQuery: 'phantasia' groupNameToQuery: 'phantasia'
},
{
config: [
{ title: 'Deamons', includeAll: true, items: [{ aspect: 'zestiria', properties: '*' }] }
],
expectation: true,
groupNameToQuery: 'phantasia'
} }
]; ];

View File

@ -25,16 +25,19 @@ import { getProperty } from './property-group-reader';
export class LayoutOrientedConfigService implements ContentMetadataConfig { export class LayoutOrientedConfigService implements ContentMetadataConfig {
constructor(private config: any) {} constructor(private config: any) { }
public isGroupAllowed(groupName: string): boolean { public isGroupAllowed(groupName: string): boolean {
if (this.isIncludeAllEnabled()) {
return true;
}
return this.getMatchingGroups(groupName).length > 0; return this.getMatchingGroups(groupName).length > 0;
} }
public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] { public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const layoutBlocks = this.config; const layoutBlocks = this.config.filter((itemsGroup) => itemsGroup.items);
return layoutBlocks.map((layoutBlock) => { let organisedPropertyGroup = layoutBlocks.map((layoutBlock) => {
const flattenedItems = this.flattenItems(layoutBlock.items), const flattenedItems = this.flattenItems(layoutBlock.items),
properties = flattenedItems.reduce((props, explodedItem) => { properties = flattenedItems.reduce((props, explodedItem) => {
const property = getProperty(propertyGroups, explodedItem.groupName, explodedItem.propertyName) || []; const property = getProperty(propertyGroups, explodedItem.groupName, explodedItem.propertyName) || [];
@ -46,6 +49,44 @@ export class LayoutOrientedConfigService implements ContentMetadataConfig {
properties properties
}; };
}); });
return organisedPropertyGroup;
}
public appendAllPreset(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])
});
});
}
public filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
let excludedConfig = this.config
.map((config) => config.exclude)
.find((exclude) => exclude !== undefined);
if (excludedConfig === undefined) {
excludedConfig = [];
} else if (typeof excludedConfig === 'string') {
excludedConfig = [excludedConfig];
}
return propertyGroups.filter((props) => {
return !excludedConfig.includes(props.name);
});
}
public isIncludeAllEnabled() {
let includeAllProperty = this.config
.map((config) => config.includeAll)
.find((includeAll) => includeAll !== undefined);
return includeAllProperty !== undefined ? includeAllProperty : false;
} }
private flattenItems(items) { private flattenItems(items) {

View File

@ -210,5 +210,40 @@ describe('ContentMetaDataService', () => {
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content'); expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
}); });
it('should exclude the property if this property is excluded from config', (done) => {
const fakeNode: Node = <Node> { name: 'Node Action', id: 'fake-id', nodeType: 'cm:content', isFile: true, aspectNames: [] } ;
const customLayoutOrientedScheme = [
{
'id': 'app.content.metadata.customGroup',
'title': 'Exif',
'includeAll': true,
'exclude': ['cm:content'],
'items': [
{
'id': 'app.content.metadata.exifAspect2',
'aspect': 'exif:exif',
'properties': '*'
}
]
}
];
setConfig('custom', customLayoutOrientedScheme);
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(contentResponse);
});
service.getGroupedProperties(fakeNode, 'custom').subscribe(
(res) => {
expect(res.length).toEqual(0);
done();
}
);
expect(classesApi.getClass).toHaveBeenCalledTimes(1);
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
});
}); });
}); });

View File

@ -18,13 +18,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Node } from '@alfresco/js-api'; import { Node } from '@alfresco/js-api';
import { BasicPropertiesService } from './basic-properties.service'; import { BasicPropertiesService } from './basic-properties.service';
import { Observable, of } from 'rxjs'; import { Observable, of, iif } from 'rxjs';
import { PropertyGroupTranslatorService } from './property-groups-translator.service'; import { PropertyGroupTranslatorService } from './property-groups-translator.service';
import { CardViewItem } from '@alfresco/adf-core'; import { CardViewItem } from '@alfresco/adf-core';
import { CardViewGroup, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces'; import { CardViewGroup, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces';
import { ContentMetadataConfigFactory } from './config/content-metadata-config.factory'; import { ContentMetadataConfigFactory } from './config/content-metadata-config.factory';
import { PropertyDescriptorsService } from './property-descriptors.service'; import { PropertyDescriptorsService } from './property-descriptors.service';
import { map } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -45,14 +45,20 @@ export class ContentMetadataService {
let groupedProperties = of([]); let groupedProperties = of([]);
if (node.aspectNames) { if (node.aspectNames) {
const config = this.contentMetadataConfigFactory.get(presetName), const contentMetadataConfig = this.contentMetadataConfigFactory.get(presetName),
groupNames = node.aspectNames groupNames = node.aspectNames
.concat(node.nodeType) .concat(node.nodeType)
.filter((groupName) => config.isGroupAllowed(groupName)); .filter((groupName) => contentMetadataConfig.isGroupAllowed(groupName));
if (groupNames.length > 0) { if (groupNames.length > 0) {
groupedProperties = this.propertyDescriptorsService.load(groupNames).pipe( groupedProperties = this.propertyDescriptorsService.load(groupNames).pipe(
map((groups) => config.reorganiseByConfig(groups)), switchMap((groups) =>
iif(
() => contentMetadataConfig.isIncludeAllEnabled(),
of(contentMetadataConfig.appendAllPreset(groups).concat(contentMetadataConfig.reorganiseByConfig(groups))),
of(contentMetadataConfig.reorganiseByConfig(groups))
)),
map((groups) => contentMetadataConfig.filterExcludedPreset(groups)),
map((groups) => this.filterEmptyPreset(groups)), map((groups) => this.filterEmptyPreset(groups)),
map((groups) => this.setTitleToNameIfNotSet(groups)), map((groups) => this.setTitleToNameIfNotSet(groups)),
map((groups) => this.propertyGroupTranslatorService.translateToCardViewGroups(groups, node.properties)) map((groups) => this.propertyGroupTranslatorService.translateToCardViewGroups(groups, node.properties))

View File

@ -328,6 +328,31 @@
"description": "Property name", "description": "Property name",
"type": "string" "type": "string"
} }
},
{
"description": "Include every property",
"type": "boolean"
},
{
"description": "Properties array",
"type": "object",
"required": [
"includeAll",
"exclude"
],
"properties": {
"includeAll": {
"type": "boolean"
},
"exclude": {
"type": "array",
"items": {
"description": "Property name",
"type": "string"
}
}
}
} }
] ]
} }
@ -340,10 +365,6 @@
{ {
"description": "Content metadata's one layout group definition", "description": "Content metadata's one layout group definition",
"type": "object", "type": "object",
"required": [
"title",
"items"
],
"properties": { "properties": {
"title": { "title": {
"type": "string", "type": "string",
@ -804,7 +825,6 @@
"properties": { "properties": {
"presets": { "presets": {
"description": "Presets for content metadata component", "description": "Presets for content metadata component",
"type": "object",
"patternProperties": { "patternProperties": {
".*": { ".*": {
"oneOf": [ "oneOf": [
@ -813,6 +833,23 @@
"pattern": "^\\*$", "pattern": "^\\*$",
"description": "Wildcard for every aspect" "description": "Wildcard for every aspect"
}, },
{
"type": "object",
"description": "",
"required": [
"includeAll"
],
"properties": {
"includeAll": {
"description": "includeAll all property",
"type": "boolean"
},
"postfix": {
"description": "exclude",
"type": ["string","array"]
}
}
},
{ {
"$ref": "#/definitions/content-metadata-aspect" "$ref": "#/definitions/content-metadata-aspect"
}, },