Skip to content

Commit ddb24b4

Browse files
authored
feat: handle focused guide key param in guide toolbar run config (#888)
1 parent c134f53 commit ddb24b4

6 files changed

Lines changed: 211 additions & 21 deletions

File tree

.changeset/good-beers-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@knocklabs/react": patch
3+
---
4+
5+
[guides] add support for focused_guide_key param in toolbar run config

packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,16 @@ export const V2 = () => {
5353
const { client } = useGuideContext();
5454

5555
const [guidesListDisplayOption, setGuidesListDisplayOption] =
56-
React.useState<DisplayOption>("only-displayable");
56+
React.useState<DisplayOption>("all-guides");
5757

5858
const [runConfig, setRunConfig] = React.useState(() => getRunConfig());
59-
const [isCollapsed, setIsCollapsed] = React.useState(true);
59+
const [isCollapsed, setIsCollapsed] = React.useState(false);
6060

6161
React.useEffect(() => {
62+
const { isVisible = false, focusedGuideKeys = {} } = runConfig || {};
6263
const isDebugging = client.store.state.debug?.debugging;
63-
if (runConfig?.isVisible && !isDebugging) {
64-
client.setDebug();
64+
if (isVisible && !isDebugging) {
65+
client.setDebug({ focusedGuideKeys });
6566
}
6667

6768
return () => {
@@ -77,7 +78,7 @@ export const V2 = () => {
7778
initialPosition: { top: 16, right: 16 },
7879
});
7980

80-
const result = useInspectGuideClientStore();
81+
const result = useInspectGuideClientStore(runConfig);
8182
if (!result || !runConfig?.isVisible) {
8283
return null;
8384
}

packages/react/src/modules/guide/components/Toolbar/V2/helpers.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,70 @@
1+
import { KnockGuide } from "@knocklabs/client";
2+
13
import { checkForWindow } from "../../../../../modules/core";
24

35
// Use this param to start Toolbar and enter into a debugging session when
46
// it is present and set to true.
57
const TOOLBAR_QUERY_PARAM = "knock_guide_toolbar";
68

9+
// Optional, when present pin/focus on this guide.
10+
const GUIDE_KEY_PARAM = "focused_guide_key";
11+
712
// Use this key to read and write the run config data.
813
const LOCAL_STORAGE_KEY = "knock_guide_debug";
914

10-
type ToolbarV2RunConfig = {
15+
export type ToolbarV2RunConfig = {
1116
isVisible: boolean;
17+
focusedGuideKeys?: Record<KnockGuide["key"], true>;
1218
};
1319

14-
export const getRunConfig = (): ToolbarV2RunConfig | undefined => {
20+
export const getRunConfig = (): ToolbarV2RunConfig => {
21+
const fallback = { isVisible: false };
22+
1523
const win = checkForWindow();
1624
if (!win || !win.location) {
17-
return undefined;
25+
return fallback;
1826
}
1927

2028
const urlSearchParams = new URLSearchParams(win.location.search);
2129
const toolbarParamValue = urlSearchParams.get(TOOLBAR_QUERY_PARAM);
30+
const guideKeyParamValue = urlSearchParams.get(GUIDE_KEY_PARAM);
2231

2332
// If toolbar param detected in the URL, write to local storage before
2433
// returning.
2534
if (toolbarParamValue !== null) {
26-
const config = {
35+
const config: ToolbarV2RunConfig = {
2736
isVisible: toolbarParamValue === "true",
2837
};
38+
if (guideKeyParamValue) {
39+
config.focusedGuideKeys = { [guideKeyParamValue]: true };
40+
}
41+
2942
writeRunConfigLS(config);
3043
return config;
3144
}
3245

3346
// If not detected, check local storage for a persisted run config. If not
3447
// present then fall back to a default config.
35-
return (
36-
readRunConfigLS() || {
37-
isVisible: false,
38-
}
39-
);
48+
return readRunConfigLS() || fallback;
4049
};
4150

4251
const writeRunConfigLS = (config: ToolbarV2RunConfig) => {
4352
const win = checkForWindow();
53+
if (!win || !win.localStorage) return;
54+
4455
try {
45-
win?.localStorage?.setItem(LOCAL_STORAGE_KEY, JSON.stringify(config));
56+
win.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(config));
4657
} catch {
4758
// localStorage may be unavailable (e.g. private browsing)
4859
}
4960
};
5061

5162
const readRunConfigLS = (): ToolbarV2RunConfig | undefined => {
5263
const win = checkForWindow();
64+
if (!win || !win.localStorage) return undefined;
65+
5366
try {
54-
const stored = win?.localStorage?.getItem(LOCAL_STORAGE_KEY);
67+
const stored = win.localStorage.getItem(LOCAL_STORAGE_KEY);
5568
if (stored) {
5669
return JSON.parse(stored);
5770
}
@@ -63,8 +76,10 @@ const readRunConfigLS = (): ToolbarV2RunConfig | undefined => {
6376

6477
export const clearRunConfigLS = () => {
6578
const win = checkForWindow();
79+
if (!win || !win.localStorage) return;
80+
6681
try {
67-
win?.localStorage?.removeItem(LOCAL_STORAGE_KEY);
82+
win.localStorage.removeItem(LOCAL_STORAGE_KEY);
6883
} catch {
6984
// localStorage may be unavailable (e.g. private browsing)
7085
}

packages/react/src/modules/guide/components/Toolbar/V2/useInspectGuideClientStore.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
} from "@knocklabs/client";
1010
import { useGuideContext, useStore } from "@knocklabs/react-core";
1111

12+
import { ToolbarV2RunConfig } from "./helpers";
13+
1214
const byKey = <T extends { key: string }>(items: T[]) => {
1315
return items.reduce((acc, item) => ({ ...acc, [item.key]: item }), {});
1416
};
@@ -115,7 +117,7 @@ export type UnknownGuide = {
115117

116118
export type InspectionResult = {
117119
guides: (AnnotatedGuide | UnknownGuide)[];
118-
error?: "no_guide_group";
120+
error?: "no_guide_group" | "no_guide_present";
119121
};
120122

121123
type StoreStateSnapshot = Pick<
@@ -394,7 +396,9 @@ const newUnknownGuide = (key: KnockGuide["key"]) =>
394396
},
395397
}) as UnknownGuide;
396398

397-
export const useInspectGuideClientStore = (): InspectionResult | undefined => {
399+
export const useInspectGuideClientStore = (
400+
runConfig: ToolbarV2RunConfig,
401+
): InspectionResult | undefined => {
398402
const { client } = useGuideContext();
399403

400404
// Extract a snapshot of the client store state for debugging.
@@ -440,6 +444,20 @@ export const useInspectGuideClientStore = (): InspectionResult | undefined => {
440444
return annotateGuide(guide, snapshot, groupStage);
441445
});
442446

447+
// Check if the focused guide actually exists and is selectable on the page.
448+
if (groupStage?.status === "closed" && runConfig.focusedGuideKeys) {
449+
const focusableGuide = orderedGuides.find(
450+
(g) =>
451+
runConfig.focusedGuideKeys![g.key] && g.annotation.selectable.status,
452+
);
453+
if (!focusableGuide) {
454+
return {
455+
error: "no_guide_present",
456+
guides: [],
457+
};
458+
}
459+
}
460+
443461
return {
444462
guides: orderedGuides,
445463
};

packages/react/test/guide/Toolbar/V2/helpers.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,56 @@ describe("Toolbar V2 helpers", () => {
118118
expect(config).toEqual({ isVisible: false });
119119
});
120120

121+
test("includes focusedGuideKeys when focused_guide_key URL param is present", () => {
122+
Object.defineProperty(window, "location", {
123+
value: {
124+
search: "?knock_guide_toolbar=true&focused_guide_key=my_guide",
125+
},
126+
writable: true,
127+
configurable: true,
128+
});
129+
130+
const config = getRunConfig();
131+
132+
expect(config).toEqual({
133+
isVisible: true,
134+
focusedGuideKeys: { my_guide: true },
135+
});
136+
});
137+
138+
test("writes focusedGuideKeys to localStorage when focused_guide_key param is present", () => {
139+
Object.defineProperty(window, "location", {
140+
value: {
141+
search: "?knock_guide_toolbar=true&focused_guide_key=my_guide",
142+
},
143+
writable: true,
144+
configurable: true,
145+
});
146+
147+
getRunConfig();
148+
149+
expect(setItemSpy).toHaveBeenCalledWith(
150+
LOCAL_STORAGE_KEY,
151+
JSON.stringify({
152+
isVisible: true,
153+
focusedGuideKeys: { my_guide: true },
154+
}),
155+
);
156+
});
157+
158+
test("does not include focusedGuideKeys when focused_guide_key param is absent", () => {
159+
Object.defineProperty(window, "location", {
160+
value: { search: "?knock_guide_toolbar=true" },
161+
writable: true,
162+
configurable: true,
163+
});
164+
165+
const config = getRunConfig();
166+
167+
expect(config).toEqual({ isVisible: true });
168+
expect(config).not.toHaveProperty("focusedGuideKeys");
169+
});
170+
121171
test("URL param takes precedence over localStorage", () => {
122172
Object.defineProperty(window, "location", {
123173
value: { search: "?knock_guide_toolbar=false" },

packages/react/test/guide/Toolbar/V2/useInspectGuideClientStore.test.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,11 @@ const setSnapshot = (partial: Record<string, unknown>) => {
121121
};
122122

123123
// Shorthand for rendering the hook and extracting the result.
124-
const renderInspect = () => {
125-
const { result } = renderHook(() => useInspectGuideClientStore());
124+
const defaultRunConfig = { isVisible: true };
125+
const renderInspect = (
126+
runConfig: { isVisible: boolean; focusedGuideKeys?: Record<string, true> } = defaultRunConfig,
127+
) => {
128+
const { result } = renderHook(() => useInspectGuideClientStore(runConfig));
126129
return result.current;
127130
};
128131

@@ -1542,4 +1545,102 @@ describe("useInspectGuideClientStore", () => {
15421545
).toBe(true);
15431546
});
15441547
});
1548+
1549+
// ----- focused guide (no_guide_present) -----
1550+
1551+
describe("focused guide filtering", () => {
1552+
test("returns no_guide_present when focused guide is not selectable on closed stage", () => {
1553+
mockGroupStage = {
1554+
status: "closed",
1555+
ordered: ["g1"],
1556+
resolved: "g1",
1557+
timeoutId: null,
1558+
results: {
1559+
key: { g1: { one: makeSelectionResult() } },
1560+
},
1561+
};
1562+
const guide = makeGuide({ key: "g1" });
1563+
setSnapshot({
1564+
guideGroups: [makeGuideGroup(["g1"])],
1565+
guides: { g1: guide },
1566+
ineligibleGuides: {},
1567+
});
1568+
1569+
const result = renderInspect({
1570+
isVisible: true,
1571+
focusedGuideKeys: { other_guide: true },
1572+
});
1573+
expect(result).toEqual({ error: "no_guide_present", guides: [] });
1574+
});
1575+
1576+
test("returns guides normally when focused guide is selectable", () => {
1577+
mockGroupStage = {
1578+
status: "closed",
1579+
ordered: ["g1"],
1580+
resolved: "g1",
1581+
timeoutId: null,
1582+
results: {
1583+
key: { g1: { one: makeSelectionResult() } },
1584+
},
1585+
};
1586+
const guide = makeGuide({ key: "g1" });
1587+
setSnapshot({
1588+
guideGroups: [makeGuideGroup(["g1"])],
1589+
guides: { g1: guide },
1590+
ineligibleGuides: {},
1591+
});
1592+
1593+
const result = renderInspect({
1594+
isVisible: true,
1595+
focusedGuideKeys: { g1: true },
1596+
});
1597+
expect(result!.guides).toHaveLength(1);
1598+
expect(result!.guides[0]!.key).toBe("g1");
1599+
expect(result!.error).toBeUndefined();
1600+
});
1601+
1602+
test("skips focused guide check when focusedGuideKeys is not set", () => {
1603+
mockGroupStage = {
1604+
status: "closed",
1605+
ordered: ["g1"],
1606+
resolved: "g1",
1607+
timeoutId: null,
1608+
results: {
1609+
key: { g1: { one: makeSelectionResult() } },
1610+
},
1611+
};
1612+
const guide = makeGuide({ key: "g1" });
1613+
setSnapshot({
1614+
guideGroups: [makeGuideGroup(["g1"])],
1615+
guides: { g1: guide },
1616+
ineligibleGuides: {},
1617+
});
1618+
1619+
const result = renderInspect({ isVisible: true });
1620+
expect(result!.guides).toHaveLength(1);
1621+
expect(result!.error).toBeUndefined();
1622+
});
1623+
1624+
test("skips focused guide check when stage is not closed", () => {
1625+
mockGroupStage = {
1626+
status: "open",
1627+
ordered: [],
1628+
results: {},
1629+
timeoutId: null,
1630+
};
1631+
const guide = makeGuide({ key: "g1" });
1632+
setSnapshot({
1633+
guideGroups: [makeGuideGroup(["g1"])],
1634+
guides: { g1: guide },
1635+
ineligibleGuides: {},
1636+
});
1637+
1638+
const result = renderInspect({
1639+
isVisible: true,
1640+
focusedGuideKeys: { missing: true },
1641+
});
1642+
expect(result!.guides).toHaveLength(1);
1643+
expect(result!.error).toBeUndefined();
1644+
});
1645+
});
15451646
});

0 commit comments

Comments
 (0)