From 5d1a98f44a5b1f1043780315d4d8843457cafe9c Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Fri, 19 Sep 2025 15:40:12 +0100 Subject: [PATCH 01/11] Add GuideCue support to DrawerSection --- .../src/compass-assistant-drawer.tsx | 11 ++++ .../src/components/drawer-portal.tsx | 58 ++++++++++++++----- .../src/components/guide-cue/guide-cue.tsx | 8 ++- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/packages/compass-assistant/src/compass-assistant-drawer.tsx b/packages/compass-assistant/src/compass-assistant-drawer.tsx index c61af06e546..f9e00c12fb7 100644 --- a/packages/compass-assistant/src/compass-assistant-drawer.tsx +++ b/packages/compass-assistant/src/compass-assistant-drawer.tsx @@ -68,6 +68,8 @@ export const CompassAssistantDrawer: React.FunctionComponent<{ ); } + const appName = 'Compass'; // TODO + return ( {}, + tooltipAlign: 'left', + tooltipJustify: 'start', + }} > ['toolbarData'][number]; @@ -30,6 +31,7 @@ type DrawerSectionProps = Omit & { * provided will stay unordered at the bottom of the list */ order?: number; + guideCue?: GuideCueProps; }; type DrawerOpenStateContextValue = boolean; @@ -253,20 +255,50 @@ export const DrawerAnchor: React.FunctionComponent<{ return orderB < orderA ? 1 : orderB > orderA ? -1 : 0; }); }, [drawerSectionItems]); + + const [assistantNodes, setAssistantNodes] = useState< + Record + >({}); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(function () { + const nodes: Record = {}; + for (const [index, item] of toolbarData.entries()) { + const button = document.querySelector( + `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` + ); + nodes[item.id] = button ? (button as HTMLButtonElement) : undefined; + } + setAssistantNodes(nodes); + }); + return ( - - {children} - + <> + {toolbarData.map((item) => { + return ( + assistantNodes[item.id] && + item.guideCue && ( + + {...item.guideCue} + triggerNode={assistantNodes[item.id]} + /> + ) + ); + })} + + {children} + + ); }; diff --git a/packages/compass-components/src/components/guide-cue/guide-cue.tsx b/packages/compass-components/src/components/guide-cue/guide-cue.tsx index 46ed242596c..d036c6a9b50 100644 --- a/packages/compass-components/src/components/guide-cue/guide-cue.tsx +++ b/packages/compass-components/src/components/guide-cue/guide-cue.tsx @@ -89,13 +89,15 @@ export type GuideCueProps = Omit< GroupAndStep & { cueId: string; description: React.ReactChild; - trigger: ({ ref }: { ref: React.Ref }) => React.ReactElement; + triggerNode?: T; + trigger?: ({ ref }: { ref: React.Ref }) => React.ReactElement; onOpenChange?: (isOpen: boolean) => void; }; export const GuideCue = ({ description, trigger, + triggerNode, cueId, groupId, step, @@ -106,7 +108,7 @@ export const GuideCue = ({ }: GuideCueProps) => { const [isCueOpen, setIsCueOpen] = useState(false); const [isIntersecting, setIsIntersecting] = useState(true); - const refEl = useRef(null); + const refEl = useRef(triggerNode ?? null); const [readyToRender, setReadyToRender] = useState(false); const context = useContext(GuideCueContext); @@ -276,7 +278,7 @@ export const GuideCue = ({ {description} )} - {trigger({ ref: refEl })} + {trigger?.({ ref: refEl })} ); }; From 280f60f636319a7d527ec08bf75619a9ea72e004 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Fri, 19 Sep 2025 16:04:35 +0100 Subject: [PATCH 02/11] better useEffect --- .../src/components/drawer-portal.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index f8509c09098..27d4695aaaf 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -278,16 +278,29 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { Record >({}); - useEffect(function () { - const nodes: Record = {}; - for (const [index, item] of toolbarData.entries()) { - const button = document.querySelector( - `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` - ); - nodes[item.id] = button ? (button as HTMLButtonElement) : undefined; - } - setAssistantNodes(nodes); - }); + useEffect( + function () { + const nodes: Record = {}; + for (const [index, item] of toolbarData.entries()) { + const button = document.querySelector( + `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` + ); + if (button) { + nodes[item.id] = button as HTMLButtonElement; + } + } + + setAssistantNodes((oldNodes) => { + for (const id of Object.keys(nodes)) { + if (nodes[id] !== oldNodes[id]) { + return nodes; + } + } + return oldNodes; + }); + }, + [toolbarData, assistantNodes] + ); return ( <> From c75bdc6dc2b54a4bb46a5ceddde4d9578111ab09 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Fri, 19 Sep 2025 16:09:01 +0100 Subject: [PATCH 03/11] account for deleted stuff --- packages/compass-components/src/components/drawer-portal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index 27d4695aaaf..e8317771d25 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -291,7 +291,7 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { } setAssistantNodes((oldNodes) => { - for (const id of Object.keys(nodes)) { + for (const id of Object.keys({ ...nodes, oldNodes })) { if (nodes[id] !== oldNodes[id]) { return nodes; } @@ -299,7 +299,7 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { return oldNodes; }); }, - [toolbarData, assistantNodes] + [toolbarData] ); return ( From 68e1e3f2ad34c1b1bdc04c27314d5c710dcf883c Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Fri, 19 Sep 2025 16:17:59 +0100 Subject: [PATCH 04/11] generic selector --- packages/compass-components/src/components/drawer-portal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index e8317771d25..f0073552139 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -282,11 +282,11 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { function () { const nodes: Record = {}; for (const [index, item] of toolbarData.entries()) { - const button = document.querySelector( + const button = document.querySelector( `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` ); if (button) { - nodes[item.id] = button as HTMLButtonElement; + nodes[item.id] = button; } } From 2e94e42a952e5a0b3f63f43e352481dffa4d2406 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Fri, 19 Sep 2025 16:32:26 +0100 Subject: [PATCH 05/11] onPrimaryButtonClick is optional --- packages/compass-assistant/src/compass-assistant-drawer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compass-assistant/src/compass-assistant-drawer.tsx b/packages/compass-assistant/src/compass-assistant-drawer.tsx index f9e00c12fb7..7496626e74c 100644 --- a/packages/compass-assistant/src/compass-assistant-drawer.tsx +++ b/packages/compass-assistant/src/compass-assistant-drawer.tsx @@ -99,7 +99,6 @@ export const CompassAssistantDrawer: React.FunctionComponent<{ title: 'Introducing MongoDB Assistant', description: `AI-powered assistant to intelligently guide you through your database tasks. Get expert MongoDB help and streamline your workflow directly within ${appName}`, buttonText: 'Got it', - onPrimaryButtonClick: () => {}, tooltipAlign: 'left', tooltipJustify: 'start', }} From 706a379caa3bf5908dec1c2f0f84bd858d883c4a Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Fri, 19 Sep 2025 17:01:47 +0100 Subject: [PATCH 06/11] add appName for the assistant --- .../compass-assistant/src/compass-assistant-drawer.tsx | 7 +++---- .../src/compass-assistant-provider.spec.tsx | 1 + packages/compass-web/src/compass-assistant-drawer.tsx | 7 ++++++- packages/compass-web/src/entrypoint.tsx | 2 +- .../src/app/components/compass-assistant-drawer.tsx | 7 ++++++- packages/compass/src/app/components/workspace.tsx | 2 +- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/compass-assistant/src/compass-assistant-drawer.tsx b/packages/compass-assistant/src/compass-assistant-drawer.tsx index 7496626e74c..f21ced59d11 100644 --- a/packages/compass-assistant/src/compass-assistant-drawer.tsx +++ b/packages/compass-assistant/src/compass-assistant-drawer.tsx @@ -35,9 +35,10 @@ const assistantTitleTextStyles = css({ * it's within an AssistantProvider. */ export const CompassAssistantDrawer: React.FunctionComponent<{ + appName: string; autoOpen?: boolean; hasNonGenuineConnections?: boolean; -}> = ({ autoOpen, hasNonGenuineConnections = false }) => { +}> = ({ appName, autoOpen, hasNonGenuineConnections = false }) => { const chat = useContext(AssistantContext); const { clearChat } = useContext(AssistantActionsContext); @@ -68,8 +69,6 @@ export const CompassAssistantDrawer: React.FunctionComponent<{ ); } - const appName = 'Compass'; // TODO - return (
Provider children
diff --git a/packages/compass-web/src/compass-assistant-drawer.tsx b/packages/compass-web/src/compass-assistant-drawer.tsx index b09ac7683c0..bae915fbe71 100644 --- a/packages/compass-web/src/compass-assistant-drawer.tsx +++ b/packages/compass-web/src/compass-assistant-drawer.tsx @@ -6,7 +6,11 @@ import { CompassAssistantDrawer } from '@mongodb-js/compass-assistant'; // TODO(COMPASS-7830): This is a temporary solution to pass the // hasNonGenuineConnections prop to the CompassAssistantDrawer as otherwise // we end up with a circular dependency. -export function CompassAssistantDrawerWithConnections() { +export function CompassAssistantDrawerWithConnections({ + appName, +}: { + appName: string; +}) { // Check for non-genuine connections const activeConnectionIds = useConnectionIds( (conn) => @@ -15,6 +19,7 @@ export function CompassAssistantDrawerWithConnections() { ); return ( 0} /> ); diff --git a/packages/compass-web/src/entrypoint.tsx b/packages/compass-web/src/entrypoint.tsx index 1fc74322fdd..68cd0522b57 100644 --- a/packages/compass-web/src/entrypoint.tsx +++ b/packages/compass-web/src/entrypoint.tsx @@ -305,7 +305,7 @@ function CompassWorkspace({ - + ); }} diff --git a/packages/compass/src/app/components/compass-assistant-drawer.tsx b/packages/compass/src/app/components/compass-assistant-drawer.tsx index b09ac7683c0..bae915fbe71 100644 --- a/packages/compass/src/app/components/compass-assistant-drawer.tsx +++ b/packages/compass/src/app/components/compass-assistant-drawer.tsx @@ -6,7 +6,11 @@ import { CompassAssistantDrawer } from '@mongodb-js/compass-assistant'; // TODO(COMPASS-7830): This is a temporary solution to pass the // hasNonGenuineConnections prop to the CompassAssistantDrawer as otherwise // we end up with a circular dependency. -export function CompassAssistantDrawerWithConnections() { +export function CompassAssistantDrawerWithConnections({ + appName, +}: { + appName: string; +}) { // Check for non-genuine connections const activeConnectionIds = useConnectionIds( (conn) => @@ -15,6 +19,7 @@ export function CompassAssistantDrawerWithConnections() { ); return ( 0} /> ); diff --git a/packages/compass/src/app/components/workspace.tsx b/packages/compass/src/app/components/workspace.tsx index e53a87fe1ae..fd534d73401 100644 --- a/packages/compass/src/app/components/workspace.tsx +++ b/packages/compass/src/app/components/workspace.tsx @@ -112,7 +112,7 @@ export default function Workspace({ - + )} > From 9a8f95d2d461ea4a292c1631b769b8d4d1315524 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 22 Sep 2025 13:35:26 +0100 Subject: [PATCH 07/11] guide cue test --- .../src/components/drawer-portal.spec.tsx | 35 +++++++++++++++++++ .../src/components/drawer-portal.tsx | 13 ++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/compass-components/src/components/drawer-portal.spec.tsx b/packages/compass-components/src/components/drawer-portal.spec.tsx index 7c4c4587daf..a8caab59d70 100644 --- a/packages/compass-components/src/components/drawer-portal.spec.tsx +++ b/packages/compass-components/src/components/drawer-portal.spec.tsx @@ -228,4 +228,39 @@ describe('DrawerSection', function () { expect(screen.queryByText('This is the controlled section')).not.to.exist; }); }); + + it('renders guide cue when passed in props', async function () { + function TestDrawer() { + return ( + + + + This is a test section + + + + ); + } + + render(); + + await waitFor(() => { + expect(screen.getByText('Introducing this new test drawer')).to.be + .visible; + }); + }); }); diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index f0073552139..63b9395e1ef 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -278,15 +278,25 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { Record >({}); + const [failedLookupCount, setFailedLookupCount] = useState(0); + useEffect( function () { const nodes: Record = {}; for (const [index, item] of toolbarData.entries()) { + if (!item.guideCue) { + continue; + } + const button = document.querySelector( `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` ); if (button) { nodes[item.id] = button; + } else { + // we don't re-render enough times for unit tests to pass and this + // forces it to keep re-trying until the node is found + setFailedLookupCount((c) => c + 1); } } @@ -299,7 +309,7 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { return oldNodes; }); }, - [toolbarData] + [toolbarData, failedLookupCount] ); return ( @@ -309,6 +319,7 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { assistantNodes[item.id] && item.guideCue && ( + key={item.id} {...item.guideCue} triggerNode={assistantNodes[item.id]} /> From 4f0afe387edadeadebf2f7ec37e2fdfc299f2e0f Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 22 Sep 2025 13:39:20 +0100 Subject: [PATCH 08/11] make copilot happy --- .../src/components/drawer-portal.spec.tsx | 1 + .../compass-components/src/components/drawer-portal.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.spec.tsx b/packages/compass-components/src/components/drawer-portal.spec.tsx index a8caab59d70..615a27bd008 100644 --- a/packages/compass-components/src/components/drawer-portal.spec.tsx +++ b/packages/compass-components/src/components/drawer-portal.spec.tsx @@ -230,6 +230,7 @@ describe('DrawerSection', function () { }); it('renders guide cue when passed in props', async function () { + localStorage.compass_guide_cues = '[]'; function TestDrawer() { return ( diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index 63b9395e1ef..15d63b326f1 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -296,12 +296,15 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { } else { // we don't re-render enough times for unit tests to pass and this // forces it to keep re-trying until the node is found - setFailedLookupCount((c) => c + 1); + if (failedLookupCount < 10) { + setFailedLookupCount((c) => c + 1); + } } } setAssistantNodes((oldNodes) => { - for (const id of Object.keys({ ...nodes, oldNodes })) { + // account for removed nodes by checking all keys of both old and new + for (const id of Object.keys({ ...oldNodes, ...nodes })) { if (nodes[id] !== oldNodes[id]) { return nodes; } From d90e059d853ca570128272b9cb5f58082652a5b0 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 22 Sep 2025 14:33:24 +0100 Subject: [PATCH 09/11] use MutationObserver --- .../src/components/drawer-portal.spec.tsx | 2 +- .../src/components/drawer-portal.tsx | 70 ++++++++++++------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.spec.tsx b/packages/compass-components/src/components/drawer-portal.spec.tsx index 615a27bd008..2bce21401b5 100644 --- a/packages/compass-components/src/components/drawer-portal.spec.tsx +++ b/packages/compass-components/src/components/drawer-portal.spec.tsx @@ -16,7 +16,7 @@ import { expect } from 'chai'; describe('DrawerSection', function () { it('renders DrawerSection in the portal and updates the content when it updates', async function () { - let setCount; + let setCount: React.Dispatch> = () => {}; function TestDrawer() { const [count, _setCount] = useState(0); diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index 15d63b326f1..e5fee818fbe 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -278,41 +278,57 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { Record >({}); - const [failedLookupCount, setFailedLookupCount] = useState(0); - - useEffect( + useLayoutEffect( function () { - const nodes: Record = {}; - for (const [index, item] of toolbarData.entries()) { - if (!item.guideCue) { - continue; - } - - const button = document.querySelector( - `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` + const drawerEl = document.querySelector('.compass-drawer-anchor'); + if (!drawerEl) { + throw new Error( + 'Can not use DrawerSection without DrawerAnchor being mounted on the page' ); - if (button) { - nodes[item.id] = button; - } else { - // we don't re-render enough times for unit tests to pass and this - // forces it to keep re-trying until the node is found - if (failedLookupCount < 10) { - setFailedLookupCount((c) => c + 1); - } - } } - setAssistantNodes((oldNodes) => { - // account for removed nodes by checking all keys of both old and new - for (const id of Object.keys({ ...oldNodes, ...nodes })) { - if (nodes[id] !== oldNodes[id]) { - return nodes; + function check() { + const nodes: Record = {}; + for (const [index, item] of toolbarData.entries()) { + if (!item.guideCue) { + continue; + } + + const button = document.querySelector( + `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` + ); + if (button) { + nodes[item.id] = button; } } - return oldNodes; + + setAssistantNodes((oldNodes) => { + // account for removed nodes by checking all keys of both old and new + for (const id of Object.keys({ ...oldNodes, ...nodes })) { + if (nodes[id] !== oldNodes[id]) { + return nodes; + } + } + return oldNodes; + }); + } + check(); + + const mutationObserver = new MutationObserver(() => { + check(); + }); + + // use a mutation observer because at least in unit tests the button + // elements don't exist immediately + mutationObserver.observe(drawerEl, { + subtree: true, + childList: true, }); + return () => { + mutationObserver.disconnect(); + }; }, - [toolbarData, failedLookupCount] + [toolbarData] ); return ( From 2fc5e42f4aa0d3827f14967dff78ecd26e3c4233 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Tue, 23 Sep 2025 10:45:02 +0100 Subject: [PATCH 10/11] feedback: naming --- .../compass-components/src/components/drawer-portal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index e5fee818fbe..fe119517f9d 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -274,7 +274,7 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { }); }, [drawerSectionItems]); - const [assistantNodes, setAssistantNodes] = useState< + const [toolbarIconNodes, setToolbarIconNodes] = useState< Record >({}); @@ -302,7 +302,7 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { } } - setAssistantNodes((oldNodes) => { + setToolbarIconNodes((oldNodes) => { // account for removed nodes by checking all keys of both old and new for (const id of Object.keys({ ...oldNodes, ...nodes })) { if (nodes[id] !== oldNodes[id]) { @@ -335,12 +335,12 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { <> {toolbarData.map((item) => { return ( - assistantNodes[item.id] && + toolbarIconNodes[item.id] && item.guideCue && ( key={item.id} {...item.guideCue} - triggerNode={assistantNodes[item.id]} + triggerNode={toolbarIconNodes[item.id]} /> ) ); From 36ce3e3acadeaf9576f3084367226320be6cb63e Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Tue, 23 Sep 2025 10:47:41 +0100 Subject: [PATCH 11/11] better button selector --- .../compass-components/src/components/drawer-portal.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index fe119517f9d..57c7a048acd 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -288,14 +288,17 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => { } function check() { + if (!drawerEl) { + return; + } const nodes: Record = {}; - for (const [index, item] of toolbarData.entries()) { + for (const item of toolbarData) { if (!item.guideCue) { continue; } - const button = document.querySelector( - `[data-testid="lg-drawer-toolbar-icon_button-${index}"]` + const button = drawerEl.querySelector( + `button[aria-label="${item.label}"]` ); if (button) { nodes[item.id] = button;