Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fikser context-lenker i header og håndterer client-side endring av context #200

Merged
merged 13 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading