Skip to content

Commit af20cc7

Browse files
committed
Merge branch 'release/26.3.0'
2 parents 64457ef + 77d7a5f commit af20cc7

17 files changed

Lines changed: 303 additions & 14 deletions

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
26.3.0 (2026-02-24)
6+
===================
7+
8+
* FAIR Signposting
9+
510
26.2.1 (2026-02-03)
611
===================
712

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "osf",
3-
"version": "26.2.1",
3+
"version": "26.3.0",
44
"scripts": {
55
"ng": "ng",
66
"analyze-bundle": "ng build --configuration=analyze-bundle && source-map-explorer dist/**/*.js --no-border-checks",

src/app/app.routes.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ export const serverRoutes: ServerRoute[] = [
137137
path: ':id/overview',
138138
renderMode: RenderMode.Server,
139139
},
140+
{
141+
path: ':id/metadata/:recordId',
142+
renderMode: RenderMode.Server,
143+
},
140144
{
141145
path: ':id/files/**',
142146
renderMode: RenderMode.Server,

src/app/core/services/osf-config.service.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,24 @@ export class OSFConfigService {
4343
* On the server, this is skipped as config is only needed in the browser.
4444
*/
4545
async load(): Promise<void> {
46-
if (!this.config && isPlatformBrowser(this.platformId)) {
46+
if (this.config) return;
47+
48+
if (isPlatformBrowser(this.platformId)) {
4749
this.config = await lastValueFrom<ConfigModel>(
4850
this.http.get<ConfigModel>('/assets/config/config.json').pipe(
4951
shareReplay(1),
5052
catchError(() => of({} as ConfigModel))
5153
)
5254
);
55+
} else {
56+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
57+
this.config = ((globalThis as any).__SSR_CONFIG__ ?? {}) as ConfigModel;
58+
}
5359

54-
// Apply every key from config to environment
55-
for (const [key, value] of Object.entries(this.config)) {
56-
// eslint-disable-next-line
57-
// @ts-ignore
58-
this.environment[key] = value;
59-
}
60+
for (const [key, value] of Object.entries(this.config)) {
61+
// eslint-disable-next-line
62+
// @ts-ignore
63+
this.environment[key] = value;
6064
}
6165
}
6266
}

src/app/features/files/pages/file-detail/file-detail.component.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ describe('FileDetailComponent', () => {
4141
} as unknown as jest.Mocked<DataciteService>;
4242

4343
const mockRoute: Partial<ActivatedRoute> = {
44-
params: of({ providerId: 'osf', preprintId: 'p1' }),
45-
queryParams: of({ providerId: 'osf', preprintId: 'p1' }),
44+
params: of({ providerId: 'osf', fileGuid: 'file-1' }),
45+
queryParams: of({ providerId: 'osf', fileGuid: 'file-1' }),
4646
};
4747
(MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => {
4848
switch (selector) {
@@ -79,6 +79,7 @@ describe('FileDetailComponent', () => {
7979
}).compileComponents();
8080
fixture = TestBed.createComponent(FileDetailComponent);
8181
component = fixture.componentInstance;
82+
document.head.innerHTML = '';
8283
fixture.detectChanges();
8384
});
8485

@@ -95,4 +96,15 @@ describe('FileDetailComponent', () => {
9596
it('should call dataciteService.logIdentifiableView on start ', () => {
9697
expect(dataciteService.logIdentifiableView).toHaveBeenCalledWith(component.fileMetadata$);
9798
});
99+
100+
it('should add signposting tags during SSR', () => {
101+
fixture.detectChanges();
102+
103+
const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]'));
104+
expect(linkTags.length).toBe(2);
105+
expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/file-1/?format=linkset');
106+
expect(linkTags[0].getAttribute('type')).toBe('application/linkset');
107+
expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/file-1/?format=linkset-json');
108+
expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json');
109+
});
98110
});

src/app/features/files/pages/file-detail/file-detail.component.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
effect,
2020
HostBinding,
2121
inject,
22+
OnDestroy,
23+
OnInit,
2224
signal,
2325
} from '@angular/core';
2426
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
@@ -47,6 +49,7 @@ import { pathJoin } from '@osf/shared/helpers/path-join.helper';
4749
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
4850
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
4951
import { MetaTagsService } from '@osf/shared/services/meta-tags.service';
52+
import { SignpostingService } from '@osf/shared/services/signposting.service';
5053
import { ToastService } from '@osf/shared/services/toast.service';
5154
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
5255
import { FileDetailsModel } from '@shared/models/files/file.model';
@@ -94,7 +97,7 @@ import {
9497
changeDetection: ChangeDetectionStrategy.OnPush,
9598
providers: [DatePipe],
9699
})
97-
export class FileDetailComponent {
100+
export class FileDetailComponent implements OnInit, OnDestroy {
98101
@HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full';
99102

100103
readonly store = inject(Store);
@@ -111,6 +114,7 @@ export class FileDetailComponent {
111114
private readonly translateService = inject(TranslateService);
112115
private readonly environment = inject(ENVIRONMENT);
113116
private readonly clipboard = inject(Clipboard);
117+
private readonly signpostingService = inject(SignpostingService);
114118

115119
readonly dataciteService = inject(DataciteService);
116120

@@ -284,6 +288,16 @@ export class FileDetailComponent {
284288
this.dataciteService.logIdentifiableView(this.fileMetadata$).pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
285289
}
286290

291+
ngOnInit(): void {
292+
this.signpostingService.addSignposting(this.fileGuid);
293+
}
294+
295+
ngOnDestroy(): void {
296+
if (this.fileGuid) {
297+
this.signpostingService.removeSignpostingLinkTags();
298+
}
299+
}
300+
287301
getIframeLink(version: string) {
288302
const url = this.getMfrUrlWithVersion(version);
289303
if (url) {

src/app/features/metadata/metadata.component.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
DestroyRef,
1212
effect,
1313
inject,
14+
OnDestroy,
1415
OnInit,
1516
signal,
1617
} from '@angular/core';
@@ -24,6 +25,7 @@ import { MetadataResourceEnum } from '@osf/shared/enums/metadata-resource.enum';
2425
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
2526
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
2627
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
28+
import { SignpostingService } from '@osf/shared/services/signposting.service';
2729
import { ToastService } from '@osf/shared/services/toast.service';
2830
import { CollectionsSelectors, GetProjectSubmissions } from '@osf/shared/stores/collections';
2931
import {
@@ -117,14 +119,15 @@ import {
117119
styleUrl: './metadata.component.scss',
118120
changeDetection: ChangeDetectionStrategy.OnPush,
119121
})
120-
export class MetadataComponent implements OnInit {
122+
export class MetadataComponent implements OnInit, OnDestroy {
121123
private readonly activeRoute = inject(ActivatedRoute);
122124
private readonly router = inject(Router);
123125
private readonly destroyRef = inject(DestroyRef);
124126
private readonly customDialogService = inject(CustomDialogService);
125127
private readonly toastService = inject(ToastService);
126128
private readonly customConfirmationService = inject(CustomConfirmationService);
127129
private readonly environment = inject(ENVIRONMENT);
130+
private readonly signpostingService = inject(SignpostingService);
128131

129132
private resourceId = '';
130133

@@ -264,12 +267,18 @@ export class MetadataComponent implements OnInit {
264267
this.actions.getCedarTemplates();
265268
this.actions.fetchSelectedSubjects(this.resourceId, this.resourceType());
266269

270+
this.signpostingService.addMetadataSignposting(this.resourceId);
271+
267272
if (this.isProjectType()) {
268273
this.actions.getProjectSubmissions(this.resourceId);
269274
}
270275
}
271276
}
272277

278+
ngOnDestroy(): void {
279+
this.signpostingService.removeSignpostingLinkTags();
280+
}
281+
273282
onTabChange(tabId: string | number): void {
274283
const tab = this.tabs().find((x) => x.id === tabId.toString());
275284

src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ describe('PreprintDetailsComponent SSR Tests', () => {
466466
store = TestBed.inject(Store);
467467
fixture = TestBed.createComponent(PreprintDetailsComponent);
468468
component = fixture.componentInstance;
469+
document.head.innerHTML = '';
469470
});
470471

471472
it('should render PreprintDetailsComponent server-side without errors', () => {
@@ -475,6 +476,17 @@ describe('PreprintDetailsComponent SSR Tests', () => {
475476
expect(component).toBeTruthy();
476477
});
477478

479+
it('should add signposting tags during SSR', () => {
480+
fixture.detectChanges();
481+
482+
const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]'));
483+
expect(linkTags.length).toBe(2);
484+
expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/preprint-1/?format=linkset');
485+
expect(linkTags[0].getAttribute('type')).toBe('application/linkset');
486+
expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/preprint-1/?format=linkset-json');
487+
expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json');
488+
});
489+
478490
it('should not access browser-only APIs during SSR', () => {
479491
const platformId = TestBed.inject(PLATFORM_ID);
480492
expect(platformId).toBe('server');

src/app/features/preprints/pages/preprint-details/preprint-details.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { FixSpecialCharPipe } from '@osf/shared/pipes/fix-special-char.pipe';
3535
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
3636
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
3737
import { MetaTagsService } from '@osf/shared/services/meta-tags.service';
38+
import { SignpostingService } from '@osf/shared/services/signposting.service';
3839
import { ToastService } from '@osf/shared/services/toast.service';
3940
import { ContributorsSelectors } from '@osf/shared/stores/contributors';
4041

@@ -104,6 +105,7 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
104105
private readonly prerenderReady = inject(PrerenderReadyService);
105106
private readonly platformId = inject(PLATFORM_ID);
106107
private readonly isBrowser = isPlatformBrowser(this.platformId);
108+
private readonly signpostingService = inject(SignpostingService);
107109

108110
private readonly environment = inject(ENVIRONMENT);
109111

@@ -304,6 +306,8 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
304306
this.actions.getPreprintProviderById(this.providerId());
305307
this.fetchPreprint(this.preprintId());
306308

309+
this.signpostingService.addSignposting(this.preprintId());
310+
307311
this.dataciteService.logIdentifiableView(this.preprint$).pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
308312
}
309313

@@ -313,6 +317,8 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
313317
this.actions.clearCurrentProvider();
314318
}
315319

320+
this.signpostingService.removeSignpostingLinkTags();
321+
316322
this.helpScoutService.unsetResourceType();
317323
}
318324

src/app/features/project/overview/project-overview.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ describe('ProjectOverviewComponent SSR Tests', () => {
262262
store = TestBed.inject(Store);
263263
fixture = TestBed.createComponent(ProjectOverviewComponent);
264264
component = fixture.componentInstance;
265+
document.head.innerHTML = '';
265266
});
266267

267268
it('should render ProjectOverviewComponent server-side without errors', () => {
@@ -285,6 +286,17 @@ describe('ProjectOverviewComponent SSR Tests', () => {
285286
expect(component).toBeTruthy();
286287
});
287288

289+
it('should add signposting tags during SSR', () => {
290+
fixture.detectChanges();
291+
292+
const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]'));
293+
expect(linkTags.length).toBe(2);
294+
expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/project-123/?format=linkset');
295+
expect(linkTags[0].getAttribute('type')).toBe('application/linkset');
296+
expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/project-123/?format=linkset-json');
297+
expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json');
298+
});
299+
288300
it('should not call browser-only actions in ngOnDestroy during SSR', () => {
289301
const dispatchSpy = jest.spyOn(store, 'dispatch');
290302

0 commit comments

Comments
 (0)