Skip to content

Commit 44d455c

Browse files
authored
♻️(frontend) support legacy and new widget attribute (#650)
The new widget loader consume `window._lasuite_widget` property to know which widget to load. The previous version was using `window._stmsg_header`. We refactor widget loading logic to support both version with ease.
1 parent 99caf32 commit 44d455c

4 files changed

Lines changed: 95 additions & 117 deletions

File tree

src/frontend/src/features/ui/components/feedback-button/index.tsx

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Button, ButtonProps, Tooltip } from "@gouvfr-lasuite/cunningham-react"
33
import { useTranslation } from "react-i18next"
44
import { useAuth } from "@/features/auth";
55
import { useState } from "react";
6+
import { WidgetHelper } from "@/features/utils/widget-helper";
67

78
type SurveyButtonProps = ButtonProps & {
89
/** Display only icon without label */
@@ -37,45 +38,27 @@ export const SurveyButton = ({ iconOnly = false, ...props }: SurveyButtonProps)
3738
const closeLabel: string = t("Close the feedback widget");
3839

3940
const showWidget = () => {
40-
// Initialize the widget array if it doesn't exist
41-
if (typeof window !== "undefined" && widgetPath) {
42-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
43-
(window as any)._lasuite_widget = (window as any)._lasuite_widget || [];
44-
45-
// Construct script URLs from the base path
46-
const feedbackScript = `${widgetPath}feedback.js`;
47-
48-
// Push the widget configuration
49-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50-
(window as any)._lasuite_widget.push([
51-
"feedback",
52-
"init",
53-
{
54-
title,
55-
api: apiUrl,
56-
channel,
57-
placeholder,
58-
emailPlaceholder,
59-
submitText,
60-
successText,
61-
successText2,
62-
closeLabel,
63-
// Add email parameter if user is logged in
64-
...(user?.email && { email: user.email }),
65-
},
66-
]);
67-
68-
// Load the loader script if not already loaded
69-
if (!document.querySelector(`script[src="${feedbackScript}"]`)) {
70-
const script = document.createElement("script");
71-
script.async = true;
72-
script.src = feedbackScript;
73-
const firstScript = document.getElementsByTagName("script")[0];
74-
if (firstScript && firstScript.parentNode) {
75-
firstScript.parentNode.insertBefore(script, firstScript);
76-
}
77-
}
78-
}
41+
if (typeof window === "undefined" || !widgetPath) return;
42+
43+
WidgetHelper.pushCommand([
44+
"feedback",
45+
"init",
46+
{
47+
title,
48+
api: apiUrl,
49+
channel,
50+
placeholder,
51+
emailPlaceholder,
52+
submitText,
53+
successText,
54+
successText2,
55+
closeLabel,
56+
// Add email parameter if user is logged in
57+
...(user?.email && { email: user.email }),
58+
},
59+
]);
60+
61+
WidgetHelper.loadScript(`${widgetPath}feedback.js`);
7962
}
8063

8164
const openHelpCenter = () => {

src/frontend/src/features/ui/components/feedback-widget/index.tsx

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect } from "react";
22
import { useTranslation } from "react-i18next";
33
import { useAuth } from "@/features/auth";
4+
import { WidgetHelper } from "@/features/utils/widget-helper";
45

56
interface FeedbackWidgetProps {
67
widget?: string;
@@ -26,54 +27,34 @@ export function FeedbackWidget({
2627

2728
useEffect(() => {
2829
if (!channel || !apiUrl || !widgetPath) return;
29-
30-
// Initialize the widget array if it doesn't exist
31-
if (typeof window !== "undefined" && widgetPath) {
32-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33-
(window as any)._lasuite_widget = (window as any)._lasuite_widget || [];
34-
35-
// Construct script URLs from the base path
36-
const loaderScript = `${widgetPath}loader.js`;
37-
const feedbackScript = `${widgetPath}feedback.js`;
38-
39-
// Push the widget configuration
40-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41-
(window as any)._lasuite_widget.push([
42-
"loader",
43-
"init",
44-
{
45-
params: {
46-
title,
47-
api: apiUrl,
48-
channel,
49-
placeholder,
50-
emailPlaceholder,
51-
submitText,
52-
successText,
53-
successText2,
54-
closeLabel,
55-
// Add email parameter if user is logged in
56-
...(user?.email && { email: user.email }),
57-
},
58-
script: feedbackScript,
59-
widget,
60-
label: title,
30+
if (typeof window === "undefined") return;
31+
32+
WidgetHelper.pushCommand([
33+
"loader",
34+
"init",
35+
{
36+
params: {
37+
title,
38+
api: apiUrl,
39+
channel,
40+
placeholder,
41+
emailPlaceholder,
42+
submitText,
43+
successText,
44+
successText2,
6145
closeLabel,
46+
// Add email parameter if user is logged in
47+
...(user?.email && { email: user.email }),
6248
},
63-
]);
64-
65-
// Load the loader script if not already loaded
66-
if (!document.querySelector(`script[src="${loaderScript}"]`)) {
67-
const script = document.createElement("script");
68-
script.async = true;
69-
script.src = loaderScript;
70-
const firstScript = document.getElementsByTagName("script")[0];
71-
if (firstScript && firstScript.parentNode) {
72-
firstScript.parentNode.insertBefore(script, firstScript);
73-
}
74-
}
75-
}
76-
}, [title, channel, apiUrl, widgetPath, widget, emailPlaceholder, submitText, successText, successText2, user?.email]);
49+
script: `${widgetPath}feedback.js`,
50+
widget,
51+
label: title,
52+
closeLabel,
53+
},
54+
]);
55+
56+
WidgetHelper.loadScript(`${widgetPath}loader.js`);
57+
}, [title, channel, apiUrl, widgetPath, widget, placeholder, emailPlaceholder, submitText, successText, successText2, closeLabel, user?.email]);
7758

7859
// This component doesn't render anything visible
7960
// The widget is injected via the script

src/frontend/src/features/ui/components/lagaufre/index.tsx

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useTranslation } from "react-i18next"
22
import { useState, useEffect, useRef } from "react"
33
import { Button, ButtonElement } from "@gouvfr-lasuite/cunningham-react";
4+
import { WidgetHelper } from "@/features/utils/widget-helper";
45

56
/**
67
* A button that opens the lagaufre widget
@@ -19,12 +20,6 @@ export const LagaufreButton = () => {
1920
useEffect(() => {
2021
if (typeof window == "undefined" || !widgetPath || !apiUrl) return;
2122

22-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23-
(window as any)._lasuite_widget = (window as any)._lasuite_widget || [];
24-
25-
// Construct script URLs from the base path
26-
const feedbackScript = `${widgetPath}lagaufre.js`;
27-
2823
document.addEventListener("stmsg-widget-lagaufre-closed", () => {
2924
// Focus the button
3025
buttonRef.current?.focus();
@@ -35,43 +30,26 @@ export const LagaufreButton = () => {
3530
buttonRef.current?.setAttribute("aria-expanded", "true");
3631
});
3732

38-
// Load the loader script if not already loaded
39-
if (!document.querySelector(`script[src="${feedbackScript}"]`)) {
40-
const script = document.createElement("script");
41-
script.async = true;
42-
script.src = feedbackScript;
43-
const firstScript = document.getElementsByTagName("script")[0];
44-
if (firstScript && firstScript.parentNode) {
45-
firstScript.parentNode.insertBefore(script, firstScript);
46-
}
47-
}
48-
49-
// Initialize the widget
50-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51-
(window as any)._lasuite_widget.push([
52-
"lagaufre",
53-
"init",
54-
{
33+
WidgetHelper.loadScript(`${widgetPath}lagaufre.js`);
34+
WidgetHelper.pushCommand([
35+
"lagaufre",
36+
"init",
37+
{
5538
api: apiUrl,
5639
label,
5740
closeLabel,
58-
position: 'fixed',
41+
position: "fixed",
5942
top: 53,
60-
right: 12
61-
},
43+
right: 12,
44+
},
6245
]);
6346

6447
setIsWidgetInitialized(true);
6548
}, [apiUrl, widgetPath, label, closeLabel]);
6649

6750
const toggleWidget = () => {
6851
if (!isWidgetInitialized) return;
69-
70-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71-
(window as any)._lasuite_widget.push([
72-
"lagaufre",
73-
"toggle"
74-
]);
52+
WidgetHelper.pushCommand(["lagaufre", "toggle"]);
7553
}
7654

7755
if (!widgetPath || !apiUrl) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Widget helper functions to share common logic
2+
3+
declare global {
4+
interface Window {
5+
_lasuite_widget?: unknown[];
6+
_stmsg_widget?: unknown[];
7+
}
8+
}
9+
10+
export class WidgetHelper {
11+
// Both keys are populated so that legacy and current widget runtimes
12+
// can consume the same command queue.
13+
static #QUEUE_KEYS = ["_lasuite_widget", "_stmsg_widget"] as const;
14+
15+
static pushCommand(command: unknown[]) {
16+
if (typeof window === "undefined") return;
17+
for (const key of WidgetHelper.#QUEUE_KEYS) {
18+
const queue = window[key] ?? [];
19+
queue.push(command);
20+
window[key] = queue;
21+
}
22+
}
23+
24+
static loadScript(scriptUrl: string) {
25+
if (typeof window === "undefined") return;
26+
if (document.querySelector(`script[src="${scriptUrl}"]`)) return;
27+
28+
const script = document.createElement("script");
29+
script.async = true;
30+
script.src = scriptUrl;
31+
const firstScript = document.getElementsByTagName("script")[0];
32+
if (firstScript && firstScript.parentNode) {
33+
firstScript.parentNode.insertBefore(script, firstScript);
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)