Skip to content

Commit 53446fc

Browse files
[DURACOM-507] fix dynamic layout decorators, clean up
1 parent 10935af commit 53446fc

22 files changed

Lines changed: 141 additions & 231 deletions

src/app/decorators.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,10 @@ export const DECORATORS: DecoratorConfig[] = [
150150
{ name: 'boxType' },
151151
],
152152
},
153+
{
154+
name: 'dynamicLayoutPage',
155+
params: [
156+
{ name: 'orientation' },
157+
],
158+
},
153159
];
Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { hasNoValue } from '@dspace/shared/utils/empty.util';
1+
import { hasValue } from '@dspace/shared/utils/empty.util';
22

3+
import { RENDER_DYNAMIC_LAYOUT_BOX_FOR_MAP } from '../../../decorator-registries/render-dynamic-layout-box-for-registry';
34
import { GenericConstructor } from '../../core/shared/generic-constructor';
45
import { LayoutBox } from '../enums/layout-box.enum';
56

@@ -13,15 +14,11 @@ import { LayoutBox } from '../enums/layout-box.enum';
1314
type DynamicLayoutBoxComponent = GenericConstructor<any>;
1415

1516
/**
16-
* Registry mapping {@link LayoutBox} types to their rendering component.
17+
* Marker decorator used to register a component as the renderer for a given {@link LayoutBox} type.
1718
*
18-
* Entries are added dynamically at class-definition time by the
19-
* {@link renderDynamicLayoutBoxFor} decorator, instead of being hardcoded here.
20-
*/
21-
const layoutBoxesMap = new Map<LayoutBox, DynamicLayoutBoxComponent>();
22-
23-
/**
24-
* Decorator used to register a component as the renderer for a given {@link LayoutBox} type.
19+
* The actual registry ({@link RENDER_DYNAMIC_LAYOUT_BOX_FOR_MAP}) is generated at build time by
20+
* `scripts/generate-decorator-registries.ts` from these decorator usages, so this function is a no-op
21+
* at runtime and only serves as metadata for the generator.
2522
*
2623
* Components extending {@link DynamicLayoutBoxDirective} inherit a static
2724
* `hasOwnContainer` flag (default `false`). If a component provides its own
@@ -32,19 +29,21 @@ const layoutBoxesMap = new Map<LayoutBox, DynamicLayoutBoxComponent>();
3229
*/
3330
export function renderDynamicLayoutBoxFor(boxType: LayoutBox) {
3431
return function decorator(component: DynamicLayoutBoxComponent) {
35-
if (hasNoValue(boxType)) {
36-
return;
37-
}
38-
layoutBoxesMap.set(boxType, component);
3932
};
4033
}
4134

4235
/**
4336
* Resolves the rendering component for a given box type.
4437
*
4538
* @param boxType the layout box type to look up
46-
* @returns the component constructor for the box type, or undefined if not registered
39+
* @param registry the registry containing all the box rendering components
40+
* @returns a promise resolving to the component constructor for the box type,
41+
* or undefined if not registered
4742
*/
48-
export function getDynamicLayoutBox(boxType: LayoutBox): DynamicLayoutBoxComponent {
49-
return layoutBoxesMap.get(boxType);
43+
export function getDynamicLayoutBox(
44+
boxType: LayoutBox,
45+
registry: Map<LayoutBox, () => Promise<DynamicLayoutBoxComponent>> = RENDER_DYNAMIC_LAYOUT_BOX_FOR_MAP,
46+
): Promise<DynamicLayoutBoxComponent> {
47+
const loader = registry.get(boxType);
48+
return hasValue(loader) ? loader() : undefined;
5049
}
Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
import { hasNoValue } from '@dspace/shared/utils/empty.util';
1+
import { hasValue } from '@dspace/shared/utils/empty.util';
22

3+
import { DYNAMIC_LAYOUT_PAGE_MAP } from '../../../decorator-registries/dynamic-layout-page-registry';
34
import { GenericConstructor } from '../../core/shared/generic-constructor';
45
import {
56
DEFAULT_LAYOUT_PAGE,
67
LayoutPage,
78
} from '../enums/layout-page.enum';
89

910
/**
10-
* Registry mapping {@link LayoutPage} orientation types to their page component.
11+
* Marker decorator used to register a component as the renderer for a given {@link LayoutPage} orientation.
1112
*
12-
* Entries are added dynamically at class-definition time by the
13-
* {@link dynamicLayoutPage} decorator, instead of being hardcoded here.
14-
*/
15-
const layoutPageMap = new Map<LayoutPage, GenericConstructor<any>>();
16-
17-
/**
18-
* Decorator used to register a component as the renderer for a given {@link LayoutPage} orientation.
13+
* The actual registry ({@link DYNAMIC_LAYOUT_PAGE_MAP}) is generated at build time by
14+
* `scripts/generate-decorator-registries.ts` from these decorator usages, so this function is a no-op
15+
* at runtime and only serves as metadata for the generator.
1916
*
2017
* @param orientation the layout page orientation the decorated component renders
2118
*/
2219
export function dynamicLayoutPage(orientation: LayoutPage) {
2320
return function decorator(component: GenericConstructor<any>) {
24-
if (hasNoValue(orientation)) {
25-
return;
26-
}
27-
layoutPageMap.set(orientation, component);
21+
/* intentionally empty: the registry is generated at build time */
2822
};
2923
}
3024

@@ -33,14 +27,16 @@ export function dynamicLayoutPage(orientation: LayoutPage) {
3327
* Falls back to {@link DEFAULT_LAYOUT_PAGE} if orientation is null or not registered.
3428
*
3529
* @param orientation the layout page orientation (horizontal or vertical)
36-
* @returns the component constructor for the requested orientation
30+
* @param registry the registry containing all the layout page components
31+
* @returns a promise resolving to the component constructor for the requested orientation,
32+
* or undefined if neither the orientation nor the default is registered
3733
*/
38-
export function getDynamicLayoutPage(orientation: LayoutPage): any {
39-
let componentLayout;
40-
if (hasNoValue(orientation) || hasNoValue(layoutPageMap.get(orientation))) {
41-
componentLayout = layoutPageMap.get(DEFAULT_LAYOUT_PAGE);
42-
} else {
43-
componentLayout = layoutPageMap.get(orientation);
44-
}
45-
return componentLayout;
34+
export function getDynamicLayoutPage(
35+
orientation: LayoutPage,
36+
registry: Map<LayoutPage, () => Promise<GenericConstructor<any>>> = DYNAMIC_LAYOUT_PAGE_MAP,
37+
): Promise<GenericConstructor<any>> {
38+
const loader = (hasValue(orientation) && hasValue(registry.get(orientation)))
39+
? registry.get(orientation)
40+
: registry.get(DEFAULT_LAYOUT_PAGE);
41+
return hasValue(loader) ? loader() : undefined;
4642
}

src/app/dynamic-layout/dynamic-layout-loader/dynamic-layout-loader.component.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,15 @@ describe('DynamicLayoutLoaderComponent', () => {
9595
await configureTestBed('horizontal');
9696
});
9797

98-
beforeEach(() => {
98+
beforeEach(async () => {
9999
fixture = TestBed.createComponent(DynamicLayoutLoaderComponent);
100100
component = fixture.componentInstance;
101101
component.item = mockItem;
102102
component.leadingTabs = [];
103103
component.tabs = loaderTabs;
104104
fixture.detectChanges();
105+
await fixture.whenStable();
106+
fixture.detectChanges();
105107
});
106108

107109
it('should create', () => {
@@ -118,13 +120,15 @@ describe('DynamicLayoutLoaderComponent', () => {
118120
await configureTestBed('vertical');
119121
});
120122

121-
beforeEach(() => {
123+
beforeEach(async () => {
122124
fixture = TestBed.createComponent(DynamicLayoutLoaderComponent);
123125
component = fixture.componentInstance;
124126
component.item = mockItem;
125127
component.leadingTabs = [];
126128
component.tabs = loaderTabs;
127129
fixture.detectChanges();
130+
await fixture.whenStable();
131+
fixture.detectChanges();
128132
});
129133

130134
it('should create', () => {

src/app/dynamic-layout/dynamic-layout-loader/dynamic-layout-loader.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class DynamicLayoutLoaderComponent extends AbstractComponentLoaderCompone
9898
*
9999
* @returns The constructor of the layout page component (horizontal or vertical)
100100
*/
101-
public getComponent(): GenericConstructor<Component> {
101+
public getComponent(): Promise<GenericConstructor<Component>> {
102102
const configuration = this.getConfiguration();
103103
return getDynamicLayoutPage(configuration.orientation as LayoutPage);
104104
}

src/app/dynamic-layout/dynamic-layout-loader/dynamic-layout-vertical/dynamic-layout-sidebar/dynamic-layout-sidebar.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
<div class="wrapper d-flex align-items-stretch" [ngClass]="{'wrapper-close': (isSideBarHidden$ | async) !== true,
2-
'container': (hasSidebar$ | async) }">
1+
<div class="wrapper d-flex align-items-stretch" [ngClass]="{'wrapper-close': (isSideBarHidden$ | async) !== true}">
32
@if ((hasSidebar$ | async) && (isSideBarHidden$ | async) !== true && showNav) {
43
<nav id="sidebar" [ngClass]="{'active': (isSideBarHidden$ | async) !== true}">
54
<div>

src/app/dynamic-layout/dynamic-layout-matrix/dynamic-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-render.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export class AttachmentRenderComponent extends AbstractComponentLoaderComponent<
7373
*
7474
* @returns The constructor of the matching attachment render component
7575
*/
76-
public getComponent(): GenericConstructor<Component> {
76+
public getComponent(): Promise<GenericConstructor<Component>> {
7777
const rendering = this.renderingType || AttachmentRenderingType.DOWNLOAD;
78-
return getAttachmentTypeRendering(rendering);
78+
return Promise.resolve(getAttachmentTypeRendering(rendering));
7979
}
8080
}
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,33 @@
11
import { GenericConstructor } from '@dspace/core/shared/generic-constructor';
2-
import {
3-
hasNoValue,
4-
hasValue,
5-
isEmpty,
6-
} from '@dspace/shared/utils/empty.util';
2+
import { hasValue } from '@dspace/shared/utils/empty.util';
73

4+
import { METADATA_BOX_FIELD_RENDERING_MAP } from '../../../../../../../decorator-registries/metadata-box-field-rendering-registry';
5+
import { getMatch } from '../../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
86
import { FieldRenderingType } from './field-rendering-type';
97
import { RenderingTypeDirective } from './rendering-type.directive';
108

119
/**
12-
* Registry mapping {@link FieldRenderingType} values to their rendering component.
13-
*
14-
* Entries are added dynamically at class-definition time by the
15-
* {@link metadataBoxFieldRendering} decorator, instead of being hardcoded in a map.
10+
* Whether a rendering type renders all metadata values in a single structured render (`true`),
11+
* or renders each metadata value individually (`false`).
1612
*/
17-
const metadataBoxFieldRenderMap = new Map<FieldRenderingType, GenericConstructor<RenderingTypeDirective>>();
13+
export const DEFAULT_METADATA_BOX_STRUCTURED = false;
1814

1915
/**
20-
* Decorator used to register a component as the renderer for a given {@link FieldRenderingType}.
16+
* Marker decorator used to register a component as the renderer for a given {@link FieldRenderingType}.
17+
*
18+
* The actual registry ({@link METADATA_BOX_FIELD_RENDERING_MAP}) is generated at build time by
19+
* `scripts/generate-decorator-registries.ts` from these decorator usages, so this function is a no-op
20+
* at runtime and only serves as metadata for the generator.
2121
*
2222
* @param fieldRenderingType the field rendering type the decorated component renders
23+
* @param structured whether the component renders all metadata values in a single structured render
2324
*/
24-
export function metadataBoxFieldRendering(fieldRenderingType: FieldRenderingType) {
25+
export function metadataBoxFieldRendering(fieldRenderingType: FieldRenderingType, structured: boolean = DEFAULT_METADATA_BOX_STRUCTURED) {
2526
return function decorator(component: GenericConstructor<RenderingTypeDirective>) {
26-
if (hasNoValue(fieldRenderingType)) {
27-
return;
28-
}
29-
metadataBoxFieldRenderMap.set(fieldRenderingType, component);
27+
/* intentionally empty: the registry is generated at build time */
3028
};
3129
}
3230

33-
/**
34-
* Returns the registry of {@link FieldRenderingType} to their rendering component,
35-
* populated by the {@link metadataBoxFieldRendering} decorator.
36-
*
37-
* @returns the map of rendering types to rendering components
38-
*/
39-
export function getMetadataBoxFieldRenderMap(): Map<FieldRenderingType, GenericConstructor<RenderingTypeDirective>> {
40-
return metadataBoxFieldRenderMap;
41-
}
42-
4331
/**
4432
* Return the rendering type of the field to render
4533
*
@@ -56,15 +44,24 @@ export const computeRenderingFn = (rendering: string, isSubtype = false): string
5644
};
5745

5846
/**
59-
* Return the rendering component related to the given rendering type
60-
* @param layoutBoxesMap
61-
* @param fieldRenderingType
47+
* Resolve the rendering component for the given rendering type.
48+
*
49+
* Falls back to the {@link FieldRenderingType.TEXT} component when the rendering type is not registered.
50+
*
51+
* @param fieldRenderingType the rendering type to look up
52+
* @param structured whether to look up the structured variant of the rendering type
53+
* @param registry the registry containing all the rendering components
54+
* @returns a promise resolving to the matching rendering component, or undefined if none is registered
6255
*/
63-
export const getMetadataBoxFieldRenderOptionsFn = (layoutBoxesMap: Map<FieldRenderingType, GenericConstructor<RenderingTypeDirective>>, fieldRenderingType: string): GenericConstructor<RenderingTypeDirective> => {
64-
let renderOptions = layoutBoxesMap.get(fieldRenderingType?.toUpperCase() as FieldRenderingType);
65-
// If the rendering type not exists will use TEXT type rendering
66-
if (isEmpty(renderOptions)) {
67-
renderOptions = layoutBoxesMap.get(FieldRenderingType.TEXT);
68-
}
69-
return renderOptions;
56+
export const getMetadataBoxFieldRenderOptionsFn = (
57+
fieldRenderingType: string,
58+
structured: boolean = DEFAULT_METADATA_BOX_STRUCTURED,
59+
registry: Map<any, any> = METADATA_BOX_FIELD_RENDERING_MAP,
60+
): Promise<GenericConstructor<RenderingTypeDirective>> => {
61+
const match = getMatch(
62+
registry,
63+
[fieldRenderingType?.toUpperCase(), structured],
64+
[FieldRenderingType.TEXT, DEFAULT_METADATA_BOX_STRUCTURED],
65+
);
66+
return hasValue(match) ? (match.match() as Promise<GenericConstructor<RenderingTypeDirective>>) : undefined;
7067
};

src/app/dynamic-layout/dynamic-layout-matrix/dynamic-layout-box-container/boxes/metadata/rendering-types/metadataGroup/inline/inline.component.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
} from '@angular/core/testing';
1010
import { By } from '@angular/platform-browser';
1111
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
12-
import { DYNAMIC_FIELD_RENDERING_MAP } from '@dspace/config/app-config.interface';
1312
import { LayoutField } from '@dspace/core/layout/models/box.model';
1413
import { Item } from '@dspace/core/shared/item.model';
1514
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
@@ -23,7 +22,6 @@ import { ToDatePipe } from '../../../../../../../../shared/access-control-form-c
2322
import { getMockThemeService } from '../../../../../../../../shared/theme-support/test/theme-service.mock';
2423
import { ThemeService } from '../../../../../../../../shared/theme-support/theme.service';
2524
import { MetadataRenderComponent } from '../../../row/metadata-container/metadata-render/metadata-render.component';
26-
import { getMetadataBoxFieldRenderMap } from '../../metadata-box.decorator';
2725
import { TextComponent } from '../../text/text.component';
2826
import { InlineComponent } from './inline.component';
2927

@@ -101,7 +99,6 @@ describe('InlineComponent', () => {
10199
{ provide: 'itemProvider', useValue: testItem },
102100
{ provide: 'renderingSubTypeProvider', useValue: '' },
103101
{ provide: 'tabNameProvider', useValue: '' },
104-
{ provide: DYNAMIC_FIELD_RENDERING_MAP, useValue: getMetadataBoxFieldRenderMap() },
105102
{ provide: ThemeService, useValue: getMockThemeService() },
106103
],
107104
schemas: [NO_ERRORS_SCHEMA],
@@ -110,10 +107,12 @@ describe('InlineComponent', () => {
110107
}).compileComponents();
111108
}));
112109

113-
beforeEach(() => {
110+
beforeEach(async () => {
114111
fixture = TestBed.createComponent(InlineComponent);
115112
component = fixture.componentInstance;
116113
fixture.detectChanges();
114+
await fixture.whenStable();
115+
fixture.detectChanges();
117116
});
118117

119118
it('should create', () => {

src/app/dynamic-layout/dynamic-layout-matrix/dynamic-layout-box-container/boxes/metadata/rendering-types/metadataGroup/table/table.component.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
waitForAsync,
66
} from '@angular/core/testing';
77
import { By } from '@angular/platform-browser';
8-
import { DYNAMIC_FIELD_RENDERING_MAP } from '@dspace/config/app-config.interface';
98
import { LayoutField } from '@dspace/core/layout/models/box.model';
109
import { Item } from '@dspace/core/shared/item.model';
1110
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
@@ -19,7 +18,6 @@ import { getMockThemeService } from '../../../../../../../../shared/theme-suppor
1918
import { ThemeService } from '../../../../../../../../shared/theme-support/theme.service';
2019
import { MetadataRenderComponent } from '../../../row/metadata-container/metadata-render/metadata-render.component';
2120
import { FieldRenderingType } from '../../field-rendering-type';
22-
import { getMetadataBoxFieldRenderMap } from '../../metadata-box.decorator';
2321
import { TextComponent } from '../../text/text.component';
2422
import { TableComponent } from './table.component';
2523

@@ -103,17 +101,18 @@ describe('TableComponent', () => {
103101
{ provide: 'itemProvider', useValue: testItem },
104102
{ provide: 'renderingSubTypeProvider', useValue: '' },
105103
{ provide: 'tabNameProvider', useValue: '' },
106-
{ provide: DYNAMIC_FIELD_RENDERING_MAP, useValue: getMetadataBoxFieldRenderMap() },
107104
{ provide: ThemeService, useValue: getMockThemeService() },
108105
],
109106
}).overrideComponent(TableComponent, {
110107
set: { changeDetection: ChangeDetectionStrategy.OnPush },
111108
}).compileComponents();
112109
}));
113-
beforeEach(() => {
110+
beforeEach(async () => {
114111
fixture = TestBed.createComponent(TableComponent);
115112
component = fixture.componentInstance;
116113
fixture.detectChanges();
114+
await fixture.whenStable();
115+
fixture.detectChanges();
117116
});
118117

119118
it('should create', (done) => {

0 commit comments

Comments
 (0)