Skip to content

Commit d67c6e6

Browse files
authored
feature/COMPASS-9847 show node warning icon (#165)
* show node warning icon * fix default type and add tests * add story with warning message and long title * discriminated type to single prop * fix storybook * justify popover to center
1 parent 8f1e0a9 commit d67c6e6

File tree

9 files changed

+332
-11
lines changed

9 files changed

+332
-11
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@leafygreen-ui/leafygreen-provider": "^5.0.2",
5757
"@leafygreen-ui/palette": "^5.0.0",
5858
"@leafygreen-ui/tokens": "^3.2.1",
59+
"@leafygreen-ui/tooltip": "^14.2.1",
5960
"@leafygreen-ui/typography": "^22.1.0",
6061
"@xyflow/react": "12.5.1",
6162
"d3-path": "^3.1.0",

src/components/diagram.stories.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ export const DiagramWithEditInteractions: Story = {
8888
editable: true,
8989
})),
9090
],
91+
variant: {
92+
type: 'warn',
93+
warnMessage: 'This is a warning message for the Orders node.',
94+
},
9195
},
9296
{
9397
...EMPLOYEES_NODE,

src/components/node/node.stories.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,54 @@ export const NodeWithSelectedFields: Story = {
528528
},
529529
};
530530

531+
export const NodeWithWarningIcon: Story = {
532+
args: {
533+
...INTERNAL_NODE,
534+
data: {
535+
title: 'orders',
536+
variant: {
537+
type: 'warn',
538+
warnMessage: 'This is a warning message for the Orders node.',
539+
},
540+
fields: [
541+
{
542+
name: '_id',
543+
type: 'objectid',
544+
glyphs: ['key'],
545+
},
546+
{
547+
name: 'customer',
548+
type: '{}',
549+
},
550+
],
551+
},
552+
},
553+
};
554+
555+
export const NodeWithLongTitleAndWarningIcon: Story = {
556+
args: {
557+
...INTERNAL_NODE,
558+
data: {
559+
title: 'orders_with_a_very_long_title_exceeding_normal_length_limits',
560+
variant: {
561+
type: 'warn',
562+
warnMessage: 'This is a warning message for the Orders node.',
563+
},
564+
fields: [
565+
{
566+
name: '_id',
567+
type: 'objectid',
568+
glyphs: ['key'],
569+
},
570+
{
571+
name: 'customer',
572+
type: '{}',
573+
},
574+
],
575+
},
576+
},
577+
};
578+
531579
export const SelectedBorder: Story = {
532580
args: { ...INTERNAL_NODE, data: { ...INTERNAL_NODE.data, borderVariant: 'selected' } },
533581
};

src/components/node/node.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { fontFamilies, spacing } from '@leafygreen-ui/tokens';
44
import { useTheme } from '@emotion/react';
55
import Icon from '@leafygreen-ui/icon';
66
import { useCallback, useState } from 'react';
7+
import { Tooltip } from '@leafygreen-ui/tooltip';
8+
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
9+
import { palette } from '@leafygreen-ui/palette';
10+
import { Body } from '@leafygreen-ui/typography';
711

812
import { DEFAULT_NODE_HEADER_HEIGHT, ZOOM_THRESHOLD } from '@/utilities/constants';
913
import { InternalNode } from '@/types/internal';
@@ -81,10 +85,14 @@ const NodeHeaderIcon = styled.div`
8185
margin-right: ${spacing[100]}px;
8286
`;
8387

88+
const NodeHeaderTitleWrapper = styled.div`
89+
margin-right: ${spacing[200]}px;
90+
min-width: 0;
91+
`;
92+
8493
export const NodeHeaderTitle = styled.div`
94+
display: inline;
8595
overflow-wrap: break-word;
86-
min-width: 0;
87-
margin-right: ${spacing[200]}px;
8896
`;
8997

9098
const NodeHandle = styled(Handle)<{ ['z-index']?: number }>`
@@ -113,14 +121,22 @@ const TitleControlsContainer = styled.div`
113121
}
114122
`;
115123

124+
const IconWrapper = styled.div<{ darkMode: boolean }>`
125+
color: ${props => (props.darkMode ? palette.yellow.light2 : palette.yellow.dark2)};
126+
display: inline;
127+
vertical-align: sub;
128+
margin-left: ${spacing[100]}px;
129+
`;
130+
116131
export const Node = ({
117132
id,
118133
type,
119134
selected,
120135
isConnectable,
121-
data: { title, fields, borderVariant, disabled },
136+
data: { title, fields, borderVariant, disabled, variant },
122137
}: NodeProps<InternalNode>) => {
123138
const theme = useTheme();
139+
const { darkMode } = useDarkMode();
124140
const { zoom } = useViewport();
125141

126142
const [isHovering, setHovering] = useState(false);
@@ -222,7 +238,22 @@ export const Node = ({
222238
<NodeHeaderIcon>
223239
<Icon fill={theme.node.headerIcon} glyph="Drag" />
224240
</NodeHeaderIcon>
225-
<NodeHeaderTitle>{title}</NodeHeaderTitle>
241+
<NodeHeaderTitleWrapper>
242+
<NodeHeaderTitle>{title}</NodeHeaderTitle>
243+
{variant?.type === 'warn' && (
244+
<Tooltip
245+
renderMode="portal"
246+
justify="middle"
247+
trigger={
248+
<IconWrapper darkMode={darkMode}>
249+
<Icon glyph="Warning" />
250+
</IconWrapper>
251+
}
252+
>
253+
<Body>{variant.warnMessage}</Body>
254+
</Tooltip>
255+
)}
256+
</NodeHeaderTitleWrapper>
226257
<TitleControlsContainer>
227258
{addFieldToNodeClickHandler && (
228259
<DiagramIconButton aria-label="Add Field" onClick={onClickAddFieldToNode} title="Add Field">

src/types/internal.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Node as ReactFlowNode } from '@xyflow/react';
22

3-
import { NodeBorderVariant, NodeField } from '@/types/node';
3+
import { NodeBorderVariant, NodeField, NodeVariant } from '@/types/node';
44
import { EdgeProps } from '@/types/edge';
55

66
export type NodeData = {
77
title: string;
88
disabled?: boolean;
99
fields: NodeField[];
1010
borderVariant?: NodeBorderVariant;
11+
variant?: NodeVariant;
1112
};
1213

1314
export type InternalNode = ReactFlowNode<NodeData>;

src/types/node.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ export interface Measured {
5555

5656
export type FieldId = string | string[];
5757

58+
export type NodeVariant =
59+
| {
60+
type?: 'default';
61+
}
62+
| {
63+
type: 'warn';
64+
warnMessage: string;
65+
};
66+
5867
export interface NodeProps {
5968
/**
6069
* Unique identifier for the node.
@@ -66,11 +75,6 @@ export interface NodeProps {
6675
*/
6776
title: string;
6877

69-
/**
70-
* Actions to display in the node header, optional.
71-
*/
72-
actions?: React.ReactNode;
73-
7478
/**
7579
* Whether the node is disabled.
7680
*/
@@ -135,6 +139,11 @@ export interface NodeProps {
135139
* Optional CSS class name for the node.
136140
*/
137141
className?: string;
142+
143+
/**
144+
* The variant of the node.
145+
*/
146+
variant?: NodeVariant;
138147
}
139148

140149
export interface NodeField {

src/utilities/convert-nodes.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,60 @@ describe('convert-nodes', () => {
3131
fields: [],
3232
});
3333
});
34+
it('Should convert to external node when variant=default', () => {
35+
const internalNode: InternalNode = {
36+
id: 'node-1',
37+
type: 'collection',
38+
position: { x: 100, y: 200 },
39+
data: {
40+
title: 'some-title',
41+
fields: [],
42+
variant: {
43+
type: 'default',
44+
},
45+
},
46+
};
47+
48+
const result = convertToExternalNode(internalNode);
49+
expect(result).toEqual({
50+
id: 'node-1',
51+
type: 'collection' as NodeType,
52+
position: { x: 100, y: 200 },
53+
title: 'some-title',
54+
fields: [],
55+
variant: {
56+
type: 'default',
57+
},
58+
});
59+
});
60+
it('Should convert to external node when variant=warn', () => {
61+
const internalNode: InternalNode = {
62+
id: 'node-1',
63+
type: 'collection',
64+
position: { x: 100, y: 200 },
65+
data: {
66+
title: 'some-title',
67+
fields: [],
68+
variant: {
69+
type: 'warn',
70+
warnMessage: 'This is a warning',
71+
},
72+
},
73+
};
74+
75+
const result = convertToExternalNode(internalNode);
76+
expect(result).toEqual({
77+
id: 'node-1',
78+
type: 'collection' as NodeType,
79+
position: { x: 100, y: 200 },
80+
title: 'some-title',
81+
fields: [],
82+
variant: {
83+
type: 'warn',
84+
warnMessage: 'This is a warning',
85+
},
86+
});
87+
});
3488
});
3589

3690
describe('convertToExternalNodes', () => {
@@ -141,6 +195,68 @@ describe('convert-nodes', () => {
141195
},
142196
});
143197
});
198+
it('Should be handle node variant=default', () => {
199+
const node = {
200+
id: 'node-1',
201+
type: 'table' as const,
202+
position: { x: 100, y: 200 },
203+
title: 'some-title',
204+
fields: [],
205+
selectable: true,
206+
variant: {
207+
type: 'default' as const,
208+
},
209+
};
210+
const result = convertToInternalNode(node);
211+
expect(result).toEqual({
212+
id: 'node-1',
213+
type: 'table',
214+
position: { x: 100, y: 200 },
215+
connectable: false,
216+
selectable: true,
217+
data: {
218+
title: 'some-title',
219+
fields: [],
220+
borderVariant: undefined,
221+
disabled: undefined,
222+
variant: {
223+
type: 'default',
224+
},
225+
},
226+
});
227+
});
228+
it('Should be handle node variant=warn', () => {
229+
const node = {
230+
id: 'node-1',
231+
type: 'table' as const,
232+
position: { x: 100, y: 200 },
233+
title: 'some-title',
234+
fields: [],
235+
selectable: true,
236+
variant: {
237+
type: 'warn' as const,
238+
warnMessage: 'This is a warning',
239+
},
240+
};
241+
const result = convertToInternalNode(node);
242+
expect(result).toEqual({
243+
id: 'node-1',
244+
type: 'table',
245+
position: { x: 100, y: 200 },
246+
connectable: false,
247+
selectable: true,
248+
data: {
249+
title: 'some-title',
250+
fields: [],
251+
borderVariant: undefined,
252+
disabled: undefined,
253+
variant: {
254+
type: 'warn',
255+
warnMessage: 'This is a warning',
256+
},
257+
},
258+
});
259+
});
144260
});
145261

146262
describe('convertToInternalNodes', () => {

src/utilities/convert-nodes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const convertToExternalNodes = (nodes: InternalNode[]): NodeProps[] => {
1515
};
1616

1717
export const convertToInternalNode = (node: NodeProps): InternalNode => {
18-
const { title, fields, borderVariant, disabled, connectable, ...rest } = node;
18+
const { title, fields, borderVariant, disabled, connectable, variant, ...rest } = node;
1919
return {
2020
...rest,
2121
connectable: connectable ?? false,
@@ -24,6 +24,7 @@ export const convertToInternalNode = (node: NodeProps): InternalNode => {
2424
disabled,
2525
fields,
2626
borderVariant,
27+
variant,
2728
},
2829
};
2930
};

0 commit comments

Comments
 (0)