From 1c7e20a1324f1497c6d2dd98848857639e915fbd Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 30 Jan 2026 11:01:32 -0500 Subject: [PATCH 1/2] wip: take 2 of adding selectable status --- packages/client/src/clients/guide/client.ts | 115 ++++++++++++----- packages/client/src/clients/guide/helpers.ts | 22 +++- packages/client/src/clients/guide/index.ts | 2 + packages/client/src/clients/guide/types.ts | 16 +++ .../guide/components/Toolbar/V2/V2.tsx | 10 +- .../Toolbar/V2/useInspectGuideClientStore.ts | 118 ++++++++++++++++-- 6 files changed, 243 insertions(+), 40 deletions(-) diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts index e458b3d8c..143b37b18 100644 --- a/packages/client/src/clients/guide/client.ts +++ b/packages/client/src/clients/guide/client.ts @@ -46,6 +46,7 @@ import { SelectFilterParams, SelectGuideOpts, SelectGuidesOpts, + SelectQueryParams, StepMessageState, StoreState, TargetParams, @@ -175,7 +176,7 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => { result.set(index, guide); } - result.metadata = { guideGroup: defaultGroup }; + result.metadata = { guideGroup: defaultGroup, filters }; return result; }; @@ -617,7 +618,22 @@ export class KnockGuideClient { `[Guide] .selectGuides (filters: ${formatFilters(filters)}; state: ${formatState(state)})`, ); - const selectedGuide = this.selectGuide(state, filters, opts); + const selectedGuide = this.selectGuide(state, filters, { + ...opts, + // Do not record this selectGuide() query, since this is a short to check + // throttling, and we should be recording the actual selectGuides query + // (as needed). + recordSelectQuery: false, + }); + + // Record AFTER the selectGuide() query to ensure we have a group stage open + // to be able to record. + const { recordSelectQuery = !!state.debug?.debugging } = opts; + this.maybeRecordSelectQuery( + { ...filters, limit: "one" }, + { ...opts, recordSelectQuery }, + ); + if (!selectedGuide) { return []; } @@ -657,32 +673,6 @@ export class KnockGuideClient { return undefined; } - const result = select(state, filters); - - if (result.size === 0) { - this.knock.log("[Guide] Selection found zero result"); - return undefined; - } - - const [index, guide] = [...result][0]!; - this.knock.log( - `[Guide] Selection found: \`${guide.key}\` (total: ${result.size})`, - ); - - // If a guide ignores the group limit, then return immediately to render - // always. - if (guide.bypass_global_group_limit) { - this.knock.log(`[Guide] Returning the unthrottled guide: ${guide.key}`); - return guide; - } - - // Check if inside the throttle window (i.e. throttled) and if so stop and - // return undefined unless explicitly given the option to include throttled. - if (!opts.includeThrottled && checkStateIfThrottled(state)) { - this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`); - return undefined; - } - // Starting here to the end of this method represents the core logic of how // "group stage" works. It provides a mechanism for 1) figuring out which // guide components are about to render on a page, 2) determining which @@ -716,6 +706,40 @@ export class KnockGuideClient { this.stage = this.openGroupStage(); // Assign here to make tsc happy } + // Must come AFTER we ensure a group stage exists, so we can record select + // queries. + const { recordSelectQuery = !!state.debug?.debugging } = opts; + this.maybeRecordSelectQuery( + { ...filters, limit: "one" }, + { ...opts, recordSelectQuery }, + ); + + const result = select(state, filters); + + if (result.size === 0) { + this.knock.log("[Guide] Selection found zero result"); + return undefined; + } + + const [index, guide] = [...result][0]!; + this.knock.log( + `[Guide] Selection found: \`${guide.key}\` (total: ${result.size})`, + ); + + // If a guide ignores the group limit, then return immediately to render + // always. + if (guide.bypass_global_group_limit) { + this.knock.log(`[Guide] Returning the unthrottled guide: ${guide.key}`); + return guide; + } + + // Check if inside the throttle window (i.e. throttled) and if so stop and + // return undefined unless explicitly given the option to include throttled. + if (!opts.includeThrottled && checkStateIfThrottled(state)) { + this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`); + return undefined; + } + switch (this.stage.status) { case "open": { this.knock.log(`[Guide] Adding to the group stage: ${guide.key}`); @@ -744,6 +768,40 @@ export class KnockGuideClient { } } + private maybeRecordSelectQuery( + params: SelectQueryParams, + opts: SelectGuideOpts, + ) { + if (!opts.recordSelectQuery) return; + if (!this.stage || this.stage.status === "closed") return; + if (!params.key && !params.type) return; + + // Deep merge into the query logs: + const queriesByKey = this.stage.queries.key || {}; + if (params.key) { + queriesByKey[params.key] = { + ...(queriesByKey[params.key] || {}), + ...{ [params.limit]: opts }, + }; + } + const queriesByType = this.stage.queries.type || {}; + if (params.type) { + queriesByType[params.type] = { + ...(queriesByType[params.type] || {}), + ...{ [params.limit]: opts }, + }; + } + + this.stage = { + ...this.stage, + queries: { key: queriesByKey, type: queriesByType }, + }; + } + + getStage() { + return this.stage; + } + private openGroupStage() { this.knock.log("[Guide] Opening a new group stage"); @@ -759,6 +817,7 @@ export class KnockGuideClient { this.stage = { status: "open", ordered: [], + queries: {}, timeoutId, }; diff --git a/packages/client/src/clients/guide/helpers.ts b/packages/client/src/clients/guide/helpers.ts index 771bb7490..86ebfaf45 100644 --- a/packages/client/src/clients/guide/helpers.ts +++ b/packages/client/src/clients/guide/helpers.ts @@ -9,11 +9,16 @@ import { StoreState, } from "./types"; +type SelectionResultMetadata = { + guideGroup: GuideGroupData; + filters: SelectFilterParams; +}; + // Extends the map class to allow having metadata on it, which is used to record // the guide group context for the selection result (though currently only a // default global group is supported). export class SelectionResult extends Map { - metadata: { guideGroup: GuideGroupData } | undefined; + metadata: SelectionResultMetadata | undefined; constructor() { super(); @@ -233,3 +238,18 @@ export const predicateUrlPatterns = ( } }, predicateDefault); }; + +// export type SelectorIdParams = { +// filters: SelectFilterParams; +// limit: "one" | "all"; +// }; +// +// export const formatSelectorId = ({ filters, limit }: SelectorIdParams) => { +// +// // change to query params format +// // key="asdf"&type +// +// +// // Keep this ordering: limit, key, and type. +// return JSON.stringify({ limit, key: filters.key, type: filters.type }); +// }; diff --git a/packages/client/src/clients/guide/index.ts b/packages/client/src/clients/guide/index.ts index 7d5519160..99ac6db4c 100644 --- a/packages/client/src/clients/guide/index.ts +++ b/packages/client/src/clients/guide/index.ts @@ -3,6 +3,7 @@ export { DEBUG_QUERY_PARAMS, checkActivatable, } from "./client"; +export { checkStateIfThrottled } from "./helpers"; export type { KnockGuide, KnockGuideStep, @@ -12,4 +13,5 @@ export type { SelectGuideOpts as KnockSelectGuideOpts, SelectGuidesOpts as KnockSelectGuidesOpts, StoreState as KnockGuideClientStoreState, + GroupStage as KnockGuideClientGroupStage, } from "./types"; diff --git a/packages/client/src/clients/guide/types.ts b/packages/client/src/clients/guide/types.ts index 0327d8270..3f38242d3 100644 --- a/packages/client/src/clients/guide/types.ts +++ b/packages/client/src/clients/guide/types.ts @@ -231,6 +231,7 @@ export type SelectFilterParams = { export type SelectGuideOpts = { includeThrottled?: boolean; + recordSelectQuery?: boolean; }; export type SelectGuidesOpts = SelectGuideOpts; @@ -247,9 +248,24 @@ export type ConstructorOpts = { throttleCheckInterval?: number; }; +export type SelectQueryParams = SelectFilterParams & { + limit: "one" | "all"; +}; + +type SelectGuideOptsByLimit = { + one?: SelectGuideOpts; + all?: SelectGuideOpts; +}; + +type RecordedSelectQueries = { + key?: Record; + type?: Record; +}; + export type GroupStage = { status: "open" | "closed" | "patch"; ordered: Array; resolved?: KnockGuide["key"]; + queries: RecordedSelectQueries; timeoutId: ReturnType | null; }; diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx index eb134539d..e908735d3 100644 --- a/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx +++ b/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx @@ -15,8 +15,8 @@ import { } from "./GuidesListDisplaySelect"; import { detectToolbarParam } from "./helpers"; import { + checkActionable, checkEligible, - checkUsable, useInspectGuideClientStore, } from "./useInspectGuideClientStore"; @@ -46,6 +46,12 @@ export const V2 = () => { return null; } + console.log( + result.guides.map( + (g) => g.__typename !== "MissingGuide" && g.inspection.selectable, + ), + ); + return ( { {data.guides.map((guide, idx) => { if ( guidesListDisplayed === "current-page" && - !checkUsable(guide) + !checkActionable(guide) ) { return null; } diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/useInspectGuideClientStore.ts b/packages/react/src/modules/guide/components/Toolbar/V2/useInspectGuideClientStore.ts index bb381c270..8ba251f9e 100644 --- a/packages/react/src/modules/guide/components/Toolbar/V2/useInspectGuideClientStore.ts +++ b/packages/react/src/modules/guide/components/Toolbar/V2/useInspectGuideClientStore.ts @@ -1,8 +1,12 @@ import { KnockGuide, + // KnockGuideClient, + KnockGuideClientGroupStage, KnockGuideClientStoreState, KnockGuideIneligibilityMarker, + KnockSelectGuideOpts, checkActivatable, + checkStateIfThrottled, } from "@knocklabs/client"; import { useGuideContext, useStore } from "@knocklabs/react-core"; @@ -28,14 +32,32 @@ type ArchivedStatus = { status: boolean; }; +type SelectGuideOptsByLimit = { + one?: KnockSelectGuideOpts; + all?: KnockSelectGuideOpts; +}; +type RecordedSelectQuery = { + key: SelectGuideOptsByLimit | undefined; + type: SelectGuideOptsByLimit | undefined; +}; + +type SelectableStatusPresent = { + status: "returned" | "throttled" | "queried"; + query: RecordedSelectQuery; +}; +type SelectableStatusAbsent = { + status: undefined; +}; +type SelectableStatus = SelectableStatusPresent | SelectableStatusAbsent; + export type InspectedGuide = KnockGuide & { inspection: { - // true status = good + // `true` status = good active: ActiveStatus; + selectable: SelectableStatus; targetable: TargetableStatus; activatable: ActivatableStatus; - - // false status = good + // `false` status = good archived: ArchivedStatus; }; }; @@ -49,11 +71,13 @@ export const checkEligible = (guide: InspectedGuide | MissingGuide) => { return true; }; -export const checkUsable = (guide: InspectedGuide | MissingGuide) => { +export const checkActionable = (guide: InspectedGuide | MissingGuide) => { if (guide.__typename === "MissingGuide") return false; if (!checkEligible(guide)) return false; if (!guide.inspection.activatable.status) return false; + // XXX + return true; }; @@ -99,15 +123,78 @@ const toArchivedStatus = ( }; }; +const getSelectableStatus = ( + guide: KnockGuide, + snapshot: StoreStateSnapshot, + stage: KnockGuideClientGroupStage, + query: RecordedSelectQuery, +) => { + if (query.type?.all) { + // TODO: This is a placeholder, as we need to accumulate select query + // results + return "returned"; + } + + if (stage.resolved !== guide.key) { + return "queried"; + } + + // At this point we know this is the resolved guide. + + if (guide.bypass_global_group_limit) { + return "returned"; + } + if (query.type?.one && query.type.one.includeThrottled) { + return "returned"; + } + if (query.key?.one && query.key.one.includeThrottled) { + return "returned"; + } + + return snapshot.throttled ? "throttled" : "returned"; +}; + +// TODO: Rename inspectGuideIfSelectable +const toSelectableStatus = ( + guide: KnockGuide, + snapshot: StoreStateSnapshot, + stage: KnockGuideClientGroupStage | undefined, +): SelectableStatus => { + if (!stage || stage.status === "open") { + return { status: undefined }; + } + + const { queries } = stage; + + const query = { + key: (queries.key || {})[guide.key], + type: (queries.type || {})[guide.type], + }; + + const queried = Boolean(query.key || query.type); + if (!queried) { + return { status: undefined }; + } + + const status = getSelectableStatus(guide, snapshot, stage, query); + + return { + status, + query, + }; +}; + const inspectGuide = ( guide: KnockGuide, - { ineligibleGuides, location }: StoreStateSnapshot, - // ineligibleGuides: KnockGuideClientStoreState["ineligibleGuides"], + snapshot: StoreStateSnapshot, + stage: KnockGuideClientGroupStage | undefined, ): InspectedGuide => { + const { ineligibleGuides, location } = snapshot; const marker = ineligibleGuides[guide.key]; const inspection: InspectedGuide["inspection"] = { active: { status: guide.active }, + selectable: toSelectableStatus(guide, snapshot, stage), targetable: marker ? toTargetableStatus(marker) : { status: true }, activatable: { status: checkActivatable(guide, location) }, archived: marker ? toArchivedStatus(marker) : { status: false }, @@ -129,20 +216,31 @@ const newMissingGuide = (key: KnockGuide["key"]) => type StoreStateSnapshot = Pick< KnockGuideClientStoreState, - "location" | "guides" | "guideGroups" | "ineligibleGuides" | "debug" ->; + | "location" + | "guides" + | "guideGroups" + | "ineligibleGuides" + | "debug" + | "counter" +> & { + throttled: boolean; +}; export const useInspectGuideClientStore = (): InspectionResult | undefined => { const { client } = useGuideContext(); // Extract a snapshot of the client store state for debugging. const snapshot: StoreStateSnapshot = useStore(client.store, (state) => { + const throttled = checkStateIfThrottled(state); + return { location: state.location, guides: state.guides, guideGroups: state.guideGroups, ineligibleGuides: state.ineligibleGuides, debug: state.debug, + counter: state.counter, + throttled, }; }); @@ -161,6 +259,8 @@ export const useInspectGuideClientStore = (): InspectionResult | undefined => { }; } + const groupStage = client.getStage(); + // Transform the raw snapshot into something more useful for debugging. const orderedGuides = defaultGroup.display_sequence.map((guideKey) => { const guide = snapshot.guides[guideKey]; @@ -168,7 +268,7 @@ export const useInspectGuideClientStore = (): InspectionResult | undefined => { return newMissingGuide(guideKey); } - return inspectGuide(guide, snapshot); + return inspectGuide(guide, snapshot, groupStage); }); return { From 7467313eb8255f853347636823f28126c8607644 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 2 Feb 2026 14:54:36 -0500 Subject: [PATCH 2/2] wip --- packages/client/src/clients/guide/client.ts | 150 ++++++++++++------ packages/client/src/clients/guide/helpers.ts | 20 +-- packages/client/src/clients/guide/index.ts | 1 + packages/client/src/clients/guide/types.ts | 43 +++-- .../guide/components/Toolbar/V2/GuideRow.tsx | 33 +++- .../Toolbar/V2/useInspectGuideClientStore.ts | 58 ++++--- 6 files changed, 208 insertions(+), 97 deletions(-) diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts index 143b37b18..0da6b63aa 100644 --- a/packages/client/src/clients/guide/client.ts +++ b/packages/client/src/clients/guide/client.ts @@ -7,7 +7,7 @@ import Knock from "../../knock"; import { DEFAULT_GROUP_KEY, - SelectionResult, + // SelectionResult, byKey, checkStateIfThrottled, findDefaultGroup, @@ -46,7 +46,9 @@ import { SelectFilterParams, SelectGuideOpts, SelectGuidesOpts, - SelectQueryParams, + SelectQueryLimit, + SelectionResult, + // SelectQueryParams, StepMessageState, StoreState, TargetParams, @@ -151,7 +153,16 @@ const safeJsonParseDebugParams = (value: string): DebugState => { } }; -const select = (state: StoreState, filters: SelectFilterParams = {}) => { +type SelectQueryMetadata = { + limit: SelectQueryLimit; + opts: SelectGuideOpts; +}; + +const select = ( + state: StoreState, + filters: SelectFilterParams, + metadata: SelectQueryMetadata, +) => { // A map of selected guides as values, with its order index as keys. const result = new SelectionResult(); @@ -176,7 +187,8 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => { result.set(index, guide); } - result.metadata = { guideGroup: defaultGroup, filters }; + result.metadata = { guideGroup: defaultGroup, filters, ...metadata }; + return result; }; @@ -618,29 +630,35 @@ export class KnockGuideClient { `[Guide] .selectGuides (filters: ${formatFilters(filters)}; state: ${formatState(state)})`, ); + // 1. First, call selectGuide() using the same filters to ensure we have a + // group stage open and respect throttling. This isn't the real query, but + // rather it's a shortcut ahead of handling the actual query result below. const selectedGuide = this.selectGuide(state, filters, { ...opts, - // Do not record this selectGuide() query, since this is a short to check - // throttling, and we should be recording the actual selectGuides query - // (as needed). + // Don't record this result, not the actual query result we need. recordSelectQuery: false, }); - // Record AFTER the selectGuide() query to ensure we have a group stage open - // to be able to record. + // 2. Now make the actual select query with the provided filters and opts, + // and record the result (as needed). By default, we only record the result + // while in debugging. const { recordSelectQuery = !!state.debug?.debugging } = opts; - this.maybeRecordSelectQuery( - { ...filters, limit: "one" }, - { ...opts, recordSelectQuery }, - ); + const metadata: SelectQueryMetadata = { + limit: "all", + opts: { ...opts, recordSelectQuery }, + }; + const result = select(state, filters, metadata); + this.maybeRecordSelectResult(result); + // 3. Stop if there is not at least one guide to return. if (!selectedGuide) { return []; } // There should be at least one guide to return here now. - const guides = [...select(state, filters).values()]; + const guides = [...result.values()]; + // 4. If throttled, filter out any throttled guides. if (!opts.includeThrottled && checkStateIfThrottled(state)) { const unthrottledGuides = guides.filter( (g) => g.bypass_global_group_limit, @@ -706,15 +724,15 @@ export class KnockGuideClient { this.stage = this.openGroupStage(); // Assign here to make tsc happy } - // Must come AFTER we ensure a group stage exists, so we can record select - // queries. + // Must come AFTER we ensure a group stage exists above, so we can record + // select queries. By default, we only record the result while in debugging. const { recordSelectQuery = !!state.debug?.debugging } = opts; - this.maybeRecordSelectQuery( - { ...filters, limit: "one" }, - { ...opts, recordSelectQuery }, - ); - - const result = select(state, filters); + const metadata: SelectQueryMetadata = { + limit: "one", + opts: { ...opts, recordSelectQuery }, + }; + const result = select(state, filters, metadata); + this.maybeRecordSelectResult(result); if (result.size === 0) { this.knock.log("[Guide] Selection found zero result"); @@ -735,10 +753,12 @@ export class KnockGuideClient { // Check if inside the throttle window (i.e. throttled) and if so stop and // return undefined unless explicitly given the option to include throttled. - if (!opts.includeThrottled && checkStateIfThrottled(state)) { - this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`); - return undefined; - } + const throttled = !opts.includeThrottled && checkStateIfThrottled(state); + + // if (!opts.includeThrottled && checkStateIfThrottled(state)) { + // this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`); + // return undefined; + // } switch (this.stage.status) { case "open": { @@ -751,6 +771,11 @@ export class KnockGuideClient { this.knock.log(`[Guide] Patching the group stage: ${guide.key}`); this.stage.ordered[index] = guide.key; + if (throttled) { + this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`); + return undefined; + } + const ret = this.stage.resolved === guide.key ? guide : undefined; this.knock.log( `[Guide] Returning \`${ret?.key}\` (stage: ${formatGroupStage(this.stage)})`, @@ -759,6 +784,11 @@ export class KnockGuideClient { } case "closed": { + if (throttled) { + this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`); + return undefined; + } + const ret = this.stage.resolved === guide.key ? guide : undefined; this.knock.log( `[Guide] Returning \`${ret?.key}\` (stage: ${formatGroupStage(this.stage)})`, @@ -768,36 +798,66 @@ export class KnockGuideClient { } } - private maybeRecordSelectQuery( - params: SelectQueryParams, - opts: SelectGuideOpts, - ) { + private maybeRecordSelectResult(result: SelectionResult) { + if (!result.metadata) return; + + const { opts, filters, limit } = result.metadata; if (!opts.recordSelectQuery) return; + if (!filters.key && !filters.type) return; if (!this.stage || this.stage.status === "closed") return; - if (!params.key && !params.type) return; - - // Deep merge into the query logs: - const queriesByKey = this.stage.queries.key || {}; - if (params.key) { - queriesByKey[params.key] = { - ...(queriesByKey[params.key] || {}), - ...{ [params.limit]: opts }, + + // Deep merge to accumulate the results. + const queriedByKey = this.stage.results.key || {}; + if (filters.key) { + queriedByKey[filters.key] = { + ...(queriedByKey[filters.key] || {}), + ...{ [limit]: result }, }; } - const queriesByType = this.stage.queries.type || {}; - if (params.type) { - queriesByType[params.type] = { - ...(queriesByType[params.type] || {}), - ...{ [params.limit]: opts }, + const queriedByType = this.stage.results.type || {}; + if (filters.type) { + queriedByType[filters.type] = { + ...(queriedByType[filters.type] || {}), + ...{ [limit]: result }, }; } this.stage = { ...this.stage, - queries: { key: queriesByKey, type: queriesByType }, + results: { key: queriedByKey, type: queriedByType }, }; } + // private maybeRecordSelectQuery( + // params: SelectQueryParams, + // opts: SelectGuideOpts, + // ) { + // if (!opts.recordSelectQuery) return; + // if (!this.stage || this.stage.status === "closed") return; + // if (!params.key && !params.type) return; + // + // // Deep merge into the query logs: + // const queriesByKey = this.stage.queries.key || {}; + // if (params.key) { + // queriesByKey[params.key] = { + // ...(queriesByKey[params.key] || {}), + // ...{ [params.limit]: opts }, + // }; + // } + // const queriesByType = this.stage.queries.type || {}; + // if (params.type) { + // queriesByType[params.type] = { + // ...(queriesByType[params.type] || {}), + // ...{ [params.limit]: opts }, + // }; + // } + // + // this.stage = { + // ...this.stage, + // queries: { key: queriesByKey, type: queriesByType }, + // }; + // } + getStage() { return this.stage; } @@ -817,7 +877,7 @@ export class KnockGuideClient { this.stage = { status: "open", ordered: [], - queries: {}, + results: {}, timeoutId, }; diff --git a/packages/client/src/clients/guide/helpers.ts b/packages/client/src/clients/guide/helpers.ts index 86ebfaf45..90cf7ddc6 100644 --- a/packages/client/src/clients/guide/helpers.ts +++ b/packages/client/src/clients/guide/helpers.ts @@ -3,28 +3,14 @@ import { GuideActivationUrlRuleData, GuideData, GuideGroupData, - KnockGuide, + // KnockGuide, KnockGuideActivationUrlPattern, SelectFilterParams, + // SelectGuideOpts, + // SelectQueryLimit, StoreState, } from "./types"; -type SelectionResultMetadata = { - guideGroup: GuideGroupData; - filters: SelectFilterParams; -}; - -// Extends the map class to allow having metadata on it, which is used to record -// the guide group context for the selection result (though currently only a -// default global group is supported). -export class SelectionResult extends Map { - metadata: SelectionResultMetadata | undefined; - - constructor() { - super(); - } -} - export const formatGroupStage = (stage: GroupStage) => { return `status=${stage.status}, resolved=${stage.resolved}`; }; diff --git a/packages/client/src/clients/guide/index.ts b/packages/client/src/clients/guide/index.ts index 99ac6db4c..5741773c0 100644 --- a/packages/client/src/clients/guide/index.ts +++ b/packages/client/src/clients/guide/index.ts @@ -14,4 +14,5 @@ export type { SelectGuidesOpts as KnockSelectGuidesOpts, StoreState as KnockGuideClientStoreState, GroupStage as KnockGuideClientGroupStage, + SelectionResult as KnockGuideSelectionResult, } from "./types"; diff --git a/packages/client/src/clients/guide/types.ts b/packages/client/src/clients/guide/types.ts index 3f38242d3..e6bb8210c 100644 --- a/packages/client/src/clients/guide/types.ts +++ b/packages/client/src/clients/guide/types.ts @@ -1,5 +1,24 @@ import { GenericData } from "@knocklabs/types"; +type SelectionResultMetadata = { + guideGroup: GuideGroupData; + // Additional info about the underlying select query behind the result. + filters: SelectFilterParams; + limit: SelectQueryLimit; + opts: SelectGuideOpts; +}; + +// Extends the map class to allow having metadata on it, which is used to record +// the guide group context for the selection result (though currently only a +// default global group is supported). +export class SelectionResult extends Map { + metadata: SelectionResultMetadata | undefined; + + constructor() { + super(); + } +} + // // Fetch guides API // @@ -231,6 +250,7 @@ export type SelectFilterParams = { export type SelectGuideOpts = { includeThrottled?: boolean; + // XXX: record result recordSelectQuery?: boolean; }; @@ -248,24 +268,27 @@ export type ConstructorOpts = { throttleCheckInterval?: number; }; -export type SelectQueryParams = SelectFilterParams & { - limit: "one" | "all"; -}; +// i.e. useGuide vs useGuides +export type SelectQueryLimit = "one" | "all"; + +// export type SelectQueryParams = SelectFilterParams & { +// limit: "one" | "all"; +// }; -type SelectGuideOptsByLimit = { - one?: SelectGuideOpts; - all?: SelectGuideOpts; +type SelectionResultByLimit = { + one?: SelectionResult; + all?: SelectionResult; }; -type RecordedSelectQueries = { - key?: Record; - type?: Record; +type RecordedSelectionResults = { + key?: Record; + type?: Record; }; export type GroupStage = { status: "open" | "closed" | "patch"; ordered: Array; resolved?: KnockGuide["key"]; - queries: RecordedSelectQueries; timeoutId: ReturnType | null; + results: RecordedSelectionResults; }; diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx index bd54daf83..6242a594a 100644 --- a/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx +++ b/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx @@ -6,6 +6,7 @@ import { Text } from "@telegraph/typography"; import { CheckCircle2, CircleDashed, + Code2, Eye, LocateFixed, UserCircle2, @@ -48,8 +49,33 @@ export const GuideRow = ({ guide, orderIndex }: Props) => { {guide.__typename === "Guide" && ( +