Skip to content

Commit 55e0a9f

Browse files
authored
feat(compass-assistant): add GuideCue support to DrawerSection COMPASS-9795 (#7347)
* Add GuideCue support to DrawerSection * better useEffect * account for deleted stuff * generic selector * onPrimaryButtonClick is optional * add appName for the assistant * guide cue test * make copilot happy * use MutationObserver * feedback: naming * better button selector
1 parent d07e301 commit 55e0a9f

File tree

9 files changed

+158
-23
lines changed

9 files changed

+158
-23
lines changed

packages/compass-assistant/src/compass-assistant-drawer.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ const assistantTitleTextStyles = css({
3535
* it's within an AssistantProvider.
3636
*/
3737
export const CompassAssistantDrawer: React.FunctionComponent<{
38+
appName: string;
3839
autoOpen?: boolean;
3940
hasNonGenuineConnections?: boolean;
40-
}> = ({ autoOpen, hasNonGenuineConnections = false }) => {
41+
}> = ({ appName, autoOpen, hasNonGenuineConnections = false }) => {
4142
const chat = useContext(AssistantContext);
4243
const { clearChat } = useContext(AssistantActionsContext);
4344

@@ -92,6 +93,14 @@ export const CompassAssistantDrawer: React.FunctionComponent<{
9293
label="MongoDB Assistant"
9394
glyph="Sparkle"
9495
autoOpen={autoOpen}
96+
guideCue={{
97+
cueId: 'assistant-drawer',
98+
title: 'Introducing MongoDB Assistant',
99+
description: `AI-powered assistant to intelligently guide you through your database tasks. Get expert MongoDB help and streamline your workflow directly within ${appName}.`,
100+
buttonText: 'Got it',
101+
tooltipAlign: 'left',
102+
tooltipJustify: 'start',
103+
}}
95104
>
96105
<AssistantChat
97106
chat={chat}

packages/compass-assistant/src/compass-assistant-provider.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const TestComponent: React.FunctionComponent<{
8989
<DrawerAnchor>
9090
<div data-testid="provider-children">Provider children</div>
9191
<CompassAssistantDrawer
92+
appName="Compass"
9293
autoOpen={autoOpen}
9394
hasNonGenuineConnections={hasNonGenuineConnections}
9495
/>

packages/compass-components/src/components/drawer-portal.spec.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { expect } from 'chai';
1616

1717
describe('DrawerSection', function () {
1818
it('renders DrawerSection in the portal and updates the content when it updates', async function () {
19-
let setCount;
19+
let setCount: React.Dispatch<React.SetStateAction<number>> = () => {};
2020

2121
function TestDrawer() {
2222
const [count, _setCount] = useState(0);
@@ -228,4 +228,40 @@ describe('DrawerSection', function () {
228228
expect(screen.queryByText('This is the controlled section')).not.to.exist;
229229
});
230230
});
231+
232+
it('renders guide cue when passed in props', async function () {
233+
localStorage.compass_guide_cues = '[]';
234+
function TestDrawer() {
235+
return (
236+
<DrawerContentProvider>
237+
<DrawerAnchor>
238+
<DrawerSection
239+
id="test-section"
240+
label="Test section"
241+
title={`Test section`}
242+
glyph="Trash"
243+
guideCue={{
244+
cueId: 'test-drawer',
245+
title: 'Introducing this new test drawer',
246+
description: 'Does all the things',
247+
buttonText: 'ok',
248+
tooltipAlign: 'bottom',
249+
tooltipJustify: 'end',
250+
}}
251+
autoOpen
252+
>
253+
This is a test section
254+
</DrawerSection>
255+
</DrawerAnchor>
256+
</DrawerContentProvider>
257+
);
258+
}
259+
260+
render(<TestDrawer></TestDrawer>);
261+
262+
await waitFor(() => {
263+
expect(screen.getByText('Introducing this new test drawer')).to.be
264+
.visible;
265+
});
266+
});
231267
});

packages/compass-components/src/components/drawer-portal.tsx

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { css, cx } from '@leafygreen-ui/emotion';
1717
import { isEqual } from 'lodash';
1818
import { rafraf } from '../utils/rafraf';
19+
import { GuideCue, type GuideCueProps } from './guide-cue/guide-cue';
1920
import { BaseFontSize, fontWeights } from '@leafygreen-ui/tokens';
2021

2122
type ToolbarData = Required<DrawerLayoutProps>['toolbarData'];
@@ -37,6 +38,7 @@ type DrawerSectionProps = Omit<SectionData, 'content' | 'onClick'> & {
3738
* provided will stay unordered at the bottom of the list
3839
*/
3940
order?: number;
41+
guideCue?: GuideCueProps<HTMLButtonElement>;
4042
};
4143

4244
type DrawerOpenStateContextValue = boolean;
@@ -271,21 +273,96 @@ export const DrawerAnchor: React.FunctionComponent = ({ children }) => {
271273
return orderB < orderA ? 1 : orderB > orderA ? -1 : 0;
272274
});
273275
}, [drawerSectionItems]);
276+
277+
const [toolbarIconNodes, setToolbarIconNodes] = useState<
278+
Record<string, HTMLButtonElement | undefined>
279+
>({});
280+
281+
useLayoutEffect(
282+
function () {
283+
const drawerEl = document.querySelector('.compass-drawer-anchor');
284+
if (!drawerEl) {
285+
throw new Error(
286+
'Can not use DrawerSection without DrawerAnchor being mounted on the page'
287+
);
288+
}
289+
290+
function check() {
291+
if (!drawerEl) {
292+
return;
293+
}
294+
const nodes: Record<string, HTMLButtonElement | undefined> = {};
295+
for (const item of toolbarData) {
296+
if (!item.guideCue) {
297+
continue;
298+
}
299+
300+
const button = drawerEl.querySelector<HTMLButtonElement>(
301+
`button[aria-label="${item.label}"]`
302+
);
303+
if (button) {
304+
nodes[item.id] = button;
305+
}
306+
}
307+
308+
setToolbarIconNodes((oldNodes) => {
309+
// account for removed nodes by checking all keys of both old and new
310+
for (const id of Object.keys({ ...oldNodes, ...nodes })) {
311+
if (nodes[id] !== oldNodes[id]) {
312+
return nodes;
313+
}
314+
}
315+
return oldNodes;
316+
});
317+
}
318+
check();
319+
320+
const mutationObserver = new MutationObserver(() => {
321+
check();
322+
});
323+
324+
// use a mutation observer because at least in unit tests the button
325+
// elements don't exist immediately
326+
mutationObserver.observe(drawerEl, {
327+
subtree: true,
328+
childList: true,
329+
});
330+
return () => {
331+
mutationObserver.disconnect();
332+
};
333+
},
334+
[toolbarData]
335+
);
336+
274337
return (
275-
<DrawerLayout
276-
displayMode={DrawerDisplayMode.Embedded}
277-
resizable
278-
toolbarData={toolbarData}
279-
className={cx(
280-
drawerLayoutFixesStyles,
281-
toolbarData.length === 0 && emptyDrawerLayoutFixesStyles,
282-
// classname is the only property leafygreen passes over to the drawer
283-
// wrapper component that would allow us to target it
284-
'compass-drawer-anchor'
285-
)}
286-
>
287-
<DrawerContextGrabber>{children}</DrawerContextGrabber>
288-
</DrawerLayout>
338+
<>
339+
{toolbarData.map((item) => {
340+
return (
341+
toolbarIconNodes[item.id] &&
342+
item.guideCue && (
343+
<GuideCue<HTMLButtonElement>
344+
key={item.id}
345+
{...item.guideCue}
346+
triggerNode={toolbarIconNodes[item.id]}
347+
/>
348+
)
349+
);
350+
})}
351+
<DrawerLayout
352+
displayMode={DrawerDisplayMode.Embedded}
353+
resizable
354+
toolbarData={toolbarData}
355+
className={cx(
356+
drawerLayoutFixesStyles,
357+
toolbarData.length === 0 && emptyDrawerLayoutFixesStyles,
358+
// classname is the only property leafygreen passes over to the drawer
359+
// wrapper component that would allow us to target it
360+
'compass-drawer-anchor'
361+
)}
362+
>
363+
<DrawerContextGrabber>{children}</DrawerContextGrabber>
364+
</DrawerLayout>
365+
</>
289366
);
290367
};
291368

packages/compass-components/src/components/guide-cue/guide-cue.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,15 @@ export type GuideCueProps<T> = Omit<
8989
GroupAndStep & {
9090
cueId: string;
9191
description: React.ReactChild;
92-
trigger: ({ ref }: { ref: React.Ref<T> }) => React.ReactElement;
92+
triggerNode?: T;
93+
trigger?: ({ ref }: { ref: React.Ref<T> }) => React.ReactElement;
9394
onOpenChange?: (isOpen: boolean) => void;
9495
};
9596

9697
export const GuideCue = <T extends HTMLElement>({
9798
description,
9899
trigger,
100+
triggerNode,
99101
cueId,
100102
groupId,
101103
step,
@@ -106,7 +108,7 @@ export const GuideCue = <T extends HTMLElement>({
106108
}: GuideCueProps<T>) => {
107109
const [isCueOpen, setIsCueOpen] = useState(false);
108110
const [isIntersecting, setIsIntersecting] = useState(true);
109-
const refEl = useRef<T>(null);
111+
const refEl = useRef<T>(triggerNode ?? null);
110112
const [readyToRender, setReadyToRender] = useState(false);
111113
const context = useContext(GuideCueContext);
112114

@@ -276,7 +278,7 @@ export const GuideCue = <T extends HTMLElement>({
276278
{description}
277279
</LGGuideCue>
278280
)}
279-
{trigger({ ref: refEl })}
281+
{trigger?.({ ref: refEl })}
280282
</>
281283
);
282284
};

packages/compass-web/src/compass-assistant-drawer.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { CompassAssistantDrawer } from '@mongodb-js/compass-assistant';
66
// TODO(COMPASS-7830): This is a temporary solution to pass the
77
// hasNonGenuineConnections prop to the CompassAssistantDrawer as otherwise
88
// we end up with a circular dependency.
9-
export function CompassAssistantDrawerWithConnections() {
9+
export function CompassAssistantDrawerWithConnections({
10+
appName,
11+
}: {
12+
appName: string;
13+
}) {
1014
// Check for non-genuine connections
1115
const activeConnectionIds = useConnectionIds(
1216
(conn) =>
@@ -15,6 +19,7 @@ export function CompassAssistantDrawerWithConnections() {
1519
);
1620
return (
1721
<CompassAssistantDrawer
22+
appName={appName}
1823
hasNonGenuineConnections={activeConnectionIds.length > 0}
1924
/>
2025
);

packages/compass-web/src/entrypoint.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ function CompassWorkspace({
305305
<CreateNamespacePlugin></CreateNamespacePlugin>
306306
<DropNamespacePlugin></DropNamespacePlugin>
307307
<RenameCollectionPlugin></RenameCollectionPlugin>
308-
<CompassAssistantDrawerWithConnections />
308+
<CompassAssistantDrawerWithConnections appName="Data Explorer" />
309309
</>
310310
);
311311
}}

packages/compass/src/app/components/compass-assistant-drawer.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { CompassAssistantDrawer } from '@mongodb-js/compass-assistant';
66
// TODO(COMPASS-7830): This is a temporary solution to pass the
77
// hasNonGenuineConnections prop to the CompassAssistantDrawer as otherwise
88
// we end up with a circular dependency.
9-
export function CompassAssistantDrawerWithConnections() {
9+
export function CompassAssistantDrawerWithConnections({
10+
appName,
11+
}: {
12+
appName: string;
13+
}) {
1014
// Check for non-genuine connections
1115
const activeConnectionIds = useConnectionIds(
1216
(conn) =>
@@ -15,6 +19,7 @@ export function CompassAssistantDrawerWithConnections() {
1519
);
1620
return (
1721
<CompassAssistantDrawer
22+
appName={appName}
1823
hasNonGenuineConnections={activeConnectionIds.length > 0}
1924
/>
2025
);

packages/compass/src/app/components/workspace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default function Workspace({
112112
<CreateNamespacePlugin></CreateNamespacePlugin>
113113
<DropNamespacePlugin></DropNamespacePlugin>
114114
<RenameCollectionPlugin></RenameCollectionPlugin>
115-
<CompassAssistantDrawerWithConnections />
115+
<CompassAssistantDrawerWithConnections appName="Compass" />
116116
</>
117117
)}
118118
></WorkspacesPlugin>

0 commit comments

Comments
 (0)