Skip to content

Commit da480de

Browse files
authored
feat(data-modeling): show collection name / relationship label in drawer header; show item associated actions in header COMPASS-9653 (#7176)
* chore(data-modeling): show collection name / relationship label in drawer header; show item associated actions in header * chore(data-modeling): adjust tests * chore(components): add test for drawer portal
1 parent db7e9b8 commit da480de

File tree

6 files changed

+215
-76
lines changed

6 files changed

+215
-76
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { useState } from 'react';
2+
import { render, screen, waitFor } from '@mongodb-js/testing-library-compass';
3+
import {
4+
DrawerContentProvider,
5+
DrawerSection,
6+
DrawerAnchor,
7+
} from './drawer-portal';
8+
import { expect } from 'chai';
9+
10+
describe('DrawerSection', function () {
11+
it('renders DrawerSection in the portal and updates the content when it updates', async function () {
12+
let setCount;
13+
14+
function TestDrawer() {
15+
const [count, _setCount] = useState(0);
16+
setCount = _setCount;
17+
return (
18+
<DrawerContentProvider>
19+
<DrawerAnchor>
20+
<DrawerSection
21+
id="test-section"
22+
label="Test section"
23+
title={`Test section: ${count}`}
24+
glyph="Trash"
25+
autoOpen
26+
>
27+
This is a test section and the count is {count}
28+
</DrawerSection>
29+
</DrawerAnchor>
30+
</DrawerContentProvider>
31+
);
32+
}
33+
34+
render(<TestDrawer></TestDrawer>);
35+
36+
await waitFor(() => {
37+
expect(screen.getByText('Test section: 0')).to.be.visible;
38+
expect(screen.getByText('This is a test section and the count is 0')).to
39+
.be.visible;
40+
});
41+
42+
setCount(42);
43+
44+
await waitFor(() => {
45+
expect(screen.getByText('Test section: 42')).to.be.visible;
46+
expect(screen.getByText('This is a test section and the count is 42')).to
47+
.be.visible;
48+
});
49+
});
50+
});

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ const drawerLayoutFixesStyles = css({
146146
'& > div:nth-child(2)': {
147147
marginTop: -1, // hiding the top border as we already have one in the place where the Anchor is currently rendered
148148
},
149+
150+
// We're stretching the title container to all available width so that we can
151+
// layout the controls there better. Doing our best to target the section
152+
// title here, leafygreen really doesn't give us anything else to try.
153+
//
154+
// TODO(ticket): This is obviously a horrible selector and we should make sure
155+
// that LG team provides a better one for us to achieve this behavior when
156+
// we're removing the vendored version of the drawer
157+
'& > div:nth-child(2) > div:nth-child(2) > div:first-child > div:first-child > div:first-child > div:first-child':
158+
{
159+
flex: 'none',
160+
width: 'calc(100% - 28px)', // disallow going over the title size (100 - close button width)
161+
overflow: 'hidden',
162+
},
149163
});
150164

151165
const emptyDrawerLayoutFixesStyles = css({

packages/compass-components/src/components/drawer/drawer-toolbar-layout/drawer-toolbar-layout-container.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,13 @@ export const DrawerToolbarLayoutContainer = forwardRef<
3939
) => {
4040
const { openDrawer, closeDrawer, getActiveDrawerContent, isDrawerOpen } =
4141
useDrawerToolbarContext();
42-
const { id, title, content } = getActiveDrawerContent() || {};
42+
const { id } = getActiveDrawerContent() || {};
4343
const lgIds = getLgIds(dataLgId);
4444
const hasData = toolbarData && toolbarData.length > 0;
45+
const { title, content } =
46+
toolbarData.find((data) => {
47+
return data.id === id;
48+
}) ?? {};
4549

4650
const handleOnClose = (event: React.MouseEvent<HTMLButtonElement>) => {
4751
onClose?.(event);

packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ describe('DiagramEditorSidePanel', function () {
7373
result.plugin.store.dispatch(selectCollection('flights.airlines'));
7474

7575
await waitFor(() => {
76-
const nameInput = screen.getByLabelText('Name');
77-
expect(nameInput).to.be.visible;
78-
expect(nameInput).to.have.value('flights.airlines');
76+
expect(screen.getByTitle('flights.airlines')).to.exist;
7977
});
8078

79+
const nameInput = screen.getByLabelText('Name');
80+
expect(nameInput).to.be.visible;
81+
expect(nameInput).to.have.value('flights.airlines');
82+
8183
userEvent.click(screen.getByRole('textbox', { name: 'Notes' }));
8284
userEvent.type(
8385
screen.getByRole('textbox', { name: 'Notes' }),
@@ -104,8 +106,8 @@ describe('DiagramEditorSidePanel', function () {
104106
);
105107

106108
await waitFor(() => {
107-
const section = screen.getByText('Relationship properties');
108-
expect(section).to.be.visible;
109+
expect(screen.getByTitle('countries.name → airports.Country')).to.be
110+
.visible;
109111
});
110112

111113
const localCollectionInput = screen.getByLabelText('Local collection');
Lines changed: 137 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,162 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
import { connect } from 'react-redux';
33
import type { DataModelingState } from '../../store/reducer';
4-
import { DrawerSection } from '@mongodb-js/compass-components';
4+
import {
5+
css,
6+
DrawerSection,
7+
ItemActionControls,
8+
} from '@mongodb-js/compass-components';
59
import CollectionDrawerContent from './collection-drawer-content';
610
import RelationshipDrawerContent from './relationship-drawer-content';
11+
import {
12+
deleteRelationship,
13+
selectCurrentModelFromState,
14+
} from '../../store/diagram';
15+
import { getDefaultRelationshipName } from '../../utils';
16+
717
export const DATA_MODELING_DRAWER_ID = 'data-modeling-drawer';
818

19+
const drawerTitleStyles = css({
20+
display: 'flex',
21+
width: '100%',
22+
});
23+
24+
const drawerTitleTextStyles = css({
25+
flex: 1,
26+
whiteSpace: 'nowrap',
27+
overflow: 'hidden',
28+
textOverflow: 'ellipsis',
29+
});
30+
31+
const drawerTitleActionGroupStyles = css({});
32+
933
type DiagramEditorSidePanelProps = {
10-
selectedItems: { type: 'relationship' | 'collection'; id: string } | null;
34+
selectedItems: {
35+
id: string;
36+
type: 'relationship' | 'collection';
37+
label: string;
38+
} | null;
39+
onDeleteRelationship: (rId: string) => void;
1140
};
1241

1342
function DiagramEditorSidePanel({
1443
selectedItems,
44+
onDeleteRelationship,
1545
}: DiagramEditorSidePanelProps) {
16-
if (!selectedItems) {
17-
return null;
18-
}
46+
const { content, label, actions, handleAction } = useMemo(() => {
47+
if (selectedItems?.type === 'collection') {
48+
return {
49+
label: selectedItems.label,
50+
content: (
51+
<CollectionDrawerContent
52+
key={selectedItems.id}
53+
namespace={selectedItems.id}
54+
></CollectionDrawerContent>
55+
),
56+
actions: [],
57+
handleAction: () => {},
58+
};
59+
}
60+
61+
if (selectedItems?.type === 'relationship') {
62+
return {
63+
label: selectedItems.label,
64+
content: (
65+
<RelationshipDrawerContent
66+
key={selectedItems.id}
67+
relationshipId={selectedItems.id}
68+
></RelationshipDrawerContent>
69+
),
70+
actions: [
71+
{ action: 'delete', label: 'Delete', icon: 'Trash' as const },
72+
],
73+
handleAction: (actionName: string) => {
74+
if (actionName === 'delete') {
75+
onDeleteRelationship(selectedItems.id);
76+
}
77+
},
78+
};
79+
}
80+
81+
return { content: null };
82+
}, [selectedItems, onDeleteRelationship]);
1983

20-
let content;
21-
22-
if (selectedItems.type === 'collection') {
23-
content = (
24-
<CollectionDrawerContent
25-
key={selectedItems.id}
26-
namespace={selectedItems.id}
27-
></CollectionDrawerContent>
28-
);
29-
} else if (selectedItems.type === 'relationship') {
30-
content = (
31-
<RelationshipDrawerContent
32-
key={selectedItems.id}
33-
relationshipId={selectedItems.id}
34-
></RelationshipDrawerContent>
35-
);
84+
if (!content) {
85+
return null;
3686
}
3787

3888
return (
3989
<DrawerSection
4090
id={DATA_MODELING_DRAWER_ID}
41-
title="Details"
42-
label="Details"
43-
glyph="InfoWithCircle"
91+
title={
92+
<div className={drawerTitleStyles}>
93+
<span className={drawerTitleTextStyles} title={label}>
94+
{label}
95+
</span>
96+
97+
<ItemActionControls
98+
actions={actions}
99+
iconSize="small"
100+
onAction={handleAction}
101+
className={drawerTitleActionGroupStyles}
102+
// Because the close button here is out of our control, we have do
103+
// adjust the actions rendering in a bit of an unconventional way:
104+
// if there's more than one action available, collapse it to "...",
105+
// if it's just one, make sure button is not collapsed by setting
106+
// collapseAfter to >0
107+
collapseAfter={actions.length > 1 ? 0 : 1}
108+
></ItemActionControls>
109+
</div>
110+
}
111+
label={label}
112+
glyph="Wrench"
44113
autoOpen
45-
// TODO: Leafygreen doesn't allow us to tie close event to a particular
46-
// action. We can add this functionality ourselves, but I'm not sure that
47-
// adding even more logic on top of the drawer is a good idea. Maybe we're
48-
// okay with the drawer close button click just staying there until you
49-
// explicitly click something else?
50-
// onClose={onClose}
51114
>
52115
{content}
53116
</DrawerSection>
54117
);
55118
}
56119

57-
export default connect((state: DataModelingState) => {
58-
return {
59-
selectedItems: state.diagram?.selectedItems ?? null,
60-
};
61-
})(DiagramEditorSidePanel);
120+
export default connect(
121+
(state: DataModelingState) => {
122+
const selected = state.diagram?.selectedItems;
123+
124+
if (!selected) {
125+
return {
126+
selectedItems: null,
127+
};
128+
}
129+
130+
if (selected.type === 'collection') {
131+
return {
132+
selectedItems: {
133+
...selected,
134+
label: selected.id,
135+
},
136+
};
137+
}
138+
139+
if (selected.type === 'relationship') {
140+
const model = selectCurrentModelFromState(state);
141+
const relationship = model.relationships.find((relationship) => {
142+
return relationship.id === selected.id;
143+
});
144+
145+
if (!relationship) {
146+
throw new Error(
147+
'Can not find corresponding relationship when rendering DiagramEditorSidePanel'
148+
);
149+
}
150+
151+
return {
152+
selectedItems: {
153+
...selected,
154+
label: getDefaultRelationshipName(relationship.relationship),
155+
},
156+
};
157+
}
158+
},
159+
{
160+
onDeleteRelationship: deleteRelationship,
161+
}
162+
)(DiagramEditorSidePanel);

packages/compass-data-modeling/src/components/drawer/relationship-drawer-content.tsx

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {
99
spacing,
1010
css,
1111
palette,
12-
Button,
13-
Icon,
1412
TextArea,
1513
} from '@mongodb-js/compass-components';
1614
import {
@@ -34,7 +32,6 @@ type RelationshipDrawerContentProps = {
3432
relationship: Relationship;
3533
fields: Record<string, string[][]>;
3634
onRelationshipUpdate: (relationship: Relationship) => void;
37-
onDeleteRelationshipClick: (rId: string) => void;
3835
};
3936

4037
type RelationshipFormFields = {
@@ -47,11 +44,6 @@ type RelationshipFormFields = {
4744
note: string;
4845
};
4946

50-
const titleBtnStyles = css({
51-
marginLeft: 'auto',
52-
maxHeight: 20, // to make sure we're matching accordion line height
53-
});
54-
5547
const FIELD_DIVIDER = '~~##$$##~~';
5648

5749
function useRelationshipFormFields(
@@ -156,13 +148,7 @@ const configurationForeignFieldStyles = css({
156148

157149
const RelationshipDrawerContent: React.FunctionComponent<
158150
RelationshipDrawerContentProps
159-
> = ({
160-
relationshipId,
161-
relationship,
162-
fields,
163-
onRelationshipUpdate,
164-
onDeleteRelationshipClick,
165-
}) => {
151+
> = ({ relationshipId, relationship, fields, onRelationshipUpdate }) => {
166152
const collections = useMemo(() => {
167153
return Object.keys(fields);
168154
}, [fields]);
@@ -192,25 +178,7 @@ const RelationshipDrawerContent: React.FunctionComponent<
192178

193179
return (
194180
<div data-relationship-id={relationshipId}>
195-
<DMDrawerSection
196-
label={
197-
<>
198-
<span>Relationship properties</span>
199-
200-
<Button
201-
variant="dangerOutline"
202-
leftGlyph={<Icon glyph="Trash" />}
203-
className={titleBtnStyles}
204-
size="xsmall"
205-
onClick={() => {
206-
onDeleteRelationshipClick(relationshipId);
207-
}}
208-
>
209-
Delete
210-
</Button>
211-
</>
212-
}
213-
>
181+
<DMDrawerSection label="Relationship properties">
214182
<div className={configurationContainerStyles}>
215183
<div className={configurationLocalFieldStyles}>
216184
<DMFormFieldContainer>

0 commit comments

Comments
 (0)