Skip to content

Commit f00744e

Browse files
committed
card list and toolbar
1 parent bd6a3dd commit f00744e

File tree

3 files changed

+278
-55
lines changed

3 files changed

+278
-55
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {
2+
Card,
3+
css,
4+
cx,
5+
ItemActionMenu,
6+
palette,
7+
spacing,
8+
Subtitle,
9+
useDarkMode,
10+
} from '@mongodb-js/compass-components';
11+
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
12+
import React from 'react';
13+
14+
// Same as saved-queries-aggregations
15+
export const CARD_WIDTH = spacing[1600] * 4;
16+
export const CARD_HEIGHT = 218;
17+
18+
const diagramCardStyles = css({
19+
width: CARD_WIDTH,
20+
height: CARD_HEIGHT,
21+
display: 'flex',
22+
flexDirection: 'column',
23+
});
24+
25+
const cardHeaderStyles = css({
26+
display: 'flex',
27+
gap: spacing[200],
28+
alignItems: 'flex-start',
29+
});
30+
const cardTitle = css({
31+
fontWeight: 'bold',
32+
height: spacing[600] * 2,
33+
marginBottom: spacing[400],
34+
35+
// WebkitLineClamp css property is in a very weird state in the spec and
36+
// requires using deprecated flexbox spec props, but this does work in
37+
// (Chromium) Electron and is the only way to get multiline text overflow to
38+
// work using CSS only
39+
//
40+
// See: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp
41+
display: '-webkit-box',
42+
WebkitBoxOrient: 'vertical',
43+
WebkitLineClamp: 2,
44+
textOverflow: 'ellipsis',
45+
overflow: 'hidden',
46+
});
47+
48+
const cardTitleDark = css({
49+
color: palette.green.light2,
50+
});
51+
const cardTitleLight = css({
52+
color: palette.green.dark2,
53+
});
54+
55+
export function DiagramCard({
56+
diagram,
57+
onOpen,
58+
onRename,
59+
onDelete,
60+
}: {
61+
diagram: MongoDBDataModelDescription;
62+
onOpen: (diagram: MongoDBDataModelDescription) => void;
63+
onRename: (id: string) => void;
64+
onDelete: (id: string) => void;
65+
}) {
66+
const darkmode = useDarkMode();
67+
return (
68+
<Card
69+
className={diagramCardStyles}
70+
contentStyle="clickable"
71+
onClick={() => onOpen(diagram)}
72+
data-testid="saved-diagram-card"
73+
data-diagram-name={diagram.name}
74+
title={diagram.name}
75+
>
76+
<div className={cardHeaderStyles}>
77+
<Subtitle
78+
as="div"
79+
className={cx(cardTitle, darkmode ? cardTitleDark : cardTitleLight)}
80+
title={diagram.name}
81+
>
82+
{diagram.name}
83+
</Subtitle>
84+
<ItemActionMenu
85+
isVisible
86+
actions={[
87+
{ action: 'rename', label: 'Rename' },
88+
{ action: 'delete', label: 'Delete' },
89+
]}
90+
onAction={(action) => {
91+
switch (action) {
92+
case 'rename':
93+
onRename(diagram.id);
94+
break;
95+
case 'delete':
96+
onDelete(diagram.id);
97+
break;
98+
default:
99+
break;
100+
}
101+
}}
102+
></ItemActionMenu>
103+
</div>
104+
</Card>
105+
);
106+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { useCallback, useState } from 'react';
2+
import {
3+
Button,
4+
css,
5+
cx,
6+
Icon,
7+
palette,
8+
SearchInput,
9+
spacing,
10+
Subtitle,
11+
useDarkMode,
12+
} from '@mongodb-js/compass-components';
13+
14+
const containerStyles = css({
15+
padding: spacing[400],
16+
display: 'grid',
17+
gridTemplateAreas: `
18+
'title createDiagram'
19+
'searchInput sortControls'
20+
`,
21+
columnGap: spacing[800],
22+
rowGap: spacing[200],
23+
gridTemplateColumns: '5fr',
24+
});
25+
26+
const titleStyles = css({
27+
gridArea: 'title',
28+
});
29+
const createDiagramContainerStyles = css({
30+
gridArea: 'createDiagram',
31+
display: 'flex',
32+
justifyContent: 'flex-end',
33+
});
34+
const searchInputStyles = css({
35+
gridArea: 'searchInput',
36+
});
37+
const sortControlsStyles = css({
38+
gridArea: 'sortControls',
39+
});
40+
41+
const toolbarTitleLightStyles = css({ color: palette.gray.dark1 });
42+
const toolbarTitleDarkStyles = css({ color: palette.gray.light1 });
43+
44+
export function DiagramListToolbar({
45+
sortControls,
46+
onFilter,
47+
onCreateDiagramClick,
48+
}: {
49+
sortControls: React.ReactElement;
50+
onFilter: (search: string) => void;
51+
onCreateDiagramClick: () => void;
52+
}) {
53+
const [search, setSearch] = useState('');
54+
const darkMode = useDarkMode();
55+
56+
const onSearch = useCallback(
57+
(text: string) => {
58+
setSearch(text);
59+
onFilter(text);
60+
},
61+
[onFilter]
62+
);
63+
64+
return (
65+
<div className={containerStyles}>
66+
<Subtitle
67+
className={cx(
68+
titleStyles,
69+
darkMode ? toolbarTitleDarkStyles : toolbarTitleLightStyles
70+
)}
71+
>
72+
Open an existing diagram:
73+
</Subtitle>
74+
<div className={createDiagramContainerStyles}>
75+
<Button
76+
onClick={onCreateDiagramClick}
77+
variant="primary"
78+
size="small"
79+
data-testid="create-diagram-button"
80+
leftGlyph={<Icon glyph="Plus"></Icon>}
81+
>
82+
Generate new diagram
83+
</Button>
84+
</div>
85+
<SearchInput
86+
aria-label="Search diagrams"
87+
value={search}
88+
className={searchInputStyles}
89+
onChange={(e) => onSearch(e.target.value)}
90+
/>
91+
<div className={sortControlsStyles}>{sortControls}</div>
92+
</div>
93+
);
94+
}

packages/compass-data-modeling/src/components/saved-diagrams-list.tsx

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
1-
import React from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import { connect } from 'react-redux';
33
import { createNewDiagram } from '../store/generate-diagram-wizard';
44
import {
55
Button,
6-
Card,
6+
css,
77
EmptyContent,
88
Icon,
9-
ItemActionMenu,
9+
spacing,
10+
useSortControls,
11+
useSortedItems,
12+
VirtualGrid,
1013
WorkspaceContainer,
1114
} from '@mongodb-js/compass-components';
1215
import { useDataModelSavedItems } from '../provider';
1316
import { deleteDiagram, openDiagram, renameDiagram } from '../store/diagram';
1417
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
18+
import { CARD_HEIGHT, CARD_WIDTH, DiagramCard } from './diagram-card';
19+
import { DiagramListToolbar } from './diagram-list-toolbar';
20+
21+
const sortBy = [
22+
{
23+
name: 'name',
24+
label: 'Name',
25+
},
26+
// TODO: Currently we do not have lastModified.
27+
// {
28+
// name: 'lastModified',
29+
// label: 'Last Modified',
30+
// },
31+
] as const;
32+
33+
const contentStyles = css({
34+
paddingLeft: spacing[400],
35+
paddingRight: spacing[400],
36+
width: '100%',
37+
height: '100%',
38+
});
39+
40+
const rowStyles = css({
41+
gap: spacing[200],
42+
paddingLeft: spacing[400],
43+
paddingRight: spacing[400],
44+
paddingBottom: spacing[200],
45+
});
1546

1647
const SavedDiagramsList: React.FunctionComponent<{
1748
onCreateDiagramClick: () => void;
@@ -26,6 +57,23 @@ const SavedDiagramsList: React.FunctionComponent<{
2657
}) => {
2758
const { items, status } = useDataModelSavedItems();
2859

60+
const [filteredItems, setFilteredItems] = useState(items);
61+
const [sortControls, sortState] = useSortControls(sortBy);
62+
const sortedItems = useSortedItems(filteredItems, sortState);
63+
64+
const onFilterItems = useCallback(
65+
(search: string) => {
66+
try {
67+
const regex = new RegExp(search, 'i');
68+
// TODO: Currently only searching for name. Add more fields
69+
setFilteredItems(items.filter((x) => regex.test(x.name)));
70+
} catch {
71+
setFilteredItems(items);
72+
}
73+
},
74+
[items]
75+
);
76+
2977
if (status === 'INITIAL' || status === 'LOADING') {
3078
return null;
3179
}
@@ -36,38 +84,32 @@ const SavedDiagramsList: React.FunctionComponent<{
3684

3785
if (showList) {
3886
content = (
39-
<div>
40-
{items.map((diagram) => {
41-
return (
42-
<Card
43-
style={{ marginTop: 8, display: 'flex' }}
44-
key={diagram.id}
45-
onClick={() => {
46-
onOpenDiagramClick(diagram);
47-
}}
48-
data-testid="saved-diagram-card"
49-
data-diagram-name={diagram.name}
50-
>
51-
{diagram.name}
52-
<ItemActionMenu
53-
isVisible
54-
actions={[
55-
{ action: 'rename', label: 'Rename' },
56-
{ action: 'delete', label: 'Delete' },
57-
]}
58-
onAction={(action) => {
59-
if (action === 'rename') {
60-
onDiagramRenameClick(diagram.id);
61-
}
62-
if (action === 'delete') {
63-
onDiagramDeleteClick(diagram.id);
64-
}
65-
}}
66-
></ItemActionMenu>
67-
</Card>
68-
);
69-
})}
70-
</div>
87+
<VirtualGrid
88+
data-testid="data-modeling-diagrams-list"
89+
itemMinWidth={CARD_WIDTH}
90+
itemHeight={CARD_HEIGHT + spacing[200]}
91+
itemsCount={sortedItems.length}
92+
renderItem={({ index }) => (
93+
<DiagramCard
94+
diagram={sortedItems[index]}
95+
onOpen={onOpenDiagramClick}
96+
onRename={onDiagramRenameClick}
97+
onDelete={onDiagramDeleteClick}
98+
/>
99+
)}
100+
itemKey={(index: number) => sortedItems[index].id}
101+
renderHeader={() => (
102+
<DiagramListToolbar
103+
onCreateDiagramClick={onCreateDiagramClick}
104+
onFilter={onFilterItems}
105+
sortControls={sortControls}
106+
/>
107+
)}
108+
headerHeight={spacing[800] + 36}
109+
// renderEmptyList={NoSearchResults}
110+
classNames={{ row: rowStyles }}
111+
resetActiveItemOnBlur={false}
112+
></VirtualGrid>
71113
);
72114
} else {
73115
content = (
@@ -95,26 +137,7 @@ const SavedDiagramsList: React.FunctionComponent<{
95137
);
96138
}
97139

98-
return (
99-
<WorkspaceContainer
100-
toolbar={() => {
101-
return showList ? (
102-
<>
103-
<Button
104-
onClick={onCreateDiagramClick}
105-
variant="primary"
106-
size="xsmall"
107-
data-testid="create-diagram-button"
108-
>
109-
Create diagram
110-
</Button>
111-
</>
112-
) : null;
113-
}}
114-
>
115-
{content}
116-
</WorkspaceContainer>
117-
);
140+
return <WorkspaceContainer>{content}</WorkspaceContainer>;
118141
};
119142

120143
export default connect(null, {

0 commit comments

Comments
 (0)