Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/components/canvas/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { SelfReferencingEdge } from '@/components/edge/self-referencing-edge';
import { FieldEdge } from '@/components/edge/field-edge';
import { MarkerList } from '@/components/markers/marker-list';
import { ConnectionLine } from '@/components/line/connection-line';
import { convertToExternalNode, convertToExternalNodes, convertToInternalNodes } from '@/utilities/convert-nodes';
import { getExternalNode, convertToInternalNodes } from '@/utilities/convert-nodes';
import { convertToExternalEdge, convertToExternalEdges, convertToInternalEdges } from '@/utilities/convert-edges';
import { EditableDiagramInteractionsProvider } from '@/hooks/use-editable-diagram-interactions';

Expand Down Expand Up @@ -66,6 +66,7 @@ export const Canvas = ({
onFieldNameChange,
onFieldTypeChange,
onFieldClick,
onFieldExpandToggle,
onNodeContextMenu,
onNodeDrag,
onNodeDragStop,
Expand Down Expand Up @@ -94,28 +95,28 @@ export const Canvas = ({

const _onNodeContextMenu = useCallback(
(event: MouseEvent, node: InternalNode) => {
onNodeContextMenu?.(event, convertToExternalNode(node));
onNodeContextMenu?.(event, getExternalNode(node));
},
[onNodeContextMenu],
);

const _onNodeDrag = useCallback(
(event: MouseEvent, node: InternalNode, nodes: InternalNode[]) => {
onNodeDrag?.(event, convertToExternalNode(node), convertToExternalNodes(nodes));
onNodeDrag?.(event, getExternalNode(node), nodes.map(getExternalNode));
},
[onNodeDrag],
);

const _onNodeDragStop = useCallback(
(event: MouseEvent, node: InternalNode, nodes: InternalNode[]) => {
onNodeDragStop?.(event, convertToExternalNode(node), convertToExternalNodes(nodes));
onNodeDragStop?.(event, getExternalNode(node), nodes.map(getExternalNode));
},
[onNodeDragStop],
);

const _onSelectionDragStop = useCallback(
(event: MouseEvent, nodes: InternalNode[]) => {
onSelectionDragStop?.(event, convertToExternalNodes(nodes));
onSelectionDragStop?.(event, nodes.map(getExternalNode));
},
[onSelectionDragStop],
);
Expand All @@ -129,21 +130,21 @@ export const Canvas = ({

const _onNodeClick = useCallback(
(event: MouseEvent, node: InternalNode) => {
onNodeClick?.(event, convertToExternalNode(node));
onNodeClick?.(event, getExternalNode(node));
},
[onNodeClick],
);

const _onSelectionContextMenu = useCallback(
(event: MouseEvent, nodes: InternalNode[]) => {
onSelectionContextMenu?.(event, convertToExternalNodes(nodes));
onSelectionContextMenu?.(event, nodes.map(getExternalNode));
},
[onSelectionContextMenu],
);

const _onSelectionChange = useCallback(
({ nodes, edges }: { nodes: InternalNode[]; edges: InternalEdge[] }) => {
onSelectionChange?.({ nodes: convertToExternalNodes(nodes), edges: convertToExternalEdges(edges) });
onSelectionChange?.({ nodes: nodes.map(getExternalNode), edges: convertToExternalEdges(edges) });
},
[onSelectionChange],
);
Expand All @@ -153,6 +154,7 @@ export const Canvas = ({
onFieldClick={onFieldClick}
onAddFieldToNodeClick={onAddFieldToNodeClick}
onNodeExpandToggle={onNodeExpandToggle}
onFieldExpandToggle={onFieldExpandToggle}
onAddFieldToObjectFieldClick={onAddFieldToObjectFieldClick}
onFieldNameChange={onFieldNameChange}
onFieldTypeChange={onFieldTypeChange}
Expand Down
16 changes: 13 additions & 3 deletions src/components/diagram.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ export const DiagramWithFieldToFieldEdges: Story = {
title: 'MongoDB Diagram',
isDarkMode: true,
edges: [
{ ...EMPLOYEES_TO_ORDERS_EDGE, sourceFieldIndex: 0, targetFieldIndex: 1 },
{ ...EMPLOYEES_TO_EMPLOYEE_TERRITORIES_EDGE, sourceFieldIndex: 0, targetFieldIndex: 1 },
{ ...EMPLOYEES_TO_ORDERS_EDGE, sourceFieldId: ['address', 'city'], targetFieldId: ['SUPPLIER_ID'] },
{
...EMPLOYEES_TO_EMPLOYEE_TERRITORIES_EDGE,
sourceFieldId: ['employeeId'],
targetFieldId: ['employeeId'],
},
],
nodes: [
{ ...EMPLOYEE_TERRITORIES_NODE, position: { x: 100, y: 100 } },
Expand Down Expand Up @@ -76,7 +80,13 @@ export const DiagramWithEditInteractions: Story = {
args: {
title: 'MongoDB Diagram',
isDarkMode: true,
edges: [],
edges: [
{
...EMPLOYEES_TO_ORDERS_EDGE,
sourceFieldId: ['employeeId'],
targetFieldId: ['SUPPLIER_ID'],
},
],
nodes: [
{
...ORDERS_NODE,
Expand Down
25 changes: 20 additions & 5 deletions src/components/edge/field-edge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ComponentProps } from 'react';
import { EMPLOYEES_NODE, ORDERS_NODE } from '@/mocks/datasets/nodes';
import { render, screen } from '@/mocks/testing-utils';
import { FieldEdge } from '@/components/edge/field-edge';
import { InternalNode } from '@/types/internal';

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

describe('field-edge', () => {
const nodes = [
{ ...ORDERS_NODE, data: { title: ORDERS_NODE.title, fields: ORDERS_NODE.fields } },
{ ...EMPLOYEES_NODE, data: { title: EMPLOYEES_NODE.title, fields: EMPLOYEES_NODE.fields } },
const nodes: InternalNode[] = [
{
...ORDERS_NODE,
data: {
title: ORDERS_NODE.title,
visibleFields: ORDERS_NODE.fields.map(field => ({ ...field, hasChildren: false })),
externalNode: ORDERS_NODE,
},
},
{
...EMPLOYEES_NODE,
data: {
title: EMPLOYEES_NODE.title,
visibleFields: EMPLOYEES_NODE.fields.map(field => ({ ...field, hasChildren: false })),
externalNode: EMPLOYEES_NODE,
},
},
];

beforeEach(() => {
Expand All @@ -44,13 +59,13 @@ describe('field-edge', () => {
id={'orders-to-employees'}
source={'orders'}
target={'employees'}
data={{ sourceFieldIndex: 0, targetFieldIndex: 1 }}
data={{ sourceFieldId: ['ORDER_ID'], targetFieldId: ['employeeDetail'] }}
{...props}
/>,
);
};

describe('With the nodes positioned above to each other', () => {
describe('With the nodes positioned next to each other', () => {
it('Should render edge', () => {
mockNodes([
{
Expand Down
11 changes: 6 additions & 5 deletions src/components/edge/field-edge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useMemo } from 'react';
import { getFieldEdgeParams } from '@/utilities/get-edge-params';
import { InternalNode } from '@/types/internal';
import { Edge } from '@/components/edge/edge';
import { FieldId } from '@/types/node';

export const FieldEdge = ({
id,
Expand All @@ -12,11 +13,11 @@ export const FieldEdge = ({
markerEnd,
markerStart,
selected,
data: { sourceFieldIndex, targetFieldIndex },
data: { sourceFieldId, targetFieldId },
}: EdgeProps & {
data: {
sourceFieldIndex: number;
targetFieldIndex: number;
sourceFieldId: FieldId;
targetFieldId: FieldId;
};
}) => {
const nodes = useNodes<InternalNode>();
Expand All @@ -32,8 +33,8 @@ export const FieldEdge = ({
const { sx, sy, tx, ty, sourcePos, targetPos } = getFieldEdgeParams(
sourceNode,
targetNode,
sourceFieldIndex,
targetFieldIndex,
sourceFieldId,
targetFieldId,
);

const [path] = getSmoothStepPath({
Expand Down
23 changes: 19 additions & 4 deletions src/components/edge/floating-edge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EMPLOYEES_NODE, ORDERS_NODE } from '@/mocks/datasets/nodes';
import { render, screen } from '@/mocks/testing-utils';
import { FloatingEdge } from '@/components/edge/floating-edge';
import { DEFAULT_FIELD_HEIGHT, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
import { InternalNode } from '@/types/internal';

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

describe('floating-edge', () => {
const nodes = [
{ ...ORDERS_NODE, data: { title: ORDERS_NODE.title, fields: ORDERS_NODE.fields } },
{ ...EMPLOYEES_NODE, data: { title: EMPLOYEES_NODE.title, fields: EMPLOYEES_NODE.fields } },
const nodes: InternalNode[] = [
{
...ORDERS_NODE,
data: {
title: ORDERS_NODE.title,
visibleFields: ORDERS_NODE.fields.map(field => ({ ...field, hasChildren: false })),
externalNode: ORDERS_NODE,
},
},
{
...EMPLOYEES_NODE,
data: {
title: EMPLOYEES_NODE.title,
visibleFields: EMPLOYEES_NODE.fields.map(field => ({ ...field, hasChildren: false })),
externalNode: EMPLOYEES_NODE,
},
},
];

beforeEach(() => {
Expand Down Expand Up @@ -57,7 +72,7 @@ describe('floating-edge', () => {
expect(path).toHaveAttribute('id', 'orders-to-employees');
expect(path).toHaveAttribute(
'd',
'M263 189.5L263 209.5L 263,236Q 263,241 268,241L 358,241Q 363,241 363,246L363 272.5L363 292.5',
'M263 189.5L263 209.5L 263,236Q 263,241 268,241L 331,241Q 336,241 336,246L336 272.5L336 292.5',
);
});
});
Expand Down
14 changes: 12 additions & 2 deletions src/components/edge/self-referencing-edge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { render, screen } from '@/mocks/testing-utils';
import { FloatingEdge } from '@/components/edge/floating-edge';
import { SelfReferencingEdge } from '@/components/edge/self-referencing-edge';
import { DEFAULT_FIELD_HEIGHT, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
import { InternalNode } from '@/types/internal';

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

describe('self-referencing-edge', () => {
const nodes = [{ ...EMPLOYEES_NODE, data: { title: EMPLOYEES_NODE.title, fields: EMPLOYEES_NODE.fields } }];
const nodes: InternalNode[] = [
{
...EMPLOYEES_NODE,
data: {
title: EMPLOYEES_NODE.title,
visibleFields: EMPLOYEES_NODE.fields.map(field => ({ ...field, hasChildren: false })),
externalNode: EMPLOYEES_NODE,
},
},
];

beforeEach(() => {
mockNodes(nodes);
Expand Down Expand Up @@ -53,7 +63,7 @@ describe('self-referencing-edge', () => {
renderComponent();
const path = screen.getByTestId('self-referencing-edge-employees-to-employees');
expect(path).toHaveAttribute('id', 'employees-to-employees');
expect(path).toHaveAttribute('d', 'M422,292.5L422,262.5L584,262.5L584,351.5L551.5,351.5');
expect(path).toHaveAttribute('d', 'M422,292.5L422,262.5L584,262.5L584,378.5L551.5,378.5');
});
});

Expand Down
45 changes: 43 additions & 2 deletions src/components/field/field-content.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import styled from '@emotion/styled';
import { fontWeights } from '@leafygreen-ui/tokens';
import Icon from '@leafygreen-ui/icon';
import { useTheme } from '@emotion/react';
import { useCallback, useEffect, useRef, useState } from 'react';

import { ellipsisTruncation } from '@/styles/styles';
import { FieldDepth } from '@/components/field/field-depth';
import { FieldType } from '@/components/field/field-type';
import { DiagramIconButton } from '@/components/buttons/diagram-icon-button';
import { FieldId, NodeField } from '@/types';
import { useEditableDiagramInteractions } from '@/hooks/use-editable-diagram-interactions';

Expand All @@ -26,14 +29,31 @@ interface FieldContentProps extends NodeField {
id: FieldId;
isEditable: boolean;
isDisabled: boolean;
isExpandable?: boolean;
nodeId: string;
}

export const FieldContent = ({ isEditable, isDisabled, depth = 0, name, type, id, nodeId }: FieldContentProps) => {
export const FieldContent = ({
isEditable,
isDisabled,
isExpandable,
depth = 0,
name,
type,
id,
nodeId,
expanded,
}: FieldContentProps) => {
const [isEditing, setIsEditing] = useState(false);
const fieldContentRef = useRef<HTMLDivElement>(null);
const theme = useTheme();

const { onChangeFieldName, onChangeFieldType, fieldTypes, onFieldExpandToggle } = useEditableDiagramInteractions();

const hasExpandFunctionality = !!onFieldExpandToggle;
const hasExpandButton = hasExpandFunctionality && isExpandable;
const placeholderCollapse = hasExpandFunctionality && !hasExpandButton;

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

const handleFieldExpandToggle = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (!onFieldExpandToggle) return;
// Don't click on the field element.
event.stopPropagation();
onFieldExpandToggle(event, nodeId, Array.isArray(id) ? id : [id], !expanded);
},
[onFieldExpandToggle, nodeId, id, expanded],
);

useEffect(() => {
// When clicking outside of the field content while editing, stop editing.
const container = fieldContentRef.current;
Expand Down Expand Up @@ -98,7 +128,18 @@ export const FieldContent = ({ isEditable, isDisabled, depth = 0, name, type, id
isEditing={isTypeEditable}
isDisabled={isDisabled}
onChange={handleTypeChange}
placeholderCollapse={placeholderCollapse}
/>
{hasExpandButton && (
<DiagramIconButton
data-testid={`field-expand-toggle-${nodeId}-${typeof id === 'string' ? id : id.join('.')}`}
onClick={handleFieldExpandToggle}
aria-label={expanded ? 'Collapse Field' : 'Expand Field'}
title={expanded ? 'Collapse Field' : 'Expand Field'}
>
<Icon glyph={expanded ? 'ChevronDown' : 'ChevronLeft'} color={theme.node.fieldIconButton} size={14} />
</DiagramIconButton>
)}
</FieldContentWrapper>
);
};
Loading
Loading