Skip to content

Commit 4b0911f

Browse files
authored
Merge pull request #71 from GleapSDK/Checklist-component
v14.4.0
2 parents 3352a2d + 9959b6b commit 4b0911f

File tree

13 files changed

+1641
-54
lines changed

13 files changed

+1641
-54
lines changed

build/cjs/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/esm/index.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

demo/index.html

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
flex: 1;
9292
padding: 3rem;
9393
display: grid;
94-
grid-template-columns: repeat(3, 1fr);
94+
grid-template-columns: repeat(1, 1fr);
9595
grid-gap: 2rem;
9696
align-items: start;
9797
justify-content: start;
@@ -157,24 +157,20 @@
157157
<div class="sidebar-block" style="width: 70%"></div>
158158
<div class="sidebar-block" style="width: 67%"></div>
159159
<div class="sidebar-block" style="width: 55%"></div>
160+
161+
<div style="padding: 12px;">
162+
<gleap-checklist
163+
floating="true"
164+
checklistid="67f0ccc770eef2e4332c5237"
165+
></gleap-checklist>
166+
</div>
160167
</aside>
161168

162169
<!-- Main content wireframe blocks -->
163170
<main class="main-placeholder" id="main1">
164-
<div class="wireframe-block"></div>
165-
<div class="wireframe-block"></div>
166-
<div class="wireframe-block"></div>
167-
<div class="wireframe-block"></div>
168-
<div class="wireframe-block"></div>
169-
<div class="wireframe-block"></div>
170-
<div class="wireframe-block"></div>
171-
<div class="wireframe-block"></div>
172-
<div class="wireframe-block"></div>
173-
<div class="wireframe-block"></div>
174-
<div class="wireframe-block"></div>
175-
<div class="wireframe-block"></div>
176-
<div class="wireframe-block"></div>
177-
<div class="wireframe-block"></div>
171+
<gleap-checklist
172+
checklistid="67f0ccc770eef2e4332c5237"
173+
></gleap-checklist>
178174
</main>
179175

180176
<main class="main-placeholder-2" id="main2" style="display: none">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gleap",
3-
"version": "14.3.3",
3+
"version": "14.4.0",
44
"main": "build/cjs/index.js",
55
"module": "build/esm/index.mjs",
66
"exports": {

published/14.4.0/index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

published/latest/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ChecklistNetworkManager.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { GleapSession, GleapTranslationManager } from "./Gleap"; // Adjust path if needed
2+
3+
// Enum for request states
4+
const RequestStatus = {
5+
PENDING: "pending",
6+
SUCCESS: "success",
7+
ERROR: "error",
8+
};
9+
10+
class ChecklistNetworkManager {
11+
static instance = null;
12+
13+
/** @private {Map<string, Promise<string>>} outboundId -> Promise<internalId> */
14+
validationRequests = new Map();
15+
/** @private {Map<string, { status: RequestStatus, internalId?: string, error?: any }>} */
16+
validationCache = new Map(); // Caches final results (success/error)
17+
18+
/** @private {Map<string, Promise<any>>} internalId -> Promise<checklistData> */
19+
fetchRequests = new Map();
20+
/** @private {Map<string, { status: RequestStatus, data?: any, error?: any }>} */
21+
fetchCache = new Map(); // Caches final results (success/error)
22+
23+
// Private constructor for Singleton
24+
constructor() {
25+
if (ChecklistNetworkManager.instance) {
26+
return ChecklistNetworkManager.instance;
27+
}
28+
ChecklistNetworkManager.instance = this;
29+
}
30+
31+
/**
32+
* Gets the singleton instance of the ChecklistNetworkManager.
33+
* @returns {ChecklistNetworkManager} The singleton instance.
34+
*/
35+
static getInstance() {
36+
if (!ChecklistNetworkManager.instance) {
37+
ChecklistNetworkManager.instance = new ChecklistNetworkManager();
38+
}
39+
return ChecklistNetworkManager.instance;
40+
}
41+
42+
clearCache() {
43+
this.validationCache.clear();
44+
this.fetchCache.clear();
45+
this.validationRequests.clear();
46+
this.fetchRequests.clear();
47+
}
48+
49+
/**
50+
* @private
51+
* Gets common query parameters for API requests.
52+
* @returns {string} Query parameter string.
53+
*/
54+
_getQueryParams() {
55+
const gleapSessionInstance = GleapSession.getInstance();
56+
const session = gleapSessionInstance?.session;
57+
const lang =
58+
GleapTranslationManager.getInstance().getActiveLanguage() || "en";
59+
return `gleapId=${session?.gleapId || ""}&gleapHash=${
60+
session?.gleapHash || ""
61+
}&lang=${lang}`;
62+
}
63+
64+
/**
65+
* @private
66+
* Gets the API base URL.
67+
* @returns {string | null} The API URL or null if not configured.
68+
*/
69+
_getApiUrl() {
70+
const gleapSessionInstance = GleapSession.getInstance();
71+
return gleapSessionInstance?.apiUrl || null;
72+
}
73+
74+
/**
75+
* @private
76+
* Makes an XMLHttpRequest and returns a Promise.
77+
* @param {string} method - HTTP method.
78+
* @param {string} url - The request URL.
79+
* @param {object|null} data - Data to send in the request body.
80+
* @returns {Promise<any>} Promise resolving with parsed JSON response on success, rejecting on error.
81+
*/
82+
_makeRequest(method, url, data) {
83+
return new Promise((resolve, reject) => {
84+
const xhr = new XMLHttpRequest();
85+
xhr.open(method, url);
86+
87+
const gleapSessionInstance = GleapSession.getInstance();
88+
gleapSessionInstance?.injectSession(xhr); // Inject session headers
89+
90+
if (data) {
91+
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
92+
}
93+
94+
xhr.onreadystatechange = () => {
95+
if (xhr.readyState === 4) {
96+
if (xhr.status >= 200 && xhr.status < 300) {
97+
try {
98+
// Handle potential empty success responses (e.g., 204)
99+
const responseData = xhr.responseText
100+
? JSON.parse(xhr.responseText)
101+
: null;
102+
resolve(responseData);
103+
} catch (err) {
104+
reject({
105+
status: xhr.status,
106+
statusText: "JSON Parse Error",
107+
responseText: xhr.responseText,
108+
error: err,
109+
});
110+
}
111+
} else {
112+
reject({
113+
status: xhr.status,
114+
statusText: xhr.statusText,
115+
responseText: xhr.responseText,
116+
});
117+
}
118+
}
119+
};
120+
121+
xhr.onerror = () => {
122+
reject({ status: 0, statusText: "Network Error", responseText: null });
123+
};
124+
125+
xhr.send(data ? JSON.stringify(data) : null);
126+
});
127+
}
128+
129+
/**
130+
* Validates an outbound checklist ID, returning a Promise for the internal ID.
131+
* Manages caching and deduplicates requests.
132+
* @param {string} outboundId - The public/outbound checklist ID.
133+
* @returns {Promise<string>} A promise that resolves with the internal checklist ID.
134+
*/
135+
validateChecklist(outboundId) {
136+
// 1. Check cache for final result (success or error)
137+
const cachedResult = this.validationCache.get(outboundId);
138+
if (cachedResult) {
139+
if (cachedResult.status === RequestStatus.SUCCESS) {
140+
return Promise.resolve(cachedResult.internalId);
141+
} else {
142+
return Promise.reject(cachedResult.error);
143+
}
144+
}
145+
146+
// 2. Check for an ongoing request
147+
if (this.validationRequests.has(outboundId)) {
148+
return this.validationRequests.get(outboundId);
149+
}
150+
151+
// 3. Start a new request
152+
const apiUrl = this._getApiUrl();
153+
if (!apiUrl) {
154+
const error = new Error(
155+
"ChecklistNetworkManager: Gleap API URL not configured."
156+
);
157+
this.validationCache.set(outboundId, {
158+
status: RequestStatus.ERROR,
159+
error,
160+
});
161+
return Promise.reject(error);
162+
}
163+
164+
const url = `${apiUrl}/outbound/checklists?${this._getQueryParams()}`;
165+
const requestPromise = this._makeRequest("POST", url, { outboundId })
166+
.then((responseData) => {
167+
if (responseData && responseData.id) {
168+
this.validationCache.set(outboundId, {
169+
status: RequestStatus.SUCCESS,
170+
internalId: responseData.id,
171+
});
172+
return responseData.id;
173+
} else {
174+
const error = new Error("Validation response missing checklist ID.");
175+
this.validationCache.set(outboundId, {
176+
status: RequestStatus.ERROR,
177+
error: responseData || error,
178+
});
179+
throw error; // Rethrow to be caught by catch block
180+
}
181+
})
182+
.catch((error) => {
183+
// Store the error object itself in the cache
184+
this.validationCache.set(outboundId, {
185+
status: RequestStatus.ERROR,
186+
error,
187+
});
188+
throw error; // Re-throw so callers can catch it
189+
})
190+
.finally(() => {
191+
// Remove from pending requests map once done (success or fail)
192+
this.validationRequests.delete(outboundId);
193+
});
194+
195+
// Store the promise for potential concurrent requests
196+
this.validationRequests.set(outboundId, requestPromise);
197+
return requestPromise;
198+
}
199+
200+
/**
201+
* Fetches the full checklist data using the internal ID, returning a Promise.
202+
* Manages caching and deduplicates requests.
203+
* @param {string} internalId - The internal checklist ID.
204+
* @returns {Promise<object>} A promise that resolves with the full checklist data.
205+
*/
206+
fetchChecklist(internalId) {
207+
// 1. Check cache for final result (success or error)
208+
const cachedResult = this.fetchCache.get(internalId);
209+
if (cachedResult) {
210+
if (cachedResult.status === RequestStatus.SUCCESS) {
211+
// Return a deep copy to prevent mutation issues if multiple components use it
212+
return Promise.resolve(JSON.parse(JSON.stringify(cachedResult.data)));
213+
} else {
214+
return Promise.reject(cachedResult.error);
215+
}
216+
}
217+
218+
// 2. Check for an ongoing request
219+
if (this.fetchRequests.has(internalId)) {
220+
// Return a promise that resolves with a deep copy
221+
return this.fetchRequests
222+
.get(internalId)
223+
.then((data) => JSON.parse(JSON.stringify(data)));
224+
}
225+
226+
// 3. Start a new request
227+
const apiUrl = this._getApiUrl();
228+
if (!apiUrl) {
229+
const error = new Error(
230+
"ChecklistNetworkManager: Gleap API URL not configured."
231+
);
232+
this.fetchCache.set(internalId, { status: RequestStatus.ERROR, error });
233+
return Promise.reject(error);
234+
}
235+
236+
const url = `${apiUrl}/outbound/checklists/${internalId}?convertTipTap=true&${this._getQueryParams()}`;
237+
const requestPromise = this._makeRequest("GET", url, null)
238+
.then((responseData) => {
239+
if (responseData) {
240+
// Cache the successful data
241+
this.fetchCache.set(internalId, {
242+
status: RequestStatus.SUCCESS,
243+
data: responseData,
244+
});
245+
// Return a deep copy of the data
246+
return JSON.parse(JSON.stringify(responseData));
247+
} else {
248+
// Should not happen with successful GET usually, but handle defensively
249+
const error = new Error(
250+
"Empty response received for checklist fetch."
251+
);
252+
this.fetchCache.set(internalId, {
253+
status: RequestStatus.ERROR,
254+
error: responseData || error,
255+
});
256+
throw error;
257+
}
258+
})
259+
.catch((error) => {
260+
this.fetchCache.set(internalId, { status: RequestStatus.ERROR, error });
261+
throw error; // Re-throw so callers can catch it
262+
})
263+
.finally(() => {
264+
// Remove from pending requests map once done (success or fail)
265+
this.fetchRequests.delete(internalId);
266+
});
267+
268+
this.fetchRequests.set(internalId, requestPromise);
269+
// Return a promise that resolves with a deep copy
270+
return requestPromise.then((data) => JSON.parse(JSON.stringify(data)));
271+
}
272+
}
273+
274+
export default ChecklistNetworkManager;

src/Gleap.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import GleapTagManager from "./GleapTagManager";
2929
import GleapAdminManager from "./GleapAdminManager";
3030
import GleapProductTours from "./GleapProductTours";
3131
import { checkPageFilter } from "./GleapPageFilter";
32+
import { registerGleapChecklist } from "./GleapChecklist";
3233

3334
if (
3435
typeof window !== "undefined" &&
@@ -46,6 +47,14 @@ if (
4647
};
4748
}
4849

50+
if (
51+
typeof customElements !== "undefined" &&
52+
typeof HTMLElement !== "undefined" &&
53+
typeof window !== "undefined"
54+
) {
55+
registerGleapChecklist();
56+
}
57+
4958
class Gleap {
5059
static invoked = true;
5160
static silentCrashReportSent = false;
@@ -225,6 +234,10 @@ class Gleap {
225234
sessionInstance.startSession();
226235
}
227236

237+
static openURL(url, newTab = false) {
238+
GleapFrameManager.getInstance().urlHandler(url, newTab);
239+
}
240+
228241
static checkForUrlParams() {
229242
if (typeof window === "undefined" || !window.location.search) {
230243
return;
@@ -1322,4 +1335,4 @@ export {
13221335
handleGleapLink,
13231336
};
13241337

1325-
export default Gleap;
1338+
export default Gleap;

0 commit comments

Comments
 (0)