Skip to content

Commit c42d569

Browse files
authored
feat/COMPASS-9935 collapse field level (#167)
1 parent 124e316 commit c42d569

32 files changed

+1050
-616
lines changed

src/components/canvas/canvas.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { SelfReferencingEdge } from '@/components/edge/self-referencing-edge';
2020
import { FieldEdge } from '@/components/edge/field-edge';
2121
import { MarkerList } from '@/components/markers/marker-list';
2222
import { ConnectionLine } from '@/components/line/connection-line';
23-
import { convertToExternalNode, convertToExternalNodes, convertToInternalNodes } from '@/utilities/convert-nodes';
23+
import { getExternalNode, convertToInternalNodes } from '@/utilities/convert-nodes';
2424
import { convertToExternalEdge, convertToExternalEdges, convertToInternalEdges } from '@/utilities/convert-edges';
2525
import { EditableDiagramInteractionsProvider } from '@/hooks/use-editable-diagram-interactions';
2626

@@ -66,6 +66,7 @@ export const Canvas = ({
6666
onFieldNameChange,
6767
onFieldTypeChange,
6868
onFieldClick,
69+
onFieldExpandToggle,
6970
onNodeContextMenu,
7071
onNodeDrag,
7172
onNodeDragStop,
@@ -94,28 +95,28 @@ export const Canvas = ({
9495

9596
const _onNodeContextMenu = useCallback(
9697
(event: MouseEvent, node: InternalNode) => {
97-
onNodeContextMenu?.(event, convertToExternalNode(node));
98+
onNodeContextMenu?.(event, getExternalNode(node));
9899
},
99100
[onNodeContextMenu],
100101
);
101102

102103
const _onNodeDrag = useCallback(
103104
(event: MouseEvent, node: InternalNode, nodes: InternalNode[]) => {
104-
onNodeDrag?.(event, convertToExternalNode(node), convertToExternalNodes(nodes));
105+
onNodeDrag?.(event, getExternalNode(node), nodes.map(getExternalNode));
105106
},
106107
[onNodeDrag],
107108
);
108109

109110
const _onNodeDragStop = useCallback(
110111
(event: MouseEvent, node: InternalNode, nodes: InternalNode[]) => {
111-
onNodeDragStop?.(event, convertToExternalNode(node), convertToExternalNodes(nodes));
112+
onNodeDragStop?.(event, getExternalNode(node), nodes.map(getExternalNode));
112113
},
113114
[onNodeDragStop],
114115
);
115116

116117
const _onSelectionDragStop = useCallback(
117118
(event: MouseEvent, nodes: InternalNode[]) => {
118-
onSelectionDragStop?.(event, convertToExternalNodes(nodes));
119+
onSelectionDragStop?.(event, nodes.map(getExternalNode));
119120
},
120121
[onSelectionDragStop],
121122
);
@@ -129,21 +130,21 @@ export const Canvas = ({
129130

130131
const _onNodeClick = useCallback(
131132
(event: MouseEvent, node: InternalNode) => {
132-
onNodeClick?.(event, convertToExternalNode(node));
133+
onNodeClick?.(event, getExternalNode(node));
133134
},
134135
[onNodeClick],
135136
);
136137

137138
const _onSelectionContextMenu = useCallback(
138139
(event: MouseEvent, nodes: InternalNode[]) => {
139-
onSelectionContextMenu?.(event, convertToExternalNodes(nodes));
140+
onSelectionContextMenu?.(event, nodes.map(getExternalNode));
140141
},
141142
[onSelectionContextMenu],
142143
);
143144

144145
const _onSelectionChange = useCallback(
145146
({ nodes, edges }: { nodes: InternalNode[]; edges: InternalEdge[] }) => {
146-
onSelectionChange?.({ nodes: convertToExternalNodes(nodes), edges: convertToExternalEdges(edges) });
147+
onSelectionChange?.({ nodes: nodes.map(getExternalNode), edges: convertToExternalEdges(edges) });
147148
},
148149
[onSelectionChange],
149150
);
@@ -153,6 +154,7 @@ export const Canvas = ({
153154
onFieldClick={onFieldClick}
154155
onAddFieldToNodeClick={onAddFieldToNodeClick}
155156
onNodeExpandToggle={onNodeExpandToggle}
157+
onFieldExpandToggle={onFieldExpandToggle}
156158
onAddFieldToObjectFieldClick={onAddFieldToObjectFieldClick}
157159
onFieldNameChange={onFieldNameChange}
158160
onFieldTypeChange={onFieldTypeChange}

src/components/diagram.stories.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ export const DiagramWithFieldToFieldEdges: Story = {
3131
title: 'MongoDB Diagram',
3232
isDarkMode: true,
3333
edges: [
34-
{ ...EMPLOYEES_TO_ORDERS_EDGE, sourceFieldIndex: 0, targetFieldIndex: 1 },
35-
{ ...EMPLOYEES_TO_EMPLOYEE_TERRITORIES_EDGE, sourceFieldIndex: 0, targetFieldIndex: 1 },
34+
{ ...EMPLOYEES_TO_ORDERS_EDGE, sourceFieldId: ['address', 'city'], targetFieldId: ['SUPPLIER_ID'] },
35+
{
36+
...EMPLOYEES_TO_EMPLOYEE_TERRITORIES_EDGE,
37+
sourceFieldId: ['employeeId'],
38+
targetFieldId: ['employeeId'],
39+
},
3640
],
3741
nodes: [
3842
{ ...EMPLOYEE_TERRITORIES_NODE, position: { x: 100, y: 100 } },
@@ -76,7 +80,13 @@ export const DiagramWithEditInteractions: Story = {
7680
args: {
7781
title: 'MongoDB Diagram',
7882
isDarkMode: true,
79-
edges: [],
83+
edges: [
84+
{
85+
...EMPLOYEES_TO_ORDERS_EDGE,
86+
sourceFieldId: ['employeeId'],
87+
targetFieldId: ['SUPPLIER_ID'],
88+
},
89+
],
8090
nodes: [
8191
{
8292
...ORDERS_NODE,

src/components/edge/field-edge.test.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ComponentProps } from 'react';
44
import { EMPLOYEES_NODE, ORDERS_NODE } from '@/mocks/datasets/nodes';
55
import { render, screen } from '@/mocks/testing-utils';
66
import { FieldEdge } from '@/components/edge/field-edge';
7+
import { InternalNode } from '@/types/internal';
78

89
vi.mock('@xyflow/react', async () => {
910
const actual = await vi.importActual<typeof import('@xyflow/react')>('@xyflow/react');
@@ -19,9 +20,23 @@ function mockNodes(nodes: Node[]) {
1920
}
2021

2122
describe('field-edge', () => {
22-
const nodes = [
23-
{ ...ORDERS_NODE, data: { title: ORDERS_NODE.title, fields: ORDERS_NODE.fields } },
24-
{ ...EMPLOYEES_NODE, data: { title: EMPLOYEES_NODE.title, fields: EMPLOYEES_NODE.fields } },
23+
const nodes: InternalNode[] = [
24+
{
25+
...ORDERS_NODE,
26+
data: {
27+
title: ORDERS_NODE.title,
28+
visibleFields: ORDERS_NODE.fields.map(field => ({ ...field, hasChildren: false })),
29+
externalNode: ORDERS_NODE,
30+
},
31+
},
32+
{
33+
...EMPLOYEES_NODE,
34+
data: {
35+
title: EMPLOYEES_NODE.title,
36+
visibleFields: EMPLOYEES_NODE.fields.map(field => ({ ...field, hasChildren: false })),
37+
externalNode: EMPLOYEES_NODE,
38+
},
39+
},
2540
];
2641

2742
beforeEach(() => {
@@ -44,13 +59,13 @@ describe('field-edge', () => {
4459
id={'orders-to-employees'}
4560
source={'orders'}
4661
target={'employees'}
47-
data={{ sourceFieldIndex: 0, targetFieldIndex: 1 }}
62+
data={{ sourceFieldId: ['ORDER_ID'], targetFieldId: ['employeeDetail'] }}
4863
{...props}
4964
/>,
5065
);
5166
};
5267

53-
describe('With the nodes positioned above to each other', () => {
68+
describe('With the nodes positioned next to each other', () => {
5469
it('Should render edge', () => {
5570
mockNodes([
5671
{

src/components/edge/field-edge.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useMemo } from 'react';
44
import { getFieldEdgeParams } from '@/utilities/get-edge-params';
55
import { InternalNode } from '@/types/internal';
66
import { Edge } from '@/components/edge/edge';
7+
import { FieldId } from '@/types/node';
78

89
export const FieldEdge = ({
910
id,
@@ -12,11 +13,11 @@ export const FieldEdge = ({
1213
markerEnd,
1314
markerStart,
1415
selected,
15-
data: { sourceFieldIndex, targetFieldIndex },
16+
data: { sourceFieldId, targetFieldId },
1617
}: EdgeProps & {
1718
data: {
18-
sourceFieldIndex: number;
19-
targetFieldIndex: number;
19+
sourceFieldId: FieldId;
20+
targetFieldId: FieldId;
2021
};
2122
}) => {
2223
const nodes = useNodes<InternalNode>();
@@ -32,8 +33,8 @@ export const FieldEdge = ({
3233
const { sx, sy, tx, ty, sourcePos, targetPos } = getFieldEdgeParams(
3334
sourceNode,
3435
targetNode,
35-
sourceFieldIndex,
36-
targetFieldIndex,
36+
sourceFieldId,
37+
targetFieldId,
3738
);
3839

3940
const [path] = getSmoothStepPath({

src/components/edge/floating-edge.test.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { EMPLOYEES_NODE, ORDERS_NODE } from '@/mocks/datasets/nodes';
55
import { render, screen } from '@/mocks/testing-utils';
66
import { FloatingEdge } from '@/components/edge/floating-edge';
77
import { DEFAULT_FIELD_HEIGHT, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
8+
import { InternalNode } from '@/types/internal';
89

910
vi.mock('@xyflow/react', async () => {
1011
const actual = await vi.importActual<typeof import('@xyflow/react')>('@xyflow/react');
@@ -20,9 +21,23 @@ function mockNodes(nodes: Node[]) {
2021
}
2122

2223
describe('floating-edge', () => {
23-
const nodes = [
24-
{ ...ORDERS_NODE, data: { title: ORDERS_NODE.title, fields: ORDERS_NODE.fields } },
25-
{ ...EMPLOYEES_NODE, data: { title: EMPLOYEES_NODE.title, fields: EMPLOYEES_NODE.fields } },
24+
const nodes: InternalNode[] = [
25+
{
26+
...ORDERS_NODE,
27+
data: {
28+
title: ORDERS_NODE.title,
29+
visibleFields: ORDERS_NODE.fields.map(field => ({ ...field, hasChildren: false })),
30+
externalNode: ORDERS_NODE,
31+
},
32+
},
33+
{
34+
...EMPLOYEES_NODE,
35+
data: {
36+
title: EMPLOYEES_NODE.title,
37+
visibleFields: EMPLOYEES_NODE.fields.map(field => ({ ...field, hasChildren: false })),
38+
externalNode: EMPLOYEES_NODE,
39+
},
40+
},
2641
];
2742

2843
beforeEach(() => {
@@ -57,7 +72,7 @@ describe('floating-edge', () => {
5772
expect(path).toHaveAttribute('id', 'orders-to-employees');
5873
expect(path).toHaveAttribute(
5974
'd',
60-
'M263 189.5L263 209.5L 263,236Q 263,241 268,241L 358,241Q 363,241 363,246L363 272.5L363 292.5',
75+
'M263 189.5L263 209.5L 263,236Q 263,241 268,241L 331,241Q 336,241 336,246L336 272.5L336 292.5',
6176
);
6277
});
6378
});

src/components/edge/self-referencing-edge.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { render, screen } from '@/mocks/testing-utils';
66
import { FloatingEdge } from '@/components/edge/floating-edge';
77
import { SelfReferencingEdge } from '@/components/edge/self-referencing-edge';
88
import { DEFAULT_FIELD_HEIGHT, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
9+
import { InternalNode } from '@/types/internal';
910

1011
vi.mock('@xyflow/react', async () => {
1112
const actual = await vi.importActual<typeof import('@xyflow/react')>('@xyflow/react');
@@ -21,7 +22,16 @@ function mockNodes(nodes: Node[]) {
2122
}
2223

2324
describe('self-referencing-edge', () => {
24-
const nodes = [{ ...EMPLOYEES_NODE, data: { title: EMPLOYEES_NODE.title, fields: EMPLOYEES_NODE.fields } }];
25+
const nodes: InternalNode[] = [
26+
{
27+
...EMPLOYEES_NODE,
28+
data: {
29+
title: EMPLOYEES_NODE.title,
30+
visibleFields: EMPLOYEES_NODE.fields.map(field => ({ ...field, hasChildren: false })),
31+
externalNode: EMPLOYEES_NODE,
32+
},
33+
},
34+
];
2535

2636
beforeEach(() => {
2737
mockNodes(nodes);
@@ -53,7 +63,7 @@ describe('self-referencing-edge', () => {
5363
renderComponent();
5464
const path = screen.getByTestId('self-referencing-edge-employees-to-employees');
5565
expect(path).toHaveAttribute('id', 'employees-to-employees');
56-
expect(path).toHaveAttribute('d', 'M422,292.5L422,262.5L584,262.5L584,351.5L551.5,351.5');
66+
expect(path).toHaveAttribute('d', 'M422,292.5L422,262.5L584,262.5L584,378.5L551.5,378.5');
5767
});
5868
});
5969

src/components/field/field-content.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import styled from '@emotion/styled';
22
import { fontWeights } from '@leafygreen-ui/tokens';
3+
import Icon from '@leafygreen-ui/icon';
4+
import { useTheme } from '@emotion/react';
35
import { useCallback, useEffect, useRef, useState } from 'react';
46

57
import { ellipsisTruncation } from '@/styles/styles';
68
import { FieldDepth } from '@/components/field/field-depth';
79
import { FieldType } from '@/components/field/field-type';
10+
import { DiagramIconButton } from '@/components/buttons/diagram-icon-button';
811
import { FieldId, NodeField } from '@/types';
912
import { useEditableDiagramInteractions } from '@/hooks/use-editable-diagram-interactions';
1013

@@ -26,14 +29,31 @@ interface FieldContentProps extends NodeField {
2629
id: FieldId;
2730
isEditable: boolean;
2831
isDisabled: boolean;
32+
isExpandable?: boolean;
2933
nodeId: string;
3034
}
3135

32-
export const FieldContent = ({ isEditable, isDisabled, depth = 0, name, type, id, nodeId }: FieldContentProps) => {
36+
export const FieldContent = ({
37+
isEditable,
38+
isDisabled,
39+
isExpandable,
40+
depth = 0,
41+
name,
42+
type,
43+
id,
44+
nodeId,
45+
expanded,
46+
}: FieldContentProps) => {
3347
const [isEditing, setIsEditing] = useState(false);
3448
const fieldContentRef = useRef<HTMLDivElement>(null);
49+
const theme = useTheme();
50+
51+
const { onChangeFieldName, onChangeFieldType, fieldTypes, onFieldExpandToggle } = useEditableDiagramInteractions();
52+
53+
const hasExpandFunctionality = !!onFieldExpandToggle;
54+
const hasExpandButton = hasExpandFunctionality && isExpandable;
55+
const placeholderCollapse = hasExpandFunctionality && !hasExpandButton;
3556

36-
const { onChangeFieldName, onChangeFieldType, fieldTypes } = useEditableDiagramInteractions();
3757
const handleNameChange = useCallback(
3858
(newName: string) => onChangeFieldName?.(nodeId, Array.isArray(id) ? id : [id], newName),
3959
[onChangeFieldName, id, nodeId],
@@ -47,6 +67,16 @@ export const FieldContent = ({ isEditable, isDisabled, depth = 0, name, type, id
4767
setIsEditing(true);
4868
}, []);
4969

70+
const handleFieldExpandToggle = useCallback(
71+
(event: React.MouseEvent<HTMLButtonElement>) => {
72+
if (!onFieldExpandToggle) return;
73+
// Don't click on the field element.
74+
event.stopPropagation();
75+
onFieldExpandToggle(event, nodeId, Array.isArray(id) ? id : [id], !expanded);
76+
},
77+
[onFieldExpandToggle, nodeId, id, expanded],
78+
);
79+
5080
useEffect(() => {
5181
// When clicking outside of the field content while editing, stop editing.
5282
const container = fieldContentRef.current;
@@ -98,7 +128,18 @@ export const FieldContent = ({ isEditable, isDisabled, depth = 0, name, type, id
98128
isEditing={isTypeEditable}
99129
isDisabled={isDisabled}
100130
onChange={handleTypeChange}
131+
placeholderCollapse={placeholderCollapse}
101132
/>
133+
{hasExpandButton && (
134+
<DiagramIconButton
135+
data-testid={`field-expand-toggle-${nodeId}-${typeof id === 'string' ? id : id.join('.')}`}
136+
onClick={handleFieldExpandToggle}
137+
aria-label={expanded ? 'Collapse Field' : 'Expand Field'}
138+
title={expanded ? 'Collapse Field' : 'Expand Field'}
139+
>
140+
<Icon glyph={expanded ? 'ChevronDown' : 'ChevronLeft'} color={theme.node.fieldIconButton} size={14} />
141+
</DiagramIconButton>
142+
)}
102143
</FieldContentWrapper>
103144
);
104145
};

0 commit comments

Comments
 (0)