From 3fcf965eca64c4818fd8e57bcf846e1c2aa4c1df Mon Sep 17 00:00:00 2001 From: davidcanonieto Date: Fri, 20 Mar 2020 22:16:35 +0000 Subject: [PATCH] [ADF-4673] Add editable property to metadata config (#5557) * [ADF-4673] Add editable property to metadata config * Add readonly mode for aspect oriented config * Fix linting --- demo-shell/src/app.config.json | 7 +- .../content-metadata-card.component.md | 45 +++++ .../layout-oriented-config.interface.ts | 1 + .../interfaces/property.interface.ts | 1 + .../aspect-oriented-config.service.spec.ts | 30 ++++ .../config/aspect-oriented-config.service.ts | 40 ++++- .../layout-oriented-config.service.spec.ts | 162 +++++++++++------- .../config/layout-oriented-config.service.ts | 19 +- .../property-groups-translator.service.ts | 2 +- lib/core/app-config/schema.json | 34 +++- 10 files changed, 266 insertions(+), 75 deletions(-) diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index 1263c98006..c86b9b95c7 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -22,7 +22,10 @@ "redirectSilentIframeUri": "{protocol}//{hostname}{:port}/assets/silent-refresh.html", "redirectUri": "/", "redirectUriLogout": "/logout", - "publicUrls": ["**/preview/s/*", "**/settings"] + "publicUrls": [ + "**/preview/s/*", + "**/settings" + ] }, "application": { "storagePrefix": "ADF", @@ -753,7 +756,7 @@ }, "adf-cloud-start-process": { "name": "My Default Cloud Name" - }, + }, "adf-process-list": { "presets": { "default": [ diff --git a/docs/content-services/components/content-metadata-card.component.md b/docs/content-services/components/content-metadata-card.component.md index 5e102c54a8..d03ee5de57 100644 --- a/docs/content-services/components/content-metadata-card.component.md +++ b/docs/content-services/components/content-metadata-card.component.md @@ -18,6 +18,8 @@ Displays and edits metadata related to a node. - [Properties](#properties) - [Details](#details) - [Application config presets](#application-config-presets) + - [Indifferent config](#indifferent-config) + - [Aspect oriented config](#aspect-oriented-config) - [Layout oriented config](#layout-oriented-config) - [Displaying all properties](#displaying-all-properties) - [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-doesnt-relate-to-that-aspect) @@ -229,6 +231,31 @@ The result of this config would be two accordion groups with the following prope | kitten:favourite-food | | kitten:recommended-food | +#### Making properties editable + +When using the layout oriented config you can also set whether or not the properties are going to be editable. + +```json +... +"content-metadata": { + "presets": { + "kitten-images": [{ + "title": "TRANSLATABLE_TITLE_FOR_GROUP_1", + "items": [ + { + "aspect": "custom:aspect", + "properties": "*", + "editable": false + } + ] + }] + } +} +... +``` + +As seen above in the example the `custom:aspect` aspect will always be on read-only mode since these properties are not editable. If the editable is enabled, then these properties will be able to be edited by the user. + ### 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. @@ -282,6 +309,24 @@ example below shows this with an aspect-oriented config: }, ``` +### Making aspects and properties read only + +Whenever you have properties that you want to protect from users editing their values you can add them to your configuration to make them read only. `readOnlyAspects` will make the whole aspect and its properties non editable. + +If you want to disable the editing for specific properties you will need to add them to the `readOnlyProperties` property. + +```json +"content-metadata": { + "presets": { + "default": { + "includeAll": true, + "readOnlyAspects": ["cm:author"], + "readOnlyProperties": ["cm:fileVersion"] + } + } +}, +``` + ## 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 diff --git a/lib/content-services/src/lib/content-metadata/interfaces/layout-oriented-config.interface.ts b/lib/content-services/src/lib/content-metadata/interfaces/layout-oriented-config.interface.ts index 6cba42cd70..7934b0046e 100644 --- a/lib/content-services/src/lib/content-metadata/interfaces/layout-oriented-config.interface.ts +++ b/lib/content-services/src/lib/content-metadata/interfaces/layout-oriented-config.interface.ts @@ -21,6 +21,7 @@ export interface LayoutOrientedConfigItem { properties: string | string[]; includeAll?: boolean; exclude?: string | string[]; + editable?: boolean; } export interface LayoutOrientedConfigLayoutBlock { diff --git a/lib/content-services/src/lib/content-metadata/interfaces/property.interface.ts b/lib/content-services/src/lib/content-metadata/interfaces/property.interface.ts index 3590efaa97..2d89e758d8 100644 --- a/lib/content-services/src/lib/content-metadata/interfaces/property.interface.ts +++ b/lib/content-services/src/lib/content-metadata/interfaces/property.interface.ts @@ -23,4 +23,5 @@ export interface Property { defaultValue?: any; mandatory: boolean; multiValued: boolean; + editable?: boolean; } diff --git a/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.spec.ts b/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.spec.ts index a494455880..7747585d89 100644 --- a/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.spec.ts +++ b/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.spec.ts @@ -222,5 +222,35 @@ describe('AspectOrientedConfigService', () => { }); }); }); + + it(`should set as readOnly the properties defined in the config inside readOnlyAspects`, () => { + const testCase = { + name: 'Not existing property', + config: { + includeAll: true, + readOnlyAspects: ['berseria'], + readOnlyProperties: ['property3'] + }, + 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'); + expect(organisedPropertyGroups[0].properties[0].editable).toBe(false); + expect(organisedPropertyGroups[0].properties[1].editable).toBe(false); + expect(organisedPropertyGroups[1].properties[0].editable).toBe(false); + expect(organisedPropertyGroups[1].properties[1].editable).toBe(undefined); + }); }); }); diff --git a/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.ts b/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.ts index 4eea2223d8..6205f84458 100644 --- a/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/config/aspect-oriented-config.service.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ContentMetadataConfig, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces'; +import { ContentMetadataConfig, OrganisedPropertyGroup, PropertyGroupContainer, Property } from '../../interfaces/content-metadata.interfaces'; import { getGroup, getProperty } from './property-group-reader'; export class AspectOrientedConfigService implements ContentMetadataConfig { @@ -43,19 +43,53 @@ export class AspectOrientedConfigService implements ContentMetadataConfig { } public appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] { - const groups = Object.keys(propertyGroups) + const groups = Object.keys(propertyGroups) .map((groupName) => { const propertyGroup = propertyGroups[groupName], properties = propertyGroup.properties; + if (this.isAspectReadOnly(groupName)) { + Object.keys(properties).map((propertyName) => this.setReadOnlyProperty(properties[propertyName])); + } + return Object.assign({}, propertyGroup, { - properties: Object.keys(properties).map((propertyName) => properties[propertyName]) + properties: Object.keys(properties).map((propertyName) => { + if (this.isPropertyReadOnly(propertyName)) { + this.setReadOnlyProperty(properties[propertyName]); + } + return properties[propertyName]; + }) }); }); return groups; } + private setReadOnlyProperty(property: Property) { + property.editable = false; + } + + private isPropertyReadOnly(propertyName: string): boolean { + const readOnlyAspects = this.config.readOnlyProperties; + + if (Array.isArray(readOnlyAspects)) { + return readOnlyAspects.includes(propertyName); + } else { + return readOnlyAspects === propertyName; + } + } + + private isAspectReadOnly(propertyGroupName: string): boolean { + const readOnlyAspects = this.config.readOnlyAspects; + + if (Array.isArray(readOnlyAspects)) { + return readOnlyAspects.includes(propertyGroupName); + } else { + return readOnlyAspects === propertyGroupName; + } + + } + public filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] { if (this.config.exclude) { return propertyGroups.filter((preset) => { diff --git a/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.spec.ts b/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.spec.ts index c1e009302f..d287797e48 100644 --- a/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.spec.ts +++ b/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.spec.ts @@ -45,9 +45,11 @@ describe('LayoutOrientedConfigService', () => { groupNameToQuery: 'berseria' }, { - config: [{ title: 'Deamons', items: [ - { aspect: 'zestiria', properties: '*' }, { aspect: 'berseria', properties: '*' } - ]}], + config: [{ + title: 'Deamons', items: [ + { aspect: 'zestiria', properties: '*' }, { aspect: 'berseria', properties: '*' } + ] + }], expectation: true, groupNameToQuery: 'berseria' }, @@ -98,11 +100,14 @@ describe('LayoutOrientedConfigService', () => { const property1 = { name: 'property1' }, property2 = { name: 'property2' }, property3 = { name: 'property3' }, - property4 = { name: 'property4' }; + property4 = { name: 'property4' }, + property5 = { name: 'property5' }, + property6 = { name: 'property6' }; const propertyGroups: PropertyGroupContainer = { berseria: { title: 'Berseria', description: '', name: 'berseria', properties: { property1, property2 } }, - zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } } + zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } }, + otherTales: { title: 'Other tales', description: '', name: 'otherTales', properties: { property5, property6 } } }; const testCases: TestCase[] = [ @@ -114,118 +119,155 @@ describe('LayoutOrientedConfigService', () => { { name: 'First property of a group in one item', config: [ - { title: 'First group', items: [ - { aspect: 'berseria', properties: [ 'property1' ] } - ]} + { + title: 'First group', items: [ + { aspect: 'berseria', properties: ['property1'] } + ] + } ], expectations: [ - { title: 'First group', properties: [ property1 ] } + { title: 'First group', properties: [property1] } ] }, { name: 'Second property of a group in one item', config: [ - { title: 'First group', items: [ - { aspect: 'berseria', properties: [ 'property2' ] } - ]} + { + title: 'First group', items: [ + { aspect: 'berseria', properties: ['property2'] } + ] + } ], expectations: [ - { title: 'First group', properties: [ property2 ] } + { title: 'First group', properties: [property2] } + ] + }, + { + name: 'Properties with editable flag', + config: [ + { + title: 'Editable property', items: [ + { aspect: 'otherTales', properties: ['property5'], editable: true }, + { aspect: 'otherTales', properties: ['property6'], editable: false } + + ] + } + ], + expectations: [ + { title: 'Editable property', properties: [property5, property6] } ] }, { name: 'More properties from one group in one item', config: [ - { title: 'First group', items: [ - { aspect: 'berseria', properties: [ 'property2', 'property1' ] } - ]} + { + title: 'First group', items: [ + { aspect: 'berseria', properties: ['property2', 'property1'] } + ] + } ], expectations: [ - { title: 'First group', properties: [ property2, property1 ] } + { 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' ] } - ]} + { + title: 'First group', items: [ + { aspect: 'zestiria', properties: ['property4'] } + ] + } ], expectations: [ - { title: 'First group', properties: [ property4 ] } + { 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' ] } - ]} + { + title: 'First group', items: [ + { aspect: 'zestiria', properties: ['property4'] }, + { aspect: 'berseria', properties: ['property1'] } + ] + } ], expectations: [ - { title: 'First group', properties: [ property4, property1 ] } + { 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' ] } - ]} + { + 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 ] } + { 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' ] } - ]} + { + 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 ] } + { 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' ] } - ]} + { + 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 ] } + { 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' } - ]} + { + 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 ] } + { title: 'First group', properties: [property3, property4, property2] } ] } ]; @@ -238,7 +280,7 @@ describe('LayoutOrientedConfigService', () => { 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].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.` diff --git a/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.ts b/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.ts index d9a867318e..9ad250c2a7 100644 --- a/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/config/layout-oriented-config.service.ts @@ -19,7 +19,8 @@ import { ContentMetadataConfig, LayoutOrientedConfigItem, OrganisedPropertyGroup, - PropertyGroupContainer + PropertyGroupContainer, + Property } from '../../interfaces/content-metadata.interfaces'; import { getProperty } from './property-group-reader'; @@ -40,7 +41,8 @@ export class LayoutOrientedConfigService implements ContentMetadataConfig { const organisedPropertyGroup = layoutBlocks.map((layoutBlock) => { const flattenedItems = this.flattenItems(layoutBlock.items), properties = flattenedItems.reduce((props, explodedItem) => { - const property = getProperty(propertyGroups, explodedItem.groupName, explodedItem.propertyName) || []; + let property = getProperty(propertyGroups, explodedItem.groupName, explodedItem.propertyName) || []; + property = this.setEditableProperty(property, explodedItem); return props.concat(property); }, []); @@ -89,13 +91,24 @@ export class LayoutOrientedConfigService implements ContentMetadataConfig { return includeAllProperty !== undefined ? includeAllProperty : false; } + private setEditableProperty(propertyGroup: Property | Property[], itemConfig): Property | Property[] { + if (Array.isArray(propertyGroup)) { + propertyGroup.map((property) => property.editable = itemConfig.editable !== undefined ? itemConfig.editable : true); + } else { + propertyGroup.editable = itemConfig.editable !== undefined ? itemConfig.editable : true; + } + + return propertyGroup; + } + 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 + propertyName, + editable: item.editable }; }); diff --git a/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts b/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts index 59730612d0..51c79ea7cb 100644 --- a/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts @@ -87,7 +87,7 @@ export class PropertyGroupTranslatorService { value: propertyValue, key: `${prefix}${property.name}`, default: property.defaultValue, - editable: true + editable: property.editable !== undefined ? property.editable : true }; let cardViewItemProperty; diff --git a/lib/core/app-config/schema.json b/lib/core/app-config/schema.json index 31acf70b75..6ae8ac5490 100644 --- a/lib/core/app-config/schema.json +++ b/lib/core/app-config/schema.json @@ -338,7 +338,9 @@ "type": "object", "required": [ "includeAll", - "exclude" + "exclude", + "readOnlyAspects", + "readOnlyProperties" ], "properties": { "includeAll": { @@ -350,7 +352,20 @@ "description": "Property name", "type": "string" } - + }, + "readOnlyAspects": { + "type": "array", + "items": { + "description": "Disable editing in these aspects", + "type": "string" + } + }, + "readOnlyProperties": { + "type": "array", + "items": { + "description": "Disable editing in these properties", + "type": "string" + } } } } @@ -397,7 +412,8 @@ "type": "object", "required": [ "aspect", - "properties" + "properties", + "editing" ], "properties": { "aspect": { @@ -407,6 +423,10 @@ "properties": { "description": "list of aspect properties", "type": "array" + }, + "editing": { + "description": "Enable/disable editing for this aspect", + "type": "boolean" } } }, @@ -892,7 +912,10 @@ }, "postfix": { "description": "exclude", - "type": ["string","array"] + "type": [ + "string", + "array" + ] } } }, @@ -1209,8 +1232,7 @@ "name" ], "properties": { - "id": { - }, + "id": {}, "name": { "type": "string" },