Skip to content

Commit

Permalink
Fikser context-lenker i header og håndterer client-side endring av co…
Browse files Browse the repository at this point in the history
…ntext (#200)

Sørger for at context-lenker i headeren oppdaterer innlogget meny
client-side, og relaterte fixes.

- Innfører en web component for `<user-menu>`, som håndterer client-side
endringer og holder på state for innlogget meny
- Refactor sjekk av auth state. Sender nå et custom `authupdated` event
som relevante komponenter kan lytte på. Erstatter også nåværende
analyticsReady event med denne, som gjør samme nytte.
- Endrer context-link til å wrappe et anchor-element. 
- Innfører en helper-klasse som kan extendes av web components som skal
oppføre seg som lenker/anchors
  • Loading branch information
anders-nom authored Mar 5, 2024
1 parent 2f63e78 commit e5b8c1c
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 181 deletions.
14 changes: 5 additions & 9 deletions packages/client/src/events.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Auth } from './api';
import { Context, Params, type ParamKey } from 'decorator-shared/params';
import { Context, Params } from 'decorator-shared/params';

export type MessageEvent = {
hello: 'true';
};

export type CustomEvents = {
'analytics-ready-event': void;
'analytics-load-event': Auth;
activecontext: { context: Context };
paramsupdated: {
keys: ParamKey[];
params: Partial<Params>;
};
authupdated: {
auth: Auth;
};
};

Expand All @@ -34,9 +36,3 @@ export function createEvent<TName extends keyof CustomEvents>(name: TName, optio
export const analyticsReady = new CustomEvent('analytics-ready-event', {
bubbles: true,
});

export type AnalyticsLoaded = CustomEvent<Auth>;

export const analyticsLoaded = new CustomEvent<Auth>('analytics-loaded-event', {
bubbles: true,
});
17 changes: 17 additions & 0 deletions packages/client/src/helpers/custom-link-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export class CustomLinkElement extends HTMLElement {
protected readonly anchor: HTMLAnchorElement;

constructor() {
super();

this.anchor = document.createElement('a');
this.anchor.href = this.getAttribute('href') || '';
this.anchor.innerHTML = this.innerHTML;
this.anchor.classList.add(...this.classList);

this.classList.remove(...this.classList);

this.innerHTML = '';
this.appendChild(this.anchor);
}
}
51 changes: 7 additions & 44 deletions packages/client/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="./client.d.ts" />
import { formatParams } from 'decorator-shared/json';
import { LoginLevel, type Context } from 'decorator-shared/params';
import { type Context } from 'decorator-shared/params';
import Cookies from 'js-cookie';
import 'vite/modulepreload-polyfill';
import * as api from './api';
Expand All @@ -26,10 +26,9 @@ import './views/feedback';
import './views/login-button';
import './views/chatbot-wrapper';
import './views/sticky';

import { Auth } from './api';
import './views/user-menu';
import { addFaroMetaData } from './faro';
import { analyticsLoaded, analyticsReady, createEvent } from './events';
import { analyticsReady, createEvent } from './events';
import { type ParamKey } from 'decorator-shared/params';
import { param, hasParam, updateDecoratorParams, env } from './params';
import { makeEndpointFactory } from 'decorator-shared/urls';
Expand Down Expand Up @@ -105,52 +104,16 @@ window.addEventListener('activecontext', (event) => {
});
});

async function populateLoggedInMenu(authObject: Auth) {
fetch(
`${env('APP_URL')}/user-menu?${formatParams({
...window.__DECORATOR_DATA__.params,
name: authObject.name,
level: `Level${authObject.securityLevel}` as LoginLevel,
})}`,
{
credentials: 'include',
}
)
.then((res) => res.text())
.then((html) => {
const userMenu = document.querySelector('user-menu');
if (userMenu) {
userMenu.outerHTML = html;
}
});
}

//
// await populateLoggedInMenu(response);

const init = async () => {
const response = await api.checkAuth();

dispatchEvent(
new CustomEvent(analyticsLoaded.type, {
bubbles: true,
detail: { response },
})
);

if (!response.authenticated) {
return;
}

await populateLoggedInMenu(response);
const authResponse = await api.checkAuth();
dispatchEvent(createEvent('authupdated', { detail: { auth: authResponse } }));
};

init();

window.addEventListener(analyticsReady.type, () => {
window.addEventListener(analyticsLoaded.type, (e) => {
const response = (e as CustomEvent<Auth>).detail;
window.logPageView(window.__DECORATOR_DATA__.params, response);
window.addEventListener('authupdated', (e) => {
window.logPageView(window.__DECORATOR_DATA__.params, e.detail.auth);
window.startTaskAnalyticsSurvey(window.__DECORATOR_DATA__);
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const updateDecoratorParams = (params: Partial<Params>) => {

window.dispatchEvent(
createEvent('paramsupdated', {
detail: { keys: Object.keys(params) as ParamKey[] },
detail: { params },
})
);
};
Expand Down
67 changes: 33 additions & 34 deletions packages/client/src/views/context-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,47 @@ import { erNavDekoratoren } from 'decorator-shared/urls';
import headerClasses from '../styles/header.module.css';
import { tryParse } from 'decorator-shared/json';
import { type AnalyticsEventArgs } from '../analytics/constants';
import { createEvent } from '../events';
import { createEvent, CustomEvents } from '../events';
import { Context } from 'decorator-shared/params';
import { CustomLinkElement } from '../helpers/custom-link-element';

class ContextLink extends HTMLElement {
handleActiveContext = (event: Event) => {
this.classList.toggle(
headerClasses.lenkeActive,
this.getAttribute('data-context') === (event as CustomEvent<{ context: string }>).detail.context
);
class ContextLink extends CustomLinkElement {
handleActiveContext = (event: CustomEvent<CustomEvents['activecontext']>) => {
this.anchor.classList.toggle(headerClasses.lenkeActive, this.getAttribute('data-context') === event.detail.context);
};

connectedCallback() {
const attachContext = this.getAttribute('data-attach-context') === 'true';
handleClick = (e: MouseEvent) => {
if (erNavDekoratoren(window.location.href)) {
e.preventDefault();
}

const rawEventArgs = this.getAttribute('data-analytics-event-args');
const eventArgs = tryParse<AnalyticsEventArgs, null>(rawEventArgs, null);
const attachContext = this.getAttribute('data-attach-context') === 'true';

window.addEventListener('activecontext', this.handleActiveContext);
this.dispatchEvent(
createEvent('activecontext', {
bubbles: true,
detail: {
context: this.getAttribute('data-context') as Context,
},
})
);

if (eventArgs) {
const payload = {
...eventArgs,
...(attachContext && {
context: window.__DECORATOR_DATA__.params.context,
}),
};
window.analyticsEvent(payload);
}
};

this.addEventListener('click', (e) => {
if (erNavDekoratoren(window.location.href)) {
e.preventDefault();
}

this.dispatchEvent(
createEvent('activecontext', {
bubbles: true,
detail: {
context: this.getAttribute('data-context') as Context,
},
})
);

if (eventArgs) {
const payload = {
...eventArgs,
...(attachContext && {
context: window.__DECORATOR_DATA__.params.context,
}),
};
window.analyticsEvent(payload);
}
});
connectedCallback() {
window.addEventListener('activecontext', this.handleActiveContext);
this.addEventListener('click', this.handleClick);
}

disconnectedCallback() {
Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/views/decorator-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { LanguageSelector } from './language-selector';

class DecoratorUtils extends HTMLElement {
languageSelector: LanguageSelector;
breadbrumbs: HTMLElement;
breadcrumbs: HTMLElement;

constructor() {
super();

this.languageSelector = this.querySelector('language-selector') as LanguageSelector;
this.breadbrumbs = this.querySelector('nav[is="d-breadcrumbs"]') as HTMLElement;
this.breadcrumbs = this.querySelector('nav[is="d-breadcrumbs"]') as HTMLElement;
}

update = () => {
Expand All @@ -23,7 +23,7 @@ class DecoratorUtils extends HTMLElement {

this.languageSelector.availableLanguages = availableLanguages;
this.languageSelector.language = language;
this.breadbrumbs.innerHTML = Breadcrumbs({ breadcrumbs })?.render() ?? '';
this.breadcrumbs.innerHTML = Breadcrumbs({ breadcrumbs })?.render() ?? '';
};

set utilsBackground(utilsBackground: UtilsBackground) {
Expand Down
13 changes: 2 additions & 11 deletions packages/client/src/views/lenke-med-sporing.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import type { AnalyticsEventArgs } from '../analytics/constants';
import { tryParse } from 'decorator-shared/json';
import { CustomLinkElement } from '../helpers/custom-link-element';

export class LenkeMedSporingElement extends HTMLElement {
export class LenkeMedSporingElement extends CustomLinkElement {
constructor() {
super();

const attachContext = this.getAttribute('data-attach-context') === 'true';
const rawEventArgs = this.getAttribute('data-analytics-event-args');
const eventArgs = tryParse<AnalyticsEventArgs, null>(rawEventArgs, null);

const a = document.createElement('a');
a.href = this.getAttribute('href') || '';
a.innerHTML = this.innerHTML;
a.classList.add(...this.classList);

this.classList.remove(...this.classList);

this.innerHTML = '';
this.appendChild(a);

this.addEventListener('click', () => {
if (eventArgs) {
const payload = {
Expand Down
82 changes: 82 additions & 0 deletions packages/client/src/views/user-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { CustomEvents } from '../events';
import { LoginLevel } from 'decorator-shared/params';
import { makeEndpointFactory } from 'decorator-shared/urls';
import { env } from '../params';
import { Auth } from '../api';

class UserMenu extends HTMLElement {
private readonly responseCache: Record<string, string> = {};

// TODO: use a global auth state instead?
private authState: Auth | null = null;

private updateAuthState(e: CustomEvent<CustomEvents['authupdated']>) {
this.authState = e.detail.auth;
this.populateLoggedInMenu();
}

private updateMenu(e: CustomEvent<CustomEvents['paramsupdated']>) {
const contextFromParams = e.detail.params?.context;
if (!contextFromParams) {
return;
}

this.populateLoggedInMenu();
}

private async fetchMenuHtml() {
if (!this.authState?.authenticated) {
return null;
}

const url = makeEndpointFactory(() => window.__DECORATOR_DATA__.params, env('APP_URL'))('/user-menu', {
name: this.authState.name,
level: `Level${this.authState.securityLevel}` as LoginLevel,
});

return fetch(url, {
credentials: 'include',
}).then((res) => res.text());
}

private getCacheKey() {
const { context, level, language } = window.__DECORATOR_DATA__.params;
return `${context}_${language}_${level}`;
}

private async populateLoggedInMenu() {
const cacheKey = this.getCacheKey();
const cachedHtml = this.responseCache[cacheKey];

if (cachedHtml) {
console.log('Found user menu in cache');
this.innerHTML = cachedHtml;
return;
}

this.fetchMenuHtml()
.then((html) => {
if (!html) {
throw Error('No HTML found!');
}

this.innerHTML = html;
this.responseCache[cacheKey] = html;
})
.catch((e) => {
console.error(`Failed to fetch logged in menu - ${e}`);
});
}

private connectedCallback() {
window.addEventListener('paramsupdated', this.updateMenu.bind(this));
window.addEventListener('authupdated', this.updateAuthState.bind(this));
}

private disconnectedCallback() {
window.removeEventListener('paramsupdated', this.updateMenu);
window.removeEventListener('authupdated', this.updateAuthState);
}
}

customElements.define('user-menu', UserMenu);
Loading

0 comments on commit e5b8c1c

Please sign in to comment.