Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@ import { TranslateService } from '@ngx-translate/core';

import { DialogService } from 'primeng/dynamicdialog';

import { filter } from 'rxjs/operators';

import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';

import { GetCurrentUser } from '@core/store/user';
import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails';
import { ConfirmEmailComponent } from '@shared/components';

import { FullScreenLoaderComponent, ToastComponent } from './shared/components';
import { MetaTagsService } from './shared/services';

@Component({
selector: 'osf-root',
Expand All @@ -26,18 +22,15 @@ import { MetaTagsService } from './shared/services';
providers: [DialogService],
})
export class AppComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly dialogService = inject(DialogService);
private readonly router = inject(Router);
private readonly translateService = inject(TranslateService);
private readonly metaTagsService = inject(MetaTagsService);

private readonly actions = createDispatchMap({ getCurrentUser: GetCurrentUser, getEmails: GetEmails });

unverifiedEmails = select(UserEmailsSelectors.getUnverifiedEmails);

constructor() {
this.setupMetaTagsCleanup();
effect(() => {
if (this.unverifiedEmails().length) {
this.showEmailDialog();
Expand All @@ -50,15 +43,6 @@ export class AppComponent implements OnInit {
this.actions.getEmails();
}

private setupMetaTagsCleanup(): void {
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((event: NavigationEnd) => this.metaTagsService.clearMetaTagsIfNeeded(event.url));
}

private showEmailDialog() {
this.dialogService.open(ConfirmEmailComponent, {
width: '448px',
Expand Down
44 changes: 41 additions & 3 deletions src/app/features/files/pages/file-detail/file-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createDispatchMap, select, Store } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { Menu } from 'primeng/menu';
Expand All @@ -9,6 +9,7 @@ import { Tab, TabList, Tabs } from 'primeng/tabs';

import { switchMap } from 'rxjs';

import { DatePipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -37,8 +38,9 @@ import {
} from '@osf/features/metadata/store';
import { LoadingSpinnerComponent, MetadataTabsComponent, SubHeaderComponent } from '@osf/shared/components';
import { MetadataResourceEnum, ResourceType } from '@osf/shared/enums';
import { pathJoin } from '@osf/shared/helpers';
import { MetadataTabsModel, OsfFile } from '@osf/shared/models';
import { CustomConfirmationService, ToastService } from '@osf/shared/services';
import { CustomConfirmationService, MetaTagsService, ToastService } from '@osf/shared/services';

import {
FileKeywordsComponent,
Expand All @@ -58,6 +60,8 @@ import {
GetFileRevisions,
} from '../../store';

import { environment } from 'src/environments/environment';

@Component({
selector: 'osf-file-detail',
imports: [
Expand All @@ -80,6 +84,7 @@ import {
templateUrl: './file-detail.component.html',
styleUrl: './file-detail.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [DatePipe],
})
export class FileDetailComponent {
@HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full';
Expand All @@ -91,6 +96,9 @@ export class FileDetailComponent {
readonly sanitizer = inject(DomSanitizer);
readonly toastService = inject(ToastService);
readonly customConfirmationService = inject(CustomConfirmationService);
private readonly metaTags = inject(MetaTagsService);
private readonly datePipe = inject(DatePipe);
private readonly translateService = inject(TranslateService);

private readonly actions = createDispatchMap({
getFile: GetFile,
Expand All @@ -110,8 +118,11 @@ export class FileDetailComponent {
isFileLoading = select(FilesSelectors.isOpenedFileLoading);
cedarRecords = select(MetadataSelectors.getCedarRecords);
cedarTemplates = select(MetadataSelectors.getCedarTemplates);

isAnonymous = select(FilesSelectors.isFilesAnonymous);
fileCustomMetadata = select(FilesSelectors.getFileCustomMetadata);
resourceMetadata = select(FilesSelectors.getResourceMetadata);
resourceContributors = select(FilesSelectors.getContributors);

safeLink: SafeResourceUrl | null = null;
resourceId = '';
resourceType = '';
Expand Down Expand Up @@ -162,6 +173,33 @@ export class FileDetailComponent {
selectedCedarTemplate = signal<CedarMetadataDataTemplateJsonApi | null>(null);
cedarFormReadonly = signal<boolean>(true);

private readonly effectMetaTags = effect(() => {
const metaTagsData = this.metaTagsData();
if (metaTagsData) {
this.metaTags.updateMetaTags(metaTagsData, this.destroyRef);
}
});

private readonly metaTagsData = computed(() => {
const file = this.file();
if (!file) return null;
return {
title: this.fileCustomMetadata()?.title || file.name,
description:
this.fileCustomMetadata()?.description ??
this.translateService.instant('files.metaTagDescriptionPlaceholder'),
url: pathJoin(environment.webUrl, this.fileGuid),
publishedDate: this.datePipe.transform(file.dateCreated, 'yyyy-MM-dd'),
modifiedDate: this.datePipe.transform(file.dateModified, 'yyyy-MM-dd'),
language: this.fileCustomMetadata()?.language,
contributors: this.resourceContributors()?.map((contributor) => ({
fullName: contributor.fullName,
givenName: contributor.givenName,
familyName: contributor.familyName,
})),
};
});

constructor() {
this.route.params
.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,25 +363,26 @@ export class PreprintDetailsComponent extends DataciteTrackerComponent implement
}

private setMetaTags() {
const image = 'engines-dist/registries/assets/img/osf-sharing.png';

this.metaTags.updateMetaTags({
title: this.preprint()?.title,
description: this.preprint()?.description,
publishedDate: this.datePipe.transform(this.preprint()?.datePublished, 'yyyy-MM-dd'),
modifiedDate: this.datePipe.transform(this.preprint()?.dateModified, 'yyyy-MM-dd'),
url: pathJoin(environment.webUrl, this.preprint()?.id ?? ''),
image,
identifier: this.preprint()?.id,
doi: this.preprint()?.doi,
keywords: this.preprint()?.tags,
siteName: 'OSF',
license: this.preprint()?.embeddedLicense?.name,
contributors: this.contributors().map((contributor) => ({
givenName: contributor.fullName,
familyName: contributor.familyName,
})),
});
this.metaTags.updateMetaTags(
{
title: this.preprint()?.title,
description: this.preprint()?.description,
publishedDate: this.datePipe.transform(this.preprint()?.datePublished, 'yyyy-MM-dd'),
modifiedDate: this.datePipe.transform(this.preprint()?.dateModified, 'yyyy-MM-dd'),
url: pathJoin(environment.webUrl, this.preprint()?.id ?? ''),
identifier: this.preprint()?.id,
doi: this.preprint()?.doi,
keywords: this.preprint()?.tags,
siteName: 'OSF',
license: this.preprint()?.embeddedLicense?.name,
contributors: this.contributors().map((contributor) => ({
fullName: contributor.fullName,
givenName: contributor.givenName,
familyName: contributor.familyName,
})),
},
this.destroyRef,
);
}

private hasReadWriteAccess(): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/registry/registry.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('RegistryComponent', () => {
{ provide: DataciteService, useValue: dataciteService },
{
provide: MetaTagsService,
useValue: { updateMetaTagsForRoute: jest.fn() },
useValue: { updateMetaTags: jest.fn() },
},
],
}).compileComponents();
Expand Down
11 changes: 5 additions & 6 deletions src/app/features/registry/registry.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { select } from '@ngxs/store';
import { Observable } from 'rxjs';

import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, effect, HostBinding, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, DestroyRef, effect, HostBinding, inject } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { RouterOutlet } from '@angular/router';

Expand All @@ -29,6 +29,7 @@ export class RegistryComponent extends DataciteTrackerComponent {

private readonly metaTags = inject(MetaTagsService);
private readonly datePipe = inject(DatePipe);
private readonly destroyRef = inject(DestroyRef);

readonly registry = select(RegistryOverviewSelectors.getRegistry);
readonly registry$ = toObservable(select(RegistryOverviewSelectors.getRegistry));
Expand All @@ -48,28 +49,26 @@ export class RegistryComponent extends DataciteTrackerComponent {
}

private setMetaTags(): void {
const image = 'engines-dist/registries/assets/img/osf-sharing.png';

this.metaTags.updateMetaTagsForRoute(
this.metaTags.updateMetaTags(
{
title: this.registry()?.title,
description: this.registry()?.description,
publishedDate: this.datePipe.transform(this.registry()?.dateRegistered, 'yyyy-MM-dd'),
modifiedDate: this.datePipe.transform(this.registry()?.dateModified, 'yyyy-MM-dd'),
url: pathJoin(environment.webUrl, this.registry()?.id ?? ''),
image,
identifier: this.registry()?.id,
doi: this.registry()?.doi,
keywords: this.registry()?.tags,
siteName: 'OSF',
license: this.registry()?.license?.name,
contributors:
this.registry()?.contributors?.map((contributor) => ({
fullName: contributor.fullName,
givenName: contributor.givenName,
familyName: contributor.familyName,
})) ?? [],
},
'registries'
this.destroyRef,
);
}
}
5 changes: 3 additions & 2 deletions src/app/shared/models/meta-tags/meta-tag-author.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface MetaTagAuthor {
givenName: string;
familyName: string;
fullName?: string;
givenName?: string;
familyName?: string;
}
54 changes: 27 additions & 27 deletions src/app/shared/services/meta-tags.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { DestroyRef, Inject, Injectable } from '@angular/core';
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';

import { Content, DataContent, HeadTagDef, MetaTagAuthor, MetaTagsData } from '../models/meta-tags';
Expand All @@ -14,7 +14,7 @@ export class MetaTagsService {
type: 'article',
description: 'Hosted on the OSF',
language: 'en-US',
image: `${environment.webUrl}/static/img/preprints_assets/osf/sharing.png`,
image: `${environment.webUrl}/assets/images/osf-sharing.png`,
imageType: 'image/png',
imageWidth: 1200,
imageHeight: 630,
Expand All @@ -27,25 +27,23 @@ export class MetaTagsService {
};

private readonly metaTagClass = 'osf-dynamic-meta';
private currentRouteGroup: string | null = null;

// data from all active routed components that set meta tags
private metaTagStack: Array<{ metaTagsData: MetaTagsData; componentDestroyRef: DestroyRef }> = [];

constructor(
private meta: Meta,
private title: Title,
@Inject(DOCUMENT) private document: Document
) {}

updateMetaTags(metaTagsData: MetaTagsData): void {
const combinedData = { ...this.defaultMetaTags, ...metaTagsData };
const headTags = this.getHeadTags(combinedData);

this.applyHeadTags(headTags);
this.dispatchZoteroEvent();
}

updateMetaTagsForRoute(metaTagsData: MetaTagsData, routeGroup: string): void {
this.currentRouteGroup = routeGroup;
this.updateMetaTags(metaTagsData);
updateMetaTags(metaTagsData: MetaTagsData, componentDestroyRef: DestroyRef): void {
this.metaTagStack = [...this.metaTagStackWithout(componentDestroyRef), { metaTagsData, componentDestroyRef }];
componentDestroyRef.onDestroy(() => {
this.metaTagStack = this.metaTagStackWithout(componentDestroyRef);
this.applyNearestMetaTags();
});
this.applyNearestMetaTags();
}

clearMetaTags(): void {
Expand All @@ -62,27 +60,28 @@ export class MetaTagsService {
});

this.title.setTitle(String(this.defaultMetaTags.siteName));
this.currentRouteGroup = null;
}

shouldClearMetaTags(newUrl: string): boolean {
if (!this.currentRouteGroup) return true;
return !newUrl.startsWith(`/${this.currentRouteGroup}`);
private metaTagStackWithout(destroyRefToRemove: DestroyRef) {
// get a copy of `this.metaTagStack` minus any entries with the given destroyRef
return this.metaTagStack.filter(({ componentDestroyRef }) => componentDestroyRef !== destroyRefToRemove);
}

clearMetaTagsIfNeeded(newUrl: string): void {
if (this.shouldClearMetaTags(newUrl)) {
private applyNearestMetaTags() {
// apply the meta tags for the nearest active route that called `updateMetaTags` (if any)
const nearest = this.metaTagStack.at(-1);
if (nearest) {
this.applyMetaTagsData(nearest.metaTagsData);
} else {
this.clearMetaTags();
}
}

resetToDefaults(): void {
this.updateMetaTags({});
}
};

getHeadTagsPublic(metaTagsData: MetaTagsData): HeadTagDef[] {
private applyMetaTagsData(metaTagsData: MetaTagsData) {
const combinedData = { ...this.defaultMetaTags, ...metaTagsData };
return this.getHeadTags(combinedData);
const headTags = this.getHeadTags(combinedData);
this.applyHeadTags(headTags);
this.dispatchZoteroEvent();
}

private getHeadTags(metaTagsData: MetaTagsData): HeadTagDef[] {
Expand Down Expand Up @@ -173,6 +172,7 @@ export class MetaTagsService {
.filter((person): person is MetaTagAuthor => typeof person === 'object' && person !== null)
.map((person) => ({
'@type': 'schema:Person',
name: person.fullName,
givenName: person.givenName,
familyName: person.familyName,
}));
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,7 @@
"title": "Files",
"storageLocation": "OSF Storage",
"searchPlaceholder": "Search your projects",
"metaTagDescriptionPlaceholder": "Presented by OSF",
"sort": {
"placeholder": "Sort",
"nameAZ": "Name: A-Z",
Expand Down
Binary file added src/assets/images/osf-sharing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.