[MNT-25612] ADW Metadata Drawer ignores read‑only presets (readOnlyAs… (#12095)

This commit is contained in:
Dominik Iwanek
2026-07-31 09:21:13 +02:00
committed by GitHub
parent 0c099eafb0
commit 9587995ac3
4 changed files with 113 additions and 5 deletions
@@ -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 () => {
@@ -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]) => {
@@ -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]));
@@ -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<CardViewItem[]> {
return of(this.basicPropertiesService.getProperties(node));
getBasicProperties(node: Node, preset: string | PresetConfig = 'default'): Observable<CardViewItem[]> {
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<CardViewItem[]> {