Skip to content

Commit 443dcca

Browse files
Merge remote-tracking branch '4Science_gitHub/task/main/DURACOM-507' into task/main/DURACOM-507
2 parents 53446fc + 6920e9e commit 443dcca

13 files changed

Lines changed: 256 additions & 12 deletions

File tree

src/app/admin/admin-sidebar/admin-sidebar.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ <h4 class="my-1">{{ 'menu.header.admin' | translate }}</h4>
2626
<div class="sidebar-full-width-container" id="sidebar-top-level-items-container">
2727
<div class="sidebar-full-width-container" id="sidebar-top-level-items" role="menubar"
2828
[attr.aria-label]="'menu.header.admin.description' |translate">
29-
@for (sectionDTO of (sectionDTOs$ | async); track sectionDTO) {
29+
@for (sectionDTO of (sectionDTOs$ | async); track sectionDTO.menuSection.id) {
3030
<ds-menu-component-loader [menuID]="menuID"
3131
[expandable]="sectionDTO.hasSubSections || sectionDTO.menuSection.alwaysRenderExpandable"
3232
[section]="sectionDTO.menuSection">

src/app/core/layout/section-data.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Injectable } from '@angular/core';
2+
import { dataService } from '@dspace/core/cache/builders/build-decorators';
3+
import { SECTION } from '@dspace/core/layout/models/section.resource-type';
24
import { Observable } from 'rxjs';
35

46
import { DSONameService } from '../breadcrumbs/dso-name.service';
@@ -20,6 +22,7 @@ import { Section } from './models/section.model';
2022
* A service responsible for fetching data from the REST API on the sections endpoint.
2123
*/
2224
@Injectable({ providedIn: 'root' })
25+
@dataService(SECTION)
2326
export class SectionDataService extends IdentifiableDataService<Section> {
2427

2528
protected linkPath = 'sections';

src/app/core/layout/tab-data.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Injectable } from '@angular/core';
2+
import { dataService } from '@dspace/core/cache/builders/build-decorators';
3+
import { TAB } from '@dspace/core/layout/models/tab.resource-type';
24
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
35
import {
46
hasNoValue,
@@ -31,6 +33,7 @@ import {
3133
* filtering out tabs that contain only minor (secondary) boxes.
3234
*/
3335
@Injectable({ providedIn: 'root' })
36+
@dataService(TAB)
3437
export class TabDataService extends IdentifiableDataService<DynamicLayoutTab> {
3538
protected searchFindByItem = 'findByItem';
3639
protected searchFindByEntityType = 'findByEntityType';

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
@@ -110,7 +110,9 @@ describe('DynamicLayoutLoaderComponent', () => {
110110
expect(component).toBeTruthy();
111111
});
112112

113-
it('should show horizontal component', () => {
113+
it('should show horizontal component', async () => {
114+
await fixture.whenStable();
115+
fixture.detectChanges();
114116
expect(fixture.debugElement.query(By.css('.horizontal-layout'))).toBeTruthy();
115117
});
116118
});
@@ -135,7 +137,9 @@ describe('DynamicLayoutLoaderComponent', () => {
135137
expect(component).toBeTruthy();
136138
});
137139

138-
it('should show vertical component', () => {
140+
it('should show vertical component', async () => {
141+
await fixture.whenStable();
142+
fixture.detectChanges();
139143
expect(fixture.debugElement.query(By.css('.vertical-layout'))).toBeTruthy();
140144
});
141145
});

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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ describe('InlineComponent', () => {
119119
expect(component).toBeTruthy();
120120
});
121121

122-
it('check metadata rendering', (done) => {
122+
it('check metadata rendering', async () => {
123+
await fixture.whenStable();
124+
fixture.detectChanges();
123125
const rowsFound = fixture.debugElement.queryAll(By.css('div.metadata-group-entry'));
124126
expect(rowsFound.length).toBe(2);
125127

@@ -135,7 +137,5 @@ describe('InlineComponent', () => {
135137
expect(span.nativeElement.textContent).toContain(testItem.metadata[mockField.metadataGroup.elements[0].metadata][1].value);
136138
span = divFound.query(By.css('span.metadata-group-entry-value:nth-child(2)'));
137139
expect(span.nativeElement.textContent).toContain(testItem.metadata[mockField.metadataGroup.elements[1].metadata][1].value);
138-
done();
139-
140140
});
141141
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@if ((initialized | async)) {
2+
<div [class]="'openstreetmap-container ' + valueStyle">
3+
@defer {
4+
<ds-geospatial-map
5+
[coordinates]="points"
6+
[bbox]="bboxes"
7+
[cluster]="cluster"
8+
layout="item"
9+
style="width: 100%;">
10+
</ds-geospatial-map>
11+
} @placeholder {
12+
<div class="map-placeholder"></div>
13+
}
14+
</div>
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.openstreetmap-container {
2+
min-height: 300px;
3+
width: 100%;
4+
}
5+
6+
.map-placeholder {
7+
min-height: 300px;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
NO_ERRORS_SCHEMA,
4+
} from '@angular/core';
5+
import {
6+
ComponentFixture,
7+
TestBed,
8+
waitForAsync,
9+
} from '@angular/core/testing';
10+
import { LayoutField } from '@dspace/core/layout/models/box.model';
11+
import { Item } from '@dspace/core/shared/item.model';
12+
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
13+
import {
14+
TranslateLoader,
15+
TranslateModule,
16+
} from '@ngx-translate/core';
17+
18+
import { OpenstreetmapComponent } from './openstreetmap.component';
19+
20+
describe('OpenstreetmapComponent', () => {
21+
let component: OpenstreetmapComponent;
22+
let fixture: ComponentFixture<OpenstreetmapComponent>;
23+
const testItem = Object.assign(new Item(), {
24+
uuid: 'itemUUID',
25+
id: 'itemUUID',
26+
metadata: {
27+
'dcterms.spatial': [
28+
{
29+
value: 'POINT(12.4924 41.8902)',
30+
},
31+
{
32+
value: 'POINT(2.3522 48.8566)',
33+
},
34+
],
35+
'dc.coverage.spatial': [
36+
{
37+
value: 'POINT(-0.1278 51.5074)',
38+
},
39+
{
40+
value: '{westlimit=12.23, southlimit=41.65, eastlimit=12.85, northlimit=42.02}',
41+
},
42+
],
43+
},
44+
_links: {
45+
self: { href: 'item-selflink' },
46+
},
47+
});
48+
49+
const mockField = Object.assign({
50+
id: 1,
51+
metadata: 'dcterms.spatial',
52+
fieldType: 'METADATAGROUP',
53+
label: 'Location(s)',
54+
rendering: 'OPENSTREETMAP',
55+
style: 'container row',
56+
styleLabel: 'fw-bold col-4',
57+
styleValue: 'col',
58+
metadataGroup: {
59+
leading: 'dcterms.spatial',
60+
elements: [
61+
{
62+
metadata: 'dcterms.spatial',
63+
label: 'Spatial',
64+
rendering: 'TEXT',
65+
fieldType: 'METADATA',
66+
style: null,
67+
styleLabel: 'fw-bold col-0',
68+
styleValue: 'col',
69+
},
70+
{
71+
metadata: 'dc.coverage.spatial',
72+
label: 'Coverage',
73+
rendering: 'TEXT',
74+
fieldType: 'METADATA',
75+
style: null,
76+
styleLabel: 'fw-bold col-0',
77+
styleValue: 'col',
78+
},
79+
],
80+
},
81+
}) as LayoutField;
82+
83+
beforeEach(waitForAsync(() => {
84+
TestBed.configureTestingModule({
85+
imports: [
86+
TranslateModule.forRoot({
87+
loader: {
88+
provide: TranslateLoader,
89+
useClass: TranslateLoaderMock,
90+
},
91+
}),
92+
OpenstreetmapComponent,
93+
],
94+
providers: [
95+
{ provide: 'fieldProvider', useValue: mockField },
96+
{ provide: 'itemProvider', useValue: testItem },
97+
{ provide: 'renderingSubTypeProvider', useValue: '' },
98+
{ provide: 'tabNameProvider', useValue: '' },
99+
],
100+
schemas: [NO_ERRORS_SCHEMA],
101+
}).overrideComponent(OpenstreetmapComponent, {
102+
set: { changeDetection: ChangeDetectionStrategy.OnPush },
103+
}).compileComponents();
104+
}));
105+
106+
beforeEach(() => {
107+
fixture = TestBed.createComponent(OpenstreetmapComponent);
108+
component = fixture.componentInstance;
109+
fixture.detectChanges();
110+
});
111+
112+
it('should create', () => {
113+
expect(component).toBeTruthy();
114+
});
115+
116+
it('should extract coordinates from multiple metadata fields', () => {
117+
expect(component.points.length).toBe(3);
118+
expect(component.points).toContain('POINT(12.4924 41.8902)');
119+
expect(component.points).toContain('POINT(2.3522 48.8566)');
120+
expect(component.points).toContain('POINT(-0.1278 51.5074)');
121+
});
122+
123+
it('should extract bboxes from multiple metadata fields', () => {
124+
expect(component.bboxes.length).toBe(1);
125+
expect(component.bboxes).toContain('{westlimit=12.23, southlimit=41.65, eastlimit=12.85, northlimit=42.02}');
126+
});
127+
128+
it('should have clustering enabled by default', () => {
129+
expect(component.cluster).toBeTrue();
130+
});
131+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { AsyncPipe } from '@angular/common';
2+
import {
3+
Component,
4+
OnInit,
5+
} from '@angular/core';
6+
7+
import { GeospatialMapComponent } from '../../../../../../../../shared/geospatial-map/geospatial-map.component';
8+
import { FieldRenderingType } from '../../field-rendering-type';
9+
import { MetadataBoxFieldRendering } from '../../metadata-box.decorator';
10+
import { MetadataGroupComponent } from '../metadata-group.component';
11+
12+
/**
13+
* This component renders geospatial metadata group fields on an OpenStreetMap map
14+
* using the ds-geospatial-map component.
15+
*
16+
* It supports multiple metadata fields within the metadata group for both point
17+
* coordinates and bounding boxes. All metadata values across all group elements
18+
* are collected and passed to the map component.
19+
*/
20+
@MetadataBoxFieldRendering(FieldRenderingType.OPENSTREETMAP, true)
21+
@Component({
22+
selector: 'ds-openstreetmap',
23+
templateUrl: './openstreetmap.component.html',
24+
styleUrls: ['./openstreetmap.component.scss'],
25+
imports: [
26+
AsyncPipe,
27+
GeospatialMapComponent,
28+
],
29+
})
30+
export class OpenstreetmapComponent extends MetadataGroupComponent implements OnInit {
31+
32+
/**
33+
* Point coordinates extracted from the metadata group (WKT format, e.g. 'POINT(lng lat)')
34+
*/
35+
points: string[] = [];
36+
37+
/**
38+
* Bounding box values extracted from the metadata group
39+
*/
40+
bboxes: string[] = [];
41+
42+
/**
43+
* Whether to cluster markers on the map
44+
*/
45+
cluster = true;
46+
47+
override ngOnInit(): void {
48+
super.ngOnInit();
49+
this.extractGeospatialData();
50+
}
51+
52+
/**
53+
* Extract point and bounding box values from all metadata fields in the group.
54+
* Point values are WKT POINT strings (e.g. 'POINT(lng lat)').
55+
* Bounding box values use a different format (e.g. '{westlimit=..., southlimit=..., ...}').
56+
* Simple heuristic: values starting with 'POINT' are treated as coordinates,
57+
* everything else is treated as a bounding box.
58+
*/
59+
private extractGeospatialData(): void {
60+
this.points = [];
61+
this.bboxes = [];
62+
63+
for (const element of this.field.metadataGroup.elements) {
64+
const values = this.item.allMetadataValues(element.metadata);
65+
if (values?.length) {
66+
for (const value of values) {
67+
if (!value) {
68+
continue;
69+
}
70+
if (value.trim().toUpperCase().startsWith('POINT')) {
71+
this.points.push(value);
72+
} else {
73+
this.bboxes.push(value);
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ describe('TableComponent', () => {
121121

122122
});
123123

124-
it('check metadata rendering', (done) => {
124+
it('check metadata rendering', async () => {
125+
await fixture.whenStable();
126+
fixture.detectChanges();
127+
125128
const rowsFound = fixture.debugElement.queryAll(By.css('tr'));
126129
expect(rowsFound.length).toBe(3);
127130

@@ -142,7 +145,5 @@ describe('TableComponent', () => {
142145
expect(td.nativeElement.textContent).toContain(testItem.metadata[mockField.metadataGroup.elements[0].metadata][1].value);
143146
td = rowFound.query(By.css('td:nth-child(2)'));
144147
expect(td.nativeElement.textContent).toContain(testItem.metadata[mockField.metadataGroup.elements[1].metadata][1].value);
145-
done();
146-
147148
});
148149
});

0 commit comments

Comments
 (0)