[MNT-25612] ADW Metadata Drawer ignores read‑only presets (readOnlyAspects / readOnlyProperties) (#12129)

This commit is contained in:
Dominik Iwanek
2026-08-05 08:42:52 +02:00
committed by GitHub
parent 72522c0f72
commit 12ae30ce06
4 changed files with 99 additions and 7 deletions
@@ -869,7 +869,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getContentTypeProperty).toHaveBeenCalledWith(expectedNode);
expect(contentMetadataService.getContentTypeProperty).toHaveBeenCalledWith(expectedNode, preset);
expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode, preset);
});
@@ -474,7 +474,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
private getProperties(node: Node) {
const properties$ = this.contentMetadataService.getBasicProperties(node, this.preset);
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node);
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node, this.preset);
return zip(properties$, contentTypeProperty$).pipe(
map(([properties, contentTypeProperty]) => {
const filteredProperties = contentTypeProperty.filter(
@@ -249,6 +249,15 @@ describe('ContentMetaDataService', () => {
expect(authorProperty.editable).toBeTrue();
});
it('should hide basic properties from general info when they are excluded', async () => {
setConfig('custom', [{ includeAll: true, exclude: ['cm:title'] }]);
const res = await firstValueFrom(service.getBasicProperties(fakeNode, 'custom'));
expect(res.find((property) => property.key === 'properties.cm:title')).toBeUndefined();
expect(res.find((property) => property.key === 'properties.cm:name')).toBeDefined();
});
it('should return the content type property', () => {
spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(of([{ label: 'hello i am a weird content type' } as CardViewItem]));
@@ -257,6 +266,49 @@ describe('ContentMetaDataService', () => {
});
});
it('should hide a single content type property in general info when that property is excluded', async () => {
setConfig('custom', [{ includeAll: true, exclude: ['fn:thema'] }]);
spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(
of([
{ label: 'Content Type', key: 'nodeType' } as CardViewItem,
{ label: 'System', key: 'properties.fn:system' } as CardViewItem,
{ label: 'Thema', key: 'properties.fn:thema' } as CardViewItem
])
);
const res = await firstValueFrom(service.getContentTypeProperty(fakeNode, 'custom'));
expect(res.map((item) => item.key)).toEqual(['nodeType', 'properties.fn:system']);
});
it('should hide content type specific properties in general info when the node type is excluded', async () => {
setConfig('custom', [{ includeAll: true, exclude: ['fn:fakenode'] }]);
spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(
of([
{ label: 'Content Type', key: 'nodeType' } as CardViewItem,
{ label: 'System', key: 'properties.fn:system' } as CardViewItem,
{ label: 'Thema', key: 'properties.fn:thema' } as CardViewItem
])
);
const res = await firstValueFrom(service.getContentTypeProperty(fakeNode, 'custom'));
expect(res.length).toBe(1);
expect(res[0].key).toBe('nodeType');
});
it('should keep content type specific properties in general info when the node type is not excluded', async () => {
setConfig('custom', [{ includeAll: true, exclude: ['cm:versionable'] }]);
spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(
of([{ label: 'Content Type', key: 'nodeType' } as CardViewItem, { label: 'System', key: 'properties.fn:system' } as CardViewItem])
);
const res = await firstValueFrom(service.getContentTypeProperty(fakeNode, 'custom'));
expect(res.length).toBe(2);
expect(res.map((item) => item.key)).toEqual(['nodeType', 'properties.fn:system']);
});
it('should trigger the opening of the content type dialog', () => {
spyOn(contentPropertyService, 'openContentTypeDialogConfirm').and.returnValue(of(true));
@@ -31,6 +31,7 @@ import { Property } from '../interfaces/property.interface';
interface LayoutBlockWithReadOnly extends LayoutOrientedConfigLayoutBlock {
readOnlyProperties?: string | string[];
exclude?: string | string[];
}
const CONTENT_METADATA_CONFIG_KEY = 'content-metadata';
@@ -50,8 +51,13 @@ export class ContentMetadataService {
error = new Subject<{ statusCode: number; message: string }>();
getBasicProperties(node: Node, preset: string | PresetConfig = 'default'): Observable<CardViewItem[]> {
const properties = this.basicPropertiesService.getProperties(node);
const readOnlyProperties = this.getReadOnlyPropertyNames(preset);
const excludedNames = this.getExcludedNames(preset);
const properties = this.basicPropertiesService.getProperties(node).filter((property) => {
const propertyName = this.getBasicPropertyName(property.key);
return !propertyName || !excludedNames.includes(propertyName);
});
if (readOnlyProperties.length) {
properties.forEach((property) => {
@@ -70,7 +76,7 @@ export class ContentMetadataService {
}
private getReadOnlyPropertyNames(preset: string | PresetConfig): string[] {
const presetConfig = typeof preset === 'string' ? this.appConfig.config[CONTENT_METADATA_CONFIG_KEY]?.presets?.[preset] : preset;
const presetConfig = this.getPresetConfig(preset);
if (this.isLayoutConfig(presetConfig)) {
return presetConfig.reduce((readOnly, block) => readOnly.concat(this.getLayoutBlockReadOnlyNames(block)), [] as string[]);
@@ -83,6 +89,10 @@ export class ContentMetadataService {
return [];
}
private getPresetConfig(preset: string | PresetConfig): PresetConfig {
return typeof preset === 'string' ? this.appConfig.config[CONTENT_METADATA_CONFIG_KEY]?.presets?.[preset] : preset;
}
private isLayoutConfig(config: unknown): config is LayoutOrientedConfig {
return Array.isArray(config);
}
@@ -96,15 +106,45 @@ export class ContentMetadataService {
return blockReadOnly.concat(nonEditableItems);
}
private normaliseToArray(value: string | string[] | Property[] | undefined): string[] {
private normaliseToArray(value: string | string[] | Property[] | boolean | 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[]> {
return this.contentTypePropertyService.getContentTypeCardItem(node);
getContentTypeProperty(node: Node, preset: string | PresetConfig = 'default'): Observable<CardViewItem[]> {
const excludedNames = this.getExcludedNames(preset);
const isTypeExcluded = excludedNames.includes(node.nodeType);
return this.contentTypePropertyService.getContentTypeCardItem(node).pipe(
map((items) =>
items.filter((item) => {
const propertyName = this.getBasicPropertyName(item.key);
if (!propertyName) {
return true;
}
return !isTypeExcluded && !excludedNames.includes(propertyName);
})
)
);
}
private getExcludedNames(preset: string | PresetConfig): string[] {
const presetConfig = this.getPresetConfig(preset);
if (this.isLayoutConfig(presetConfig)) {
return presetConfig.reduce(
(excluded, block) => excluded.concat(this.normaliseToArray((block as LayoutBlockWithReadOnly)?.exclude)),
[] as string[]
);
}
if (presetConfig != null && typeof presetConfig === 'object') {
return this.normaliseToArray(presetConfig.exclude);
}
return [];
}
openConfirmDialog(changedProperties): Observable<any> {