From 9587995ac3b1f2164975407ec07ca47b302d9c55 Mon Sep 17 00:00:00 2001 From: Dominik Iwanek <141320833+dominikiwanekhyland@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:21:13 +0200 Subject: [PATCH] =?UTF-8?q?[MNT-25612]=20ADW=20Metadata=20Drawer=20ignores?= =?UTF-8?q?=20read=E2=80=91only=20presets=20(readOnlyAs=E2=80=A6=20(#12095?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content-metadata.component.spec.ts | 14 +++- .../content-metadata.component.ts | 2 +- .../services/content-metadata.service.spec.ts | 35 ++++++++++ .../services/content-metadata.service.ts | 67 ++++++++++++++++++- 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts index c1e6acf34d..69346c7101 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts @@ -299,6 +299,18 @@ describe('ContentMetadataComponent', () => { expect(contentMetadataService.getGroupedProperties).toHaveBeenCalled(); })); + it('should pass preset to getBasicProperties when calling getProperties', fakeAsync(() => { + const testNode = { ...node, name: 'test-node' }; + component.preset = 'custom-preset'; + const getBasicPropertiesSpy = spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(of([])); + getGroupedPropertiesSpy.and.returnValue(of([])); + + component.ngOnChanges({ node: new SimpleChange(null, testNode, false) }); + tick(600); + + expect(getBasicPropertiesSpy).toHaveBeenCalledWith(testNode, 'custom-preset'); + })); + describe('Save button - Grouped Properties', () => { beforeEach(() => { getGroupedPropertiesSpy.and.returnValue( @@ -858,7 +870,7 @@ describe('ContentMetadataComponent', () => { component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); expect(contentMetadataService.getContentTypeProperty).toHaveBeenCalledWith(expectedNode); - expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode); + expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode, preset); }); it('should pass through the loaded basic properties to the card view', async () => { diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts index e5c532f8dd..7cf3806276 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts @@ -473,7 +473,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit { } private getProperties(node: Node) { - const properties$ = this.contentMetadataService.getBasicProperties(node); + const properties$ = this.contentMetadataService.getBasicProperties(node, this.preset); const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node); return zip(properties$, contentTypeProperty$).pipe( map(([properties, contentTypeProperty]) => { diff --git a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts index e771cba83c..9307d63202 100644 --- a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts +++ b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts @@ -214,6 +214,41 @@ describe('ContentMetaDataService', () => { }); }); + it('should mark basic properties as read-only when preset defines readOnlyProperties (includeAll)', async () => { + setConfig('custom', [{ includeAll: true, readOnlyProperties: ['cm:name', 'cm:title'] }]); + + const res = await firstValueFrom(service.getBasicProperties(fakeNode, 'custom')); + const nameProperty = res.find((property) => property.key === 'properties.cm:name'); + const titleProperty = res.find((property) => property.key === 'properties.cm:title'); + const authorProperty = res.find((property) => property.key === 'properties.cm:author'); + + expect(nameProperty.editable).toBeFalse(); + expect(titleProperty.editable).toBeFalse(); + expect(authorProperty.editable).toBeTrue(); + }); + + it('should mark basic properties as read-only when layout items are not editable', async () => { + setConfig('custom', [ + { + items: [ + { type: 'cm:content', properties: ['cm:name'], editable: false }, + { aspect: 'cm:titled', properties: ['cm:title', 'cm:description'], editable: false } + ] + } + ]); + + const res = await firstValueFrom(service.getBasicProperties(fakeNode, 'custom')); + const nameProperty = res.find((property) => property.key === 'properties.cm:name'); + const titleProperty = res.find((property) => property.key === 'properties.cm:title'); + const descriptionProperty = res.find((property) => property.key === 'properties.cm:description'); + const authorProperty = res.find((property) => property.key === 'properties.cm:author'); + + expect(nameProperty.editable).toBeFalse(); + expect(titleProperty.editable).toBeFalse(); + expect(descriptionProperty.editable).toBeFalse(); + expect(authorProperty.editable).toBeTrue(); + }); + it('should return the content type property', () => { spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(of([{ label: 'hello i am a weird content type' } as CardViewItem])); diff --git a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts index 4da11e5327..d9757599c0 100644 --- a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts @@ -20,12 +20,22 @@ import { Node } from '@alfresco/js-api'; import { BasicPropertiesService } from './basic-properties.service'; import { Observable, of, iif, Subject } from 'rxjs'; import { PropertyGroupTranslatorService } from './property-groups-translator.service'; -import { CardViewItem } from '@alfresco/adf-core'; +import { AppConfigService, CardViewItem } from '@alfresco/adf-core'; import { CardViewGroup, OrganisedPropertyGroup, PresetConfig } from '../interfaces/content-metadata.interfaces'; import { ContentMetadataConfigFactory } from './config/content-metadata-config.factory'; import { PropertyDescriptorsService } from './property-descriptors.service'; import { map, switchMap } from 'rxjs/operators'; import { ContentTypePropertiesService } from './content-type-property.service'; +import { LayoutOrientedConfig, LayoutOrientedConfigLayoutBlock } from '../interfaces/layout-oriented-config.interface'; +import { Property } from '../interfaces/property.interface'; + +interface LayoutBlockWithReadOnly extends LayoutOrientedConfigLayoutBlock { + readOnlyProperties?: string | string[]; +} + +const CONTENT_METADATA_CONFIG_KEY = 'content-metadata'; +const BASIC_PROPERTY_KEY_PREFIX = 'properties.'; + @Injectable({ providedIn: 'root' }) @@ -35,11 +45,62 @@ export class ContentMetadataService { private readonly propertyGroupTranslatorService = inject(PropertyGroupTranslatorService); private readonly propertyDescriptorsService = inject(PropertyDescriptorsService); private readonly contentTypePropertyService = inject(ContentTypePropertiesService); + private readonly appConfig = inject(AppConfigService); error = new Subject<{ statusCode: number; message: string }>(); - getBasicProperties(node: Node): Observable { - return of(this.basicPropertiesService.getProperties(node)); + getBasicProperties(node: Node, preset: string | PresetConfig = 'default'): Observable { + const properties = this.basicPropertiesService.getProperties(node); + const readOnlyProperties = this.getReadOnlyPropertyNames(preset); + + if (readOnlyProperties.length) { + properties.forEach((property) => { + const propertyName = this.getBasicPropertyName(property.key); + if (propertyName && readOnlyProperties.includes(propertyName)) { + property.editable = false; + } + }); + } + + return of(properties); + } + + private getBasicPropertyName(key: string): string | undefined { + return key?.startsWith(BASIC_PROPERTY_KEY_PREFIX) ? key.slice(BASIC_PROPERTY_KEY_PREFIX.length) : undefined; + } + + private getReadOnlyPropertyNames(preset: string | PresetConfig): string[] { + const presetConfig = typeof preset === 'string' ? this.appConfig.config[CONTENT_METADATA_CONFIG_KEY]?.presets?.[preset] : preset; + + if (this.isLayoutConfig(presetConfig)) { + return presetConfig.reduce((readOnly, block) => readOnly.concat(this.getLayoutBlockReadOnlyNames(block)), [] as string[]); + } + + if (presetConfig != null && typeof presetConfig === 'object') { + return this.normaliseToArray(presetConfig.readOnlyProperties); + } + + return []; + } + + private isLayoutConfig(config: unknown): config is LayoutOrientedConfig { + return Array.isArray(config); + } + + private getLayoutBlockReadOnlyNames(block: LayoutBlockWithReadOnly): string[] { + const blockReadOnly = this.normaliseToArray(block?.readOnlyProperties); + const nonEditableItems = (block?.items || []) + .filter((item) => item?.editable === false) + .reduce((names, item) => names.concat(this.normaliseToArray(item.properties)), [] as string[]); + + return blockReadOnly.concat(nonEditableItems); + } + + private normaliseToArray(value: string | string[] | Property[] | undefined): string[] { + if (Array.isArray(value)) { + return value.map((item) => (typeof item === 'string' ? item : item?.name)).filter((name): name is string => typeof name === 'string'); + } + return typeof value === 'string' ? [value] : []; } getContentTypeProperty(node: Node): Observable {