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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// @ts-expect-error do not provide typings.
import JsonViewer from '@andypf/json-viewer/dist/esm/react/JsonViewer';
import { useCallback, useRef, type HTMLProps, type JSX, type PropsWithChildren } from 'react';
import { GraphProvider, useCellId, useNodeSize, type GraphLink } from '@joint/react';
import { GraphProvider, useCellId, useNodeSize, type FlatLinkData } from '@joint/react';
import { PAPER_CLASSNAME, PRIMARY } from '../theme';
import type { PartialStoryFn, StoryContext } from 'storybook/internal/types';
import { Paper } from '../../src/components/paper/paper';
Expand Down Expand Up @@ -48,7 +48,7 @@ export const testElements: Record<
};

export type SimpleElement = (typeof testElements)[string];
export const testLinks: Record<string, GraphLink> = {
export const testLinks: Record<string, FlatLinkData> = {
'l-1': {
source: '1',
target: '2',
Expand Down
40 changes: 24 additions & 16 deletions packages/joint-react/src/components/graph/graph-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { dia } from '@joint/core';
import type { GraphLink } from '../../types/link-types';
import { forwardRef, type Dispatch, type SetStateAction } from 'react';
import type { GraphElement } from '../../types/element-types';
import type { CellId } from '../../types/cell-id';
import type { FlatLinkData } from '../../types/link-types';
import React, { forwardRef, type Dispatch, type SetStateAction } from 'react';
import type { FlatElementData } from '../../types/element-types';
import { useImperativeApi } from '../../hooks/use-imperative-api';
import { GraphStoreContext } from '../../context';
import { GraphStore, type ExternalGraphStore } from '../../store';
Expand All @@ -15,8 +16,8 @@ import type { GraphStateSelectors } from '../../state/graph-state-selectors';
* @template Link - The type of links in the graph
*/
interface GraphProviderProps<
Element extends GraphElement = GraphElement,
Link extends GraphLink = GraphLink,
ElementData = FlatElementData,
LinkData = FlatLinkData,
> {
/**
* Elements (nodes) to be added to the graph as a Record keyed by cell ID.
Expand All @@ -27,7 +28,7 @@ interface GraphProviderProps<
* **Uncontrolled mode:** If `onElementsChange` is not provided, this is only used for initial elements.
* The graph manages its own state internally.
*/
readonly elements?: Record<string, Element>;
readonly elements?: Record<CellId, ElementData>;

/**
* Links (edges) to be added to the graph as a Record keyed by cell ID.
Expand All @@ -38,7 +39,7 @@ interface GraphProviderProps<
* **Uncontrolled mode:** If `onLinksChange` is not provided, this is only used for initial links.
* The graph manages its own state internally.
*/
readonly links?: Record<string, Link>;
readonly links?: Record<CellId, LinkData>;

/**
* Callback triggered when elements (nodes) change in the graph.
Expand All @@ -52,7 +53,7 @@ interface GraphProviderProps<
* - State persistence
* - Integration with other React state management
*/
readonly onElementsChange?: Dispatch<SetStateAction<Record<string, Element>>>;
readonly onElementsChange?: Dispatch<SetStateAction<Record<CellId, ElementData>>>;

/**
* Callback triggered when links (edges) change in the graph.
Expand All @@ -66,7 +67,7 @@ interface GraphProviderProps<
* - State persistence
* - Integration with other React state management
*/
readonly onLinksChange?: Dispatch<SetStateAction<Record<dia.Cell.ID, Link>>>;
readonly onLinksChange?: Dispatch<SetStateAction<Record<CellId, LinkData>>>;
}

/**
Expand All @@ -76,10 +77,10 @@ interface GraphProviderProps<
* @template Link - The type of links in the graph
*/
export interface GraphProps<
Element extends GraphElement = GraphElement,
Link extends GraphLink = GraphLink,
> extends GraphProviderProps<Element, Link>,
GraphStateSelectors<Element, Link> {
ElementData = FlatElementData,
LinkData = FlatLinkData,
> extends GraphProviderProps<ElementData, LinkData>,
GraphStateSelectors<ElementData, LinkData> {
/**
* Graph instance to use. If not provided, a new graph instance will be created.
*
Expand Down Expand Up @@ -287,8 +288,8 @@ const GraphBaseRouter = forwardRef<dia.Graph, GraphProps>(
*
* 2. **React-controlled mode:**
* ```tsx
* const [elements, setElements] = useState<Record<string, GraphElement>>({});
* const [links, setLinks] = useState<Record<string, GraphLink>>({});
* const [elements, setElements] = useState<Record<string, FlatElementData>>({});
* const [links, setLinks] = useState<Record<string, FlatLinkData>>({});
*
* <GraphProvider
* elements={elements}
Expand All @@ -310,4 +311,11 @@ const GraphBaseRouter = forwardRef<dia.Graph, GraphProps>(
* ```
* @see GraphProps for all available props
*/
export const GraphProvider = GraphBaseRouter;
export const GraphProvider = GraphBaseRouter as <
ElementData = FlatElementData,
LinkData = FlatLinkData,
>(
props: GraphProps<ElementData, LinkData> & {
ref?: React.Ref<dia.Graph | null>;
}
) => ReturnType<typeof GraphBaseRouter>;
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import React, { useState, useCallback } from 'react';
import { act, render, waitFor } from '@testing-library/react';
import { dia } from '@joint/core';
import { useElements, useLinks, useGraph } from '../../../hooks';
import type { GraphElement } from '../../../types/element-types';
import type { GraphLink } from '../../../types/link-types';
import type { FlatElementData } from '../../../types/element-types';
import type { FlatLinkData } from '../../../types/link-types';
import { GraphProvider } from '../../graph/graph-provider';

describe('GraphProvider Controlled Mode', () => {
describe('Basic useState integration', () => {
it('should sync React state to store and graph on initial mount', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
'2': { width: 200, height: 200 },
};
Expand All @@ -27,7 +27,7 @@ describe('GraphProvider Controlled Mode', () => {
}

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
return (
<GraphProvider elements={elements} onElementsChange={setElements}>
<TestComponent />
Expand All @@ -44,7 +44,7 @@ describe('GraphProvider Controlled Mode', () => {
});

it('should update store when React state changes via useState', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};

Expand All @@ -58,11 +58,11 @@ describe('GraphProvider Controlled Mode', () => {
return null;
}

let setElementsExternal: ((elements: Record<string, GraphElement>) => void) | null = null;
let setElementsExternal: ((elements: Record<string, FlatElementData>) => void) | null = null;

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, GraphElement>) => void;
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, FlatElementData>) => void;
return (
<GraphProvider elements={elements} onElementsChange={setElements}>
<TestComponent />
Expand Down Expand Up @@ -92,10 +92,10 @@ describe('GraphProvider Controlled Mode', () => {
});

it('should handle both elements and links in controlled mode', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};
const initialLink: GraphLink = {
const initialLink: FlatLinkData = {
type: 'standard.Link',
source: '1',
target: '2',
Expand All @@ -110,16 +110,16 @@ describe('GraphProvider Controlled Mode', () => {
return null;
}

let setElementsExternal: ((elements: Record<string, GraphElement>) => void) | null = null;
let setLinksExternal: ((links: Record<string, GraphLink>) => void) | null = null;
let setElementsExternal: ((elements: Record<string, FlatElementData>) => void) | null = null;
let setLinksExternal: ((links: Record<string, FlatLinkData>) => void) | null = null;

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
const [links, setLinks] = useState<Record<string, GraphLink>>(() => ({
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
const [links, setLinks] = useState<Record<string, FlatLinkData>>(() => ({
'link1': initialLink,
}));
setElementsExternal = setElements as (elements: Record<string, GraphElement>) => void;
setLinksExternal = setLinks as (links: Record<string, GraphLink>) => void;
setElementsExternal = setElements as (elements: Record<string, FlatElementData>) => void;
setLinksExternal = setLinks as (links: Record<string, FlatLinkData>) => void;
return (
<GraphProvider
elements={elements}
Expand Down Expand Up @@ -177,7 +177,7 @@ describe('GraphProvider Controlled Mode', () => {

describe('Rapid consecutive updates', () => {
it('should handle rapid consecutive state updates correctly', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};

Expand All @@ -189,11 +189,11 @@ describe('GraphProvider Controlled Mode', () => {
return null;
}

let setElementsExternal: ((elements: Record<string, GraphElement>) => void) | null = null;
let setElementsExternal: ((elements: Record<string, FlatElementData>) => void) | null = null;

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, GraphElement>) => void;
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, FlatElementData>) => void;
return (
<GraphProvider elements={elements} onElementsChange={setElements}>
<TestComponent />
Expand Down Expand Up @@ -235,7 +235,7 @@ describe('GraphProvider Controlled Mode', () => {
});

it('should handle 10 rapid updates without losing state', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};

Expand All @@ -246,11 +246,11 @@ describe('GraphProvider Controlled Mode', () => {
return null;
}

let setElementsExternal: ((elements: Record<string, GraphElement>) => void) | null = null;
let setElementsExternal: ((elements: Record<string, FlatElementData>) => void) | null = null;

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, GraphElement>) => void;
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, FlatElementData>) => void;
return (
<GraphProvider elements={elements} onElementsChange={setElements}>
<TestComponent />
Expand All @@ -267,7 +267,7 @@ describe('GraphProvider Controlled Mode', () => {
// 10 rapid updates
act(() => {
for (let index = 2; index <= 11; index++) {
const newElements: Record<string, GraphElement> = {};
const newElements: Record<string, FlatElementData> = {};
for (let elementIndex = 1; elementIndex <= index; elementIndex++) {
newElements[String(elementIndex)] = {
width: 100 * elementIndex,
Expand All @@ -289,10 +289,10 @@ describe('GraphProvider Controlled Mode', () => {

describe('Concurrent updates', () => {
it('should handle concurrent element and link updates', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};
const initialLink: GraphLink = {
const initialLink: FlatLinkData = {
type: 'standard.Link',
source: '1',
target: '2',
Expand All @@ -307,16 +307,16 @@ describe('GraphProvider Controlled Mode', () => {
return null;
}

let setElementsExternal: ((elements: Record<string, GraphElement>) => void) | null = null;
let setLinksExternal: ((links: Record<string, GraphLink>) => void) | null = null;
let setElementsExternal: ((elements: Record<string, FlatElementData>) => void) | null = null;
let setLinksExternal: ((links: Record<string, FlatLinkData>) => void) | null = null;

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
const [links, setLinks] = useState<Record<string, GraphLink>>(() => ({
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
const [links, setLinks] = useState<Record<string, FlatLinkData>>(() => ({
'link1': initialLink,
}));
setElementsExternal = setElements as (elements: Record<string, GraphElement>) => void;
setLinksExternal = setLinks as (links: Record<string, GraphLink>) => void;
setElementsExternal = setElements as (elements: Record<string, FlatElementData>) => void;
setLinksExternal = setLinks as (links: Record<string, FlatLinkData>) => void;
return (
<GraphProvider
elements={elements}
Expand Down Expand Up @@ -366,7 +366,7 @@ describe('GraphProvider Controlled Mode', () => {
});

it('should handle multiple rapid updates with callbacks', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};

Expand All @@ -379,7 +379,7 @@ describe('GraphProvider Controlled Mode', () => {
}

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);

const handleAddElement = useCallback(() => {
setElements((previous) => {
Expand Down Expand Up @@ -429,20 +429,20 @@ describe('GraphProvider Controlled Mode', () => {

describe('User interaction sync back to React state', () => {
it('should sync graph changes back to React state in controlled mode', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};

let reactStateElements: Record<string, GraphElement> = {};
let storeElements: Record<string, GraphElement> = {};
let reactStateElements: Record<string, FlatElementData> = {};
let storeElements: Record<string, FlatElementData> = {};

function TestComponent() {
storeElements = useElements((items) => items);
return null;
}

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
reactStateElements = elements;

return (
Expand Down Expand Up @@ -500,14 +500,14 @@ describe('GraphProvider Controlled Mode', () => {
});

it('should handle element position changes from user interaction', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100, x: 0, y: 0 },
};

let reactStateElements: Record<string, GraphElement> = {};
let reactStateElements: Record<string, FlatElementData> = {};

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
reactStateElements = elements;

return (
Expand Down Expand Up @@ -562,7 +562,7 @@ describe('GraphProvider Controlled Mode', () => {

describe('Edge cases', () => {
it('should handle empty records correctly', async () => {
const initialElements: Record<string, GraphElement> = {
const initialElements: Record<string, FlatElementData> = {
'1': { width: 100, height: 100 },
};

Expand All @@ -573,11 +573,11 @@ describe('GraphProvider Controlled Mode', () => {
return null;
}

let setElementsExternal: ((elements: Record<string, GraphElement>) => void) | null = null;
let setElementsExternal: ((elements: Record<string, FlatElementData>) => void) | null = null;

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, GraphElement>) => void;
const [elements, setElements] = useState<Record<string, FlatElementData>>(() => initialElements);
setElementsExternal = setElements as (elements: Record<string, FlatElementData>) => void;
return (
<GraphProvider elements={elements} onElementsChange={setElements}>
<TestComponent />
Expand Down Expand Up @@ -624,8 +624,8 @@ describe('GraphProvider Controlled Mode', () => {
}

function ControlledGraph() {
const [elements, setElements] = useState<Record<string, GraphElement>>({});
const [links, setLinks] = useState<Record<string, GraphLink>>({});
const [elements, setElements] = useState<Record<string, FlatElementData>>({});
const [links, setLinks] = useState<Record<string, FlatLinkData>>({});
return (
<GraphProvider
elements={elements}
Expand Down
Loading