diff --git a/src/app/core/login-interstitials-manager/login-main-interstitials-manager.service.ts b/src/app/core/login-interstitials-manager/login-main-interstitials-manager.service.ts index ecc9325376..42d774b140 100644 --- a/src/app/core/login-interstitials-manager/login-main-interstitials-manager.service.ts +++ b/src/app/core/login-interstitials-manager/login-main-interstitials-manager.service.ts @@ -36,6 +36,11 @@ export class LoginMainInterstitialsManagerService { LoginDomainInterstitialManagerService: LoginDomainInterstitialManagerService, LoginAffiliationInterstitialManagerService: LoginAffiliationInterstitialManagerService ) { + // Delare here all the interstitial services. + // This are the entry points to add new interstitials. + // They should be added in the order they should be checked. + // The first one that returns a component or a dialog subscription will be used. + // The rest will be ignored. this.interstitialServices = [ LoginDomainInterstitialManagerService, LoginAffiliationInterstitialManagerService, diff --git a/src/app/core/observability-events/observability-events.service.ts b/src/app/core/observability-events/observability-events.service.ts index 163b5ea363..ed768cca71 100644 --- a/src/app/core/observability-events/observability-events.service.ts +++ b/src/app/core/observability-events/observability-events.service.ts @@ -1,7 +1,11 @@ import { Inject, Injectable } from '@angular/core' import { WINDOW } from 'src/app/cdk/window' -export type JourneyType = 'orcid_registration' | 'orcid_update_emails' +export type JourneyType = + | 'orcid_registration' + | 'orcid_update_emails' + | 'orcid_with_notifications' + | 'orcid_without_notifications' @Injectable({ providedIn: 'root', }) @@ -26,7 +30,7 @@ export class CustomEventService { if (runtimeEnvironment.debugger) { console.debug( - `-> Journey "${journeyType}" started at ${this.journeys[journeyType].startTime}`, + `[RUM][journey:${journeyType}] : start`, attributes ) } @@ -58,14 +62,14 @@ export class CustomEventService { eventName, elapsedTime, } - if (typeof (this.window as any)?.addPageAction === 'function') { + if (typeof (this.window as any).newrelic?.addPageAction === 'function') { ;(this.window as any).newrelic.addPageAction(journeyType, eventAttributes) } // Send the custom event to New Relic if (runtimeEnvironment.debugger) { console.debug( - `-> Event "${eventName}" recorded for journey "${journeyType}" with elapsed time ${elapsedTime}ms`, + `[RUM][journey:${journeyType}] : event ${eventName}`, eventAttributes ) } @@ -94,15 +98,18 @@ export class CustomEventService { } // Send the final custom event to New Relic - if (typeof (this.window as any)?.addPageAction === 'function') { - ;(this.window as any).addPageAction(journeyType, finalAttributes) + if (typeof (this.window as any).newrelic?.addPageAction === 'function') { + ;(this.window as any).newrelic?.addPageAction( + journeyType, + finalAttributes + ) } // Clean up the journey data delete this.journeys[journeyType] console.debug( - `Journey "${journeyType}" finished with elapsed time ${elapsedTime}ms`, + `[RUM][journey:${journeyType}] : finished`, finalAttributes ) } diff --git a/src/app/layout/header/header.component.scss b/src/app/layout/header/header.component.scss index 53de7303d7..0c182c63eb 100644 --- a/src/app/layout/header/header.component.scss +++ b/src/app/layout/header/header.component.scss @@ -195,3 +195,7 @@ nav { app-search { overflow: hidden; } + +app-user-menu { + display: flex; +} diff --git a/src/app/layout/layout.module.ts b/src/app/layout/layout.module.ts index 56682f60b0..b3a80e3ccd 100644 --- a/src/app/layout/layout.module.ts +++ b/src/app/layout/layout.module.ts @@ -21,6 +21,7 @@ import { BannerModule } from '../cdk/banner/banner.module' import { MaintenanceMessageComponent } from './maintenance-message/maintenance-message.component' import { MatDividerModule } from '@angular/material/divider' import { A11yLinkModule } from '../cdk/a11y-link/a11y-link.module' +import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip' @NgModule({ imports: [ @@ -37,6 +38,7 @@ import { A11yLinkModule } from '../cdk/a11y-link/a11y-link.module' BannerModule, MatDividerModule, A11yLinkModule, + MatTooltipModule, ], declarations: [ HeaderComponent, diff --git a/src/app/layout/user-menu/user-menu.component.html b/src/app/layout/user-menu/user-menu.component.html index 4c1d4cda75..fd48a2d258 100644 --- a/src/app/layout/user-menu/user-menu.component.html +++ b/src/app/layout/user-menu/user-menu.component.html @@ -29,6 +29,35 @@ Sign in / Register + - @@ -133,3 +165,4 @@ Logout + diff --git a/src/app/layout/user-menu/user-menu.component.scss b/src/app/layout/user-menu/user-menu.component.scss index a5192826ab..3e285730b0 100644 --- a/src/app/layout/user-menu/user-menu.component.scss +++ b/src/app/layout/user-menu/user-menu.component.scss @@ -1,3 +1,8 @@ +section { + display: flex; + flex-wrap: nowrap; +} + .main-button { height: auto; .main-button-container { @@ -37,7 +42,6 @@ :host { .user-menu-button { max-width: 100%; - padding: 0 32px; .columns-4 & { padding: 0; } @@ -69,7 +73,14 @@ span.name-text-container { overflow: hidden; - width: calc(100% - 59px); + max-width: calc(102% - 55px); display: inline-block; text-overflow: ellipsis; + width: fit-content; +} + +[mat-stroked-button] { + min-width: 40px; + padding: 0; + margin: 0 16px; } diff --git a/src/app/layout/user-menu/user-menu.component.ts b/src/app/layout/user-menu/user-menu.component.ts index ff0c38ab96..11ad97536c 100644 --- a/src/app/layout/user-menu/user-menu.component.ts +++ b/src/app/layout/user-menu/user-menu.component.ts @@ -9,6 +9,7 @@ import { ApplicationRoutes } from 'src/app/constants' import { TogglzService } from 'src/app/core/togglz/togglz.service' import { InboxService } from '../../core/inbox/inbox.service' import { first } from 'rxjs/operators' +import { CustomEventService } from 'src/app/core/observability-events/observability-events.service' @Component({ selector: 'app-user-menu', @@ -25,8 +26,11 @@ export class UserMenuComponent implements OnInit { platform: PlatformInfo labelSigninRegister = $localize`:@@layout.ariaLabelSigninRegister:Sign in to ORCID or register for your ORCID iD` labelUserMenu = $localize`:@@layout.ariaLabelUserMenu:User menu` + notificationTooltipActive = $localize`:@@layout.notificationTooltip:You have unread notifications` + notificationTooltip = $localize`:@@layout.notificationTooltipInactive:Notifications inbox` isAccountDelegate: boolean inboxUnread = 0 + userJourney!: 'orcid_with_notifications' | 'orcid_without_notifications' constructor( private _router: Router, @@ -34,7 +38,8 @@ export class UserMenuComponent implements OnInit { @Inject(WINDOW) private window: Window, _platform: PlatformInfoService, private _inboxService: InboxService, - private _togglz: TogglzService + private _togglz: TogglzService, + private observabilityEventService: CustomEventService ) { _userInfo.getUserSession().subscribe((data) => { if (data.loggedIn) { @@ -56,15 +61,27 @@ export class UserMenuComponent implements OnInit { this._inboxService .retrieveUnreadCount() .pipe(first()) - .subscribe((inboxUnread) => (this.inboxUnread = inboxUnread)) + .subscribe((inboxUnread) => { + ;(this.userJourney = + inboxUnread > 0 + ? 'orcid_with_notifications' + : 'orcid_without_notifications'), + this.observabilityEventService.startJourney( + this.userJourney, + + { inboxUnread } + ) + this.inboxUnread = inboxUnread + }) } - goto(url) { + goto(url, from?: string) { if (url === 'my-orcid') { this._router.navigate([ApplicationRoutes.myOrcid]) } else if (url === 'signin') { this._router.navigate([ApplicationRoutes.signin]) } else if (url === 'inbox') { + this.observabilityEventService.recordEvent(this.userJourney, from) this._router.navigate([ApplicationRoutes.inbox]) } else if (url === 'account') { this._router.navigate([ApplicationRoutes.account]) diff --git a/src/assets/vectors/notification-button-active.svg b/src/assets/vectors/notification-button-active.svg new file mode 100644 index 0000000000..94d7c78a4b --- /dev/null +++ b/src/assets/vectors/notification-button-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/vectors/notification-button.svg b/src/assets/vectors/notification-button.svg new file mode 100644 index 0000000000..23a55262e7 --- /dev/null +++ b/src/assets/vectors/notification-button.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/environments/environment.local.4200.ts b/src/environments/environment.local.4200.ts index aab0be1592..d0497860a8 100644 --- a/src/environments/environment.local.4200.ts +++ b/src/environments/environment.local.4200.ts @@ -7,7 +7,7 @@ export const environment: EnvironmentInterface = { API_NEWS: 'https://www.mocky.io/v2/5dced45b3000007300931ce8', API_PUB: `///v3.0`, API_WEB: `///`, - AUTH_SERVER: 'https://auth./', + AUTH_SERVER: '///auth/', BASE_URL: '///', INFO_SITE: 'https://info.orcid.org/', GOOGLE_ANALYTICS_TESTING_MODE: true, diff --git a/src/locale/properties/layout/layout.en.properties b/src/locale/properties/layout/layout.en.properties index 3fd6552239..b5277fc84a 100644 --- a/src/locale/properties/layout/layout.en.properties +++ b/src/locale/properties/layout/layout.en.properties @@ -114,3 +114,5 @@ footer.cookieSettings=Cookie Settings footer.ariaLabelLicense=license layout.ariaLabelConnectingResearchers=Connecting research and researchers layout.ariaLabelSearchRegistry=Search the ORCID registry... +layout.notificationTooltip=You have unread notifications +layout.notificationTooltipInactive=Notifications inbox \ No newline at end of file diff --git a/src/proxy.conf.qa.mjs b/src/proxy.conf.qa.mjs index fc98db5caf..a5f99d9eeb 100644 --- a/src/proxy.conf.qa.mjs +++ b/src/proxy.conf.qa.mjs @@ -1,60 +1,115 @@ -export default { - '/v3.0': { - target: 'https://pub.qa.orcid.org', - secure: false, - logLevel: 'debug', - changeOrigin: true, - cookieDomainRewrite: 'localhost', - bypass: function (req, res, proxyOptions) { - /// PRINT REQUEST PATH - if (req.headers.accept?.includes('html')) { - return '/index.html' - } - req.headers['X-Dev-Header'] = 'local-host-proxy-call' - }, - onProxyRes: responseOverights(), - }, - '/': { - target: 'https://qa.orcid.org', - secure: false, - logLevel: 'debug', - changeOrigin: true, - cookieDomainRewrite: 'localhost', - onProxyRes: responseOverights(), +// proxy.conf.qa.mjs (ESM) - bypass: function (req, res, proxyOptions) { - if (req.headers.accept?.includes('html') && req.path !== '/signout') { - return '/index.html' - } - req.headers['X-Dev-Header'] = 'local-host-proxy-call' - }, - }, +/** + * ────────────────────────────────────────────────────────────────────────────── + * Bypass hook: if the browser is requesting HTML, serve index.html; + * otherwise, inject a dev header so backend knows it’s from local dev. + * ────────────────────────────────────────────────────────────────────────────── + */ +function bypassHtmlOrJson(req, res) { + if (req.headers.accept?.includes('html')) { + return '/index.html' + } + req.headers['X-Dev-Header'] = 'local-host-proxy-call' } -function responseOverights() { + +/** + * ────────────────────────────────────────────────────────────────────────────── + * onProxyReq for /auth: before sending to auth.qa.orcid.org, + * force Origin/Referer to https://qa.orcid.org + * ────────────────────────────────────────────────────────────────────────────── + */ +function proxyReqOverrideHeaders(proxyReq /* http.ClientRequest */, req, res) { + proxyReq.setHeader('Origin', 'https://qa.orcid.org') + proxyReq.setHeader('Referer', 'https://qa.orcid.org') +} + +/** + * ────────────────────────────────────────────────────────────────────────────── + * responseOverridesAuth: for /auth proxy responses. + * 1) Rewrite Set-Cookie domain from qa.orcid.org → localhost + * 2) If a 3xx redirect points at auth.qa.orcid.org/login → localhost:4200/auth/login + * ────────────────────────────────────────────────────────────────────────────── + */ +function responseOverridesAuth() { return (proxyRes, req, res) => { - // Grab the existing 'set-cookie' headers const cookies = proxyRes.headers['set-cookie'] - if (cookies) { - // Transform each cookie - const newCookies = cookies.map((cookie) => { - // Example: rewrite "Domain=qa.orcid.org" to "Domain=localhost" - return cookie.replace(/Domain=\.?qa\.orcid\.org/i, 'Domain=localhost') - }) - - // Put the transformed cookies back into the response header - proxyRes.headers['set-cookie'] = newCookies + if (Array.isArray(cookies)) { + proxyRes.headers['set-cookie'] = cookies.map((cookie) => + cookie.replace(/Domain=\.?(qa\.)?orcid\.org/i, 'Domain=localhost') + ) } + if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400) { + let location = proxyRes.headers['location'] + if (typeof location === 'string') { + proxyRes.headers['location'] = location.replace( + 'http://auth.qa.orcid.org/login', + 'http://localhost:4200/auth/login' + ) + } + } + } +} - // Check for 3xx (especially 302) status codes: +/** + * ────────────────────────────────────────────────────────────────────────────── + * responseOverridesGeneric: for root (/) proxies. + * 1) Rewrite Set-Cookie domain from qa.orcid.org → localhost + * 2) If a 3xx redirect points at qa.orcid.org/signin → /signin + * ────────────────────────────────────────────────────────────────────────────── + */ +function responseOverridesGeneric() { + return (proxyRes, req, res) => { + const cookies = proxyRes.headers['set-cookie'] + if (Array.isArray(cookies)) { + proxyRes.headers['set-cookie'] = cookies.map((cookie) => + cookie.replace(/Domain=\.?(qa\.)?orcid\.org/i, 'Domain=localhost') + ) + } if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400) { let location = proxyRes.headers['location'] - if (location) { - location = location.replace( + if (typeof location === 'string') { + proxyRes.headers['location'] = location.replace( 'https://qa.orcid.org/signin', 'http://localhost:4200/signin' ) - proxyRes.headers['location'] = location } } } } + +/** + * ────────────────────────────────────────────────────────────────────────────── + * Finally, export the proxy table as an ES module default export. + * ────────────────────────────────────────────────────────────────────────────── + */ +export default { + '/v3.0': { + target: 'https://pub.qa.orcid.org', + secure: false, + changeOrigin: true, + logLevel: 'debug', + cookieDomainRewrite: 'localhost', + }, + + '/auth': { + target: 'https://auth.qa.orcid.org', + secure: false, + changeOrigin: true, + logLevel: 'debug', + cookieDomainRewrite: 'localhost', + pathRewrite: { '^/auth': '' }, + onProxyReq: proxyReqOverrideHeaders, + onProxyRes: responseOverridesAuth(), + }, + + '/': { + target: 'https://qa.orcid.org', + secure: false, + changeOrigin: true, + logLevel: 'debug', + cookieDomainRewrite: 'localhost', + bypass: bypassHtmlOrJson, + onProxyRes: responseOverridesGeneric(), + }, +}