diff --git a/packages/joint-react/.storybook/decorators/with-simple-data.tsx b/packages/joint-react/.storybook/decorators/with-simple-data.tsx index ec5c17f061..1d26c90c24 100644 --- a/packages/joint-react/.storybook/decorators/with-simple-data.tsx +++ b/packages/joint-react/.storybook/decorators/with-simple-data.tsx @@ -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'; @@ -48,7 +48,7 @@ export const testElements: Record< }; export type SimpleElement = (typeof testElements)[string]; -export const testLinks: Record = { +export const testLinks: Record = { 'l-1': { source: '1', target: '2', diff --git a/packages/joint-react/src/components/graph/graph-provider.tsx b/packages/joint-react/src/components/graph/graph-provider.tsx index 6de5cc18ea..6d231abc99 100644 --- a/packages/joint-react/src/components/graph/graph-provider.tsx +++ b/packages/joint-react/src/components/graph/graph-provider.tsx @@ -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'; @@ -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. @@ -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; + readonly elements?: Record; /** * Links (edges) to be added to the graph as a Record keyed by cell ID. @@ -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; + readonly links?: Record; /** * Callback triggered when elements (nodes) change in the graph. @@ -52,7 +53,7 @@ interface GraphProviderProps< * - State persistence * - Integration with other React state management */ - readonly onElementsChange?: Dispatch>>; + readonly onElementsChange?: Dispatch>>; /** * Callback triggered when links (edges) change in the graph. @@ -66,7 +67,7 @@ interface GraphProviderProps< * - State persistence * - Integration with other React state management */ - readonly onLinksChange?: Dispatch>>; + readonly onLinksChange?: Dispatch>>; } /** @@ -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, - GraphStateSelectors { + ElementData = FlatElementData, + LinkData = FlatLinkData, +> extends GraphProviderProps, + GraphStateSelectors { /** * Graph instance to use. If not provided, a new graph instance will be created. * @@ -287,8 +288,8 @@ const GraphBaseRouter = forwardRef( * * 2. **React-controlled mode:** * ```tsx - * const [elements, setElements] = useState>({}); - * const [links, setLinks] = useState>({}); + * const [elements, setElements] = useState>({}); + * const [links, setLinks] = useState>({}); * * ( * ``` * @see GraphProps for all available props */ -export const GraphProvider = GraphBaseRouter; +export const GraphProvider = GraphBaseRouter as < + ElementData = FlatElementData, + LinkData = FlatLinkData, +>( + props: GraphProps & { + ref?: React.Ref; + } +) => ReturnType; diff --git a/packages/joint-react/src/components/paper/__tests__/graph-provider-controlled-mode.test.tsx b/packages/joint-react/src/components/paper/__tests__/graph-provider-controlled-mode.test.tsx index cb341435bd..52dd13c0d9 100644 --- a/packages/joint-react/src/components/paper/__tests__/graph-provider-controlled-mode.test.tsx +++ b/packages/joint-react/src/components/paper/__tests__/graph-provider-controlled-mode.test.tsx @@ -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 = { + const initialElements: Record = { '1': { width: 100, height: 100 }, '2': { width: 200, height: 200 }, }; @@ -27,7 +27,7 @@ describe('GraphProvider Controlled Mode', () => { } function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); + const [elements, setElements] = useState>(() => initialElements); return ( @@ -44,7 +44,7 @@ describe('GraphProvider Controlled Mode', () => { }); it('should update store when React state changes via useState', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; @@ -58,11 +58,11 @@ describe('GraphProvider Controlled Mode', () => { return null; } - let setElementsExternal: ((elements: Record) => void) | null = null; + let setElementsExternal: ((elements: Record) => void) | null = null; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); - setElementsExternal = setElements as (elements: Record) => void; + const [elements, setElements] = useState>(() => initialElements); + setElementsExternal = setElements as (elements: Record) => void; return ( @@ -92,10 +92,10 @@ describe('GraphProvider Controlled Mode', () => { }); it('should handle both elements and links in controlled mode', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; - const initialLink: GraphLink = { + const initialLink: FlatLinkData = { type: 'standard.Link', source: '1', target: '2', @@ -110,16 +110,16 @@ describe('GraphProvider Controlled Mode', () => { return null; } - let setElementsExternal: ((elements: Record) => void) | null = null; - let setLinksExternal: ((links: Record) => void) | null = null; + let setElementsExternal: ((elements: Record) => void) | null = null; + let setLinksExternal: ((links: Record) => void) | null = null; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); - const [links, setLinks] = useState>(() => ({ + const [elements, setElements] = useState>(() => initialElements); + const [links, setLinks] = useState>(() => ({ 'link1': initialLink, })); - setElementsExternal = setElements as (elements: Record) => void; - setLinksExternal = setLinks as (links: Record) => void; + setElementsExternal = setElements as (elements: Record) => void; + setLinksExternal = setLinks as (links: Record) => void; return ( { describe('Rapid consecutive updates', () => { it('should handle rapid consecutive state updates correctly', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; @@ -189,11 +189,11 @@ describe('GraphProvider Controlled Mode', () => { return null; } - let setElementsExternal: ((elements: Record) => void) | null = null; + let setElementsExternal: ((elements: Record) => void) | null = null; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); - setElementsExternal = setElements as (elements: Record) => void; + const [elements, setElements] = useState>(() => initialElements); + setElementsExternal = setElements as (elements: Record) => void; return ( @@ -235,7 +235,7 @@ describe('GraphProvider Controlled Mode', () => { }); it('should handle 10 rapid updates without losing state', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; @@ -246,11 +246,11 @@ describe('GraphProvider Controlled Mode', () => { return null; } - let setElementsExternal: ((elements: Record) => void) | null = null; + let setElementsExternal: ((elements: Record) => void) | null = null; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); - setElementsExternal = setElements as (elements: Record) => void; + const [elements, setElements] = useState>(() => initialElements); + setElementsExternal = setElements as (elements: Record) => void; return ( @@ -267,7 +267,7 @@ describe('GraphProvider Controlled Mode', () => { // 10 rapid updates act(() => { for (let index = 2; index <= 11; index++) { - const newElements: Record = {}; + const newElements: Record = {}; for (let elementIndex = 1; elementIndex <= index; elementIndex++) { newElements[String(elementIndex)] = { width: 100 * elementIndex, @@ -289,10 +289,10 @@ describe('GraphProvider Controlled Mode', () => { describe('Concurrent updates', () => { it('should handle concurrent element and link updates', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; - const initialLink: GraphLink = { + const initialLink: FlatLinkData = { type: 'standard.Link', source: '1', target: '2', @@ -307,16 +307,16 @@ describe('GraphProvider Controlled Mode', () => { return null; } - let setElementsExternal: ((elements: Record) => void) | null = null; - let setLinksExternal: ((links: Record) => void) | null = null; + let setElementsExternal: ((elements: Record) => void) | null = null; + let setLinksExternal: ((links: Record) => void) | null = null; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); - const [links, setLinks] = useState>(() => ({ + const [elements, setElements] = useState>(() => initialElements); + const [links, setLinks] = useState>(() => ({ 'link1': initialLink, })); - setElementsExternal = setElements as (elements: Record) => void; - setLinksExternal = setLinks as (links: Record) => void; + setElementsExternal = setElements as (elements: Record) => void; + setLinksExternal = setLinks as (links: Record) => void; return ( { }); it('should handle multiple rapid updates with callbacks', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; @@ -379,7 +379,7 @@ describe('GraphProvider Controlled Mode', () => { } function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); + const [elements, setElements] = useState>(() => initialElements); const handleAddElement = useCallback(() => { setElements((previous) => { @@ -429,12 +429,12 @@ 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 = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; - let reactStateElements: Record = {}; - let storeElements: Record = {}; + let reactStateElements: Record = {}; + let storeElements: Record = {}; function TestComponent() { storeElements = useElements((items) => items); @@ -442,7 +442,7 @@ describe('GraphProvider Controlled Mode', () => { } function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); + const [elements, setElements] = useState>(() => initialElements); reactStateElements = elements; return ( @@ -500,14 +500,14 @@ describe('GraphProvider Controlled Mode', () => { }); it('should handle element position changes from user interaction', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, x: 0, y: 0 }, }; - let reactStateElements: Record = {}; + let reactStateElements: Record = {}; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); + const [elements, setElements] = useState>(() => initialElements); reactStateElements = elements; return ( @@ -562,7 +562,7 @@ describe('GraphProvider Controlled Mode', () => { describe('Edge cases', () => { it('should handle empty records correctly', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100 }, }; @@ -573,11 +573,11 @@ describe('GraphProvider Controlled Mode', () => { return null; } - let setElementsExternal: ((elements: Record) => void) | null = null; + let setElementsExternal: ((elements: Record) => void) | null = null; function ControlledGraph() { - const [elements, setElements] = useState>(() => initialElements); - setElementsExternal = setElements as (elements: Record) => void; + const [elements, setElements] = useState>(() => initialElements); + setElementsExternal = setElements as (elements: Record) => void; return ( @@ -624,8 +624,8 @@ describe('GraphProvider Controlled Mode', () => { } function ControlledGraph() { - const [elements, setElements] = useState>({}); - const [links, setLinks] = useState>({}); + const [elements, setElements] = useState>({}); + const [links, setLinks] = useState>({}); return ( { } function ControlledGraph() { - const [elements, setElements] = useState>({}); + const [elements, setElements] = useState>({}); return ( @@ -44,7 +44,7 @@ describe('GraphProvider Coverage Tests', () => { } function ControlledGraph() { - const [links, setLinks] = useState>({}); + const [links, setLinks] = useState>({}); return ( @@ -60,7 +60,7 @@ describe('GraphProvider Coverage Tests', () => { }); it('should handle only elements controlled (not links)', async () => { - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, }; @@ -74,7 +74,7 @@ describe('GraphProvider Coverage Tests', () => { } function ControlledGraph() { - const [elements, setElements] = useState>(initialElements); + const [elements, setElements] = useState>(initialElements); return ( @@ -91,7 +91,7 @@ describe('GraphProvider Coverage Tests', () => { }); it('should handle only links controlled (not elements)', async () => { - const initialLink: GraphLink = { + const initialLink: FlatLinkData = { type: 'standard.Link', source: '1', target: '2', @@ -107,7 +107,7 @@ describe('GraphProvider Coverage Tests', () => { } function ControlledGraph() { - const [links, setLinks] = useState>(() => ({ + const [links, setLinks] = useState>(() => ({ 'link1': initialLink, })); return ( @@ -137,7 +137,7 @@ describe('GraphProvider Coverage Tests', () => { describe('GraphProvider edge cases', () => { it('should handle unmeasured elements (width/height <= 1)', async () => { - const unmeasuredElements: Record = { + const unmeasuredElements: Record = { '1': { width: 0, height: 0, type: 'ReactElement' }, '2': { width: 1, height: 1, type: 'ReactElement' }, }; @@ -150,7 +150,7 @@ describe('GraphProvider Coverage Tests', () => { } function ControlledGraph() { - const [elements, setElements] = useState>(unmeasuredElements); + const [elements, setElements] = useState>(unmeasuredElements); return ( diff --git a/packages/joint-react/src/components/paper/__tests__/graph-provider.test.tsx b/packages/joint-react/src/components/paper/__tests__/graph-provider.test.tsx index 5d500805db..4ffd03a85b 100644 --- a/packages/joint-react/src/components/paper/__tests__/graph-provider.test.tsx +++ b/packages/joint-react/src/components/paper/__tests__/graph-provider.test.tsx @@ -5,8 +5,8 @@ import { GraphStoreContext } from '../../../context'; import { GraphStore, DEFAULT_CELL_NAMESPACE } from '../../../store'; import { dia, shapes } from '@joint/core'; import { useElements, useLinks } 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'; import { Paper } from '../../paper/paper'; import type { RenderLink } from '../../paper/paper.types'; @@ -40,13 +40,13 @@ describe('graph', () => { }); it('should render graph provider with links and elements', async () => { - const elements: Record = { + const elements: Record = { 'element1': { width: 100, height: 100, }, }; - const link: GraphLink = { type: 'standard.Link', source: 'element1', target: { x: 0, y: 0 } }; + const link: FlatLinkData = { type: 'standard.Link', source: 'element1', target: { x: 0, y: 0 } }; let linkCount = 0; let elementCount = 0; function TestComponent() { @@ -109,7 +109,7 @@ describe('graph', () => { }); it('should initialize with default elements', async () => { - const elements: Record = { + const elements: Record = { 'element1': { width: 100, height: 100 }, 'element2': { width: 200, height: 200 }, }; @@ -149,7 +149,7 @@ describe('graph', () => { const graph = new dia.Graph({}, { cellNamespace: shapes }); const cell = new dia.Element({ id: 'element1', type: 'standard.Rectangle' }); graph.addCell(cell); - let currentElements: Record = {}; + let currentElements: Record = {}; function Elements() { const elements = useElements(); currentElements = elements; @@ -192,7 +192,7 @@ describe('graph', () => { const store = new GraphStore({ graph }); const cell = new dia.Element({ id: 'element1', type: 'standard.Rectangle' }); graph.addCell(cell); - let currentElements: Record = {}; + let currentElements: Record = {}; // eslint-disable-next-line sonarjs/no-identical-functions function Elements() { const elements = useElements(); @@ -233,14 +233,14 @@ describe('graph', () => { }); it('should render graph provider with links and elements - with explicit react type', async () => { - const elements: Record = { + const elements: Record = { 'element1': { width: 100, height: 100, type: 'ReactElement', }, }; - const link: GraphLink = { type: 'standard.Link', source: 'element1', target: { x: 0, y: 0 } }; + const link: FlatLinkData = { type: 'standard.Link', source: 'element1', target: { x: 0, y: 0 } }; let linkCount = 0; let elementCount = 0; // eslint-disable-next-line sonarjs/no-identical-functions @@ -264,14 +264,14 @@ describe('graph', () => { }); it('should update graph in controlled mode', async () => { - const initialElements: Record = { + const initialElements: Record = { 'element1': { width: 100, height: 100, type: 'ReactElement', }, }; - const initialLink: GraphLink = { + const initialLink: FlatLinkData = { type: 'standard.Link', source: 'element1', target: { x: 0, y: 0 }, @@ -288,16 +288,16 @@ describe('graph', () => { return null; } - let setElementsOutside: ((elements: Record) => void) | null = null; - let setLinksOutside: ((links: Record) => void) | null = null; + let setElementsOutside: ((elements: Record) => void) | null = null; + let setLinksOutside: ((links: Record) => void) | null = null; function Graph() { - const [elements, setElements] = useState>(() => initialElements); - const [links, setLinks] = useState>(() => ({ + const [elements, setElements] = useState>(() => initialElements); + const [links, setLinks] = useState>(() => ({ 'link1': initialLink, })); - setElementsOutside = setElements as unknown as (elements: Record) => void; - setLinksOutside = setLinks as unknown as (links: Record) => void; + setElementsOutside = setElements as unknown as (elements: Record) => void; + setLinksOutside = setLinks as unknown as (links: Record) => void; return ( { }); it('should pass correct link data to renderLink function', async () => { - const elements: Record = { + const elements: Record = { 'element-1': { x: 0, y: 0, @@ -382,7 +382,7 @@ describe('graph', () => { }, }; - const links: Record = { + const links: Record = { 'link-1': { source: 'element-1', target: 'element-2', @@ -398,7 +398,7 @@ describe('graph', () => { }, }; - const receivedLinks: GraphLink[] = []; + const receivedLinks: FlatLinkData[] = []; function TestComponent() { const renderLink: RenderLink = useCallback((link) => { @@ -441,7 +441,7 @@ describe('graph', () => { }); it('should pass updated link data to renderLink when links change', async () => { - const elements: Record = { + const elements: Record = { 'element-1': { x: 0, y: 0, @@ -456,7 +456,7 @@ describe('graph', () => { }, }; - const initialLinks: Record = { + const initialLinks: Record = { 'link-1': { source: 'element-1', target: 'element-2', @@ -464,13 +464,13 @@ describe('graph', () => { }, }; - const receivedLinks: GraphLink[] = []; + const receivedLinks: FlatLinkData[] = []; - let setLinksExternal: ((links: Record) => void) | null = null; + let setLinksExternal: ((links: Record) => void) | null = null; function ControlledGraph() { - const [links, setLinks] = useState>(() => initialLinks); - setLinksExternal = setLinks as unknown as (links: Record) => void; + const [links, setLinks] = useState>(() => initialLinks); + setLinksExternal = setLinks as unknown as (links: Record) => void; const renderLink: RenderLink = useCallback((link) => { receivedLinks.push(link); diff --git a/packages/joint-react/src/components/paper/__tests__/paper-html-overlay-links.test.tsx b/packages/joint-react/src/components/paper/__tests__/paper-html-overlay-links.test.tsx index 700e4a9747..8aa8fb8119 100644 --- a/packages/joint-react/src/components/paper/__tests__/paper-html-overlay-links.test.tsx +++ b/packages/joint-react/src/components/paper/__tests__/paper-html-overlay-links.test.tsx @@ -2,10 +2,10 @@ /* eslint-disable react-perf/jsx-no-new-object-as-prop */ import { render, waitFor, screen, act } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { GraphProvider, Paper, useCellActions, type GraphElement, type GraphLink } from '../../../index'; +import { GraphProvider, Paper, useCellActions, type FlatElementData, type FlatLinkData } from '../../../index'; import { useCallback } from 'react'; -interface TestElement extends GraphElement { +interface TestElement extends FlatElementData { label: string; } @@ -14,7 +14,7 @@ const elements: Record = { '2': { label: 'World', x: 100, y: 200, width: 100, height: 50 }, }; -const links: Record = { +const links: Record = { 'link-1': { source: '1', target: '2', diff --git a/packages/joint-react/src/components/paper/__tests__/paper-link-flickering.test.tsx b/packages/joint-react/src/components/paper/__tests__/paper-link-flickering.test.tsx index 237d6b0aa2..49eccf82be 100644 --- a/packages/joint-react/src/components/paper/__tests__/paper-link-flickering.test.tsx +++ b/packages/joint-react/src/components/paper/__tests__/paper-link-flickering.test.tsx @@ -18,7 +18,7 @@ */ import { render, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { GraphProvider, Paper, type GraphElement, type GraphLink } from '../../../index'; +import { GraphProvider, Paper, type FlatElementData, type FlatLinkData } from '../../../index'; /** * Flushes the microtask queue by waiting for a microtask to complete. @@ -29,7 +29,7 @@ async function flushMicrotasks(): Promise { }); } -interface TestElement extends GraphElement { +interface TestElement extends FlatElementData { readonly label: string; } @@ -38,7 +38,7 @@ const TEST_ELEMENTS: Record = { '2': { label: 'Element2', x: 100, y: 200, width: 100, height: 50 }, }; -const TEST_LINKS: Record = { +const TEST_LINKS: Record = { 'link-1': { source: '1', target: '2', diff --git a/packages/joint-react/src/components/paper/__tests__/paper.test.tsx b/packages/joint-react/src/components/paper/__tests__/paper.test.tsx index dc01071d45..1559c4b84a 100644 --- a/packages/joint-react/src/components/paper/__tests__/paper.test.tsx +++ b/packages/joint-react/src/components/paper/__tests__/paper.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { useNodeSize } from '../../../hooks/use-node-size'; import { act, useEffect, useRef, useState, type RefObject } from 'react'; import { useGraph, usePaperStoreContext, useCellId } from '../../../hooks'; -import type { GraphElement } from '../../../types/element-types'; +import type { FlatElementData } from '../../../types/element-types'; import { GraphProvider } from '../../graph/graph-provider'; import { Paper } from '../paper'; import { ReactLink, REACT_LINK_TYPE } from '../../../models/react-link'; @@ -471,7 +471,7 @@ describe('Paper Component', () => { let currentOutsideElements: Record = {}; function Content() { const [currentElements, setCurrentElements] = - useState>(elementsWithPosition); + useState>(elementsWithPosition); currentOutsideElements = currentElements as Record; return ( @@ -493,7 +493,7 @@ describe('Paper Component', () => { it('should update elements via react state, and then reflect the changes in the paper', async () => { function Content() { const [currentElements, setCurrentElements] = - useState>(elements); + useState>(elements); return ( diff --git a/packages/joint-react/src/components/paper/paper.stories.tsx b/packages/joint-react/src/components/paper/paper.stories.tsx index c05c4f3787..a3d759a017 100644 --- a/packages/joint-react/src/components/paper/paper.stories.tsx +++ b/packages/joint-react/src/components/paper/paper.stories.tsx @@ -20,11 +20,16 @@ import { useCellActions } from '../../hooks/use-cell-actions'; import { useCellId } from '../../hooks/use-cell-id'; import { Paper } from './paper'; import type { RenderElement } from './paper.types'; -import type { GraphElement } from '../../types/element-types'; +import type { FlatElementData } from '../../types/element-types'; import { GraphProvider } from '../graph/graph-provider'; export type Story = StoryObj; +interface StoryElementData extends FlatElementData { + readonly label: string; + readonly hoverColor: string; +} + const API_URL = getAPILink('Paper', 'variables'); const meta: Meta = { title: 'Components/Paper', @@ -369,7 +374,7 @@ export const WithOnClickColorChange: Story = { x: 50, y: 50, hoverColor: 'red', - } as GraphElement & { label: string; hoverColor: string }, + } satisfies StoryElementData, '2': { width: 100, height: 40, @@ -377,7 +382,7 @@ export const WithOnClickColorChange: Story = { x: 100, y: 250, hoverColor: 'red', - } as GraphElement & { label: string; hoverColor: string }, + } satisfies StoryElementData, }} links={{ l1: { @@ -448,11 +453,8 @@ export const WithDataWithoutWidthAndHeightAndXAndY: Story = { label: 'Element 1', hoverColor: 'red', somethingMine: true, - } as GraphElement & { label: string; hoverColor: string }, - '2': { label: 'Element 1', hoverColor: 'red' } as GraphElement & { - label: string; - hoverColor: string; - }, + } satisfies StoryElementData, + '2': { label: 'Element 1', hoverColor: 'red' } satisfies StoryElementData, }} links={{ l1: { diff --git a/packages/joint-react/src/components/paper/paper.tsx b/packages/joint-react/src/components/paper/paper.tsx index a8c7fd88e6..8880d6bf29 100644 --- a/packages/joint-react/src/components/paper/paper.tsx +++ b/packages/joint-react/src/components/paper/paper.tsx @@ -26,8 +26,8 @@ import { type CSSProperties, } from 'react'; import { useElements, useLinks } 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 type { PaperProps, RenderElement, RenderLink } from './paper.types'; import { assignOptions, dependencyExtract } from '../../utils/object-utilities'; import { PaperHTMLContainer } from './render-element/paper-html-container'; @@ -45,7 +45,7 @@ import { } from '../../hooks/use-graph-store-selector'; import type { ReactPaper } from '../../models/react-paper'; -const EMPTY_OBJECT = {} as Record; +const EMPTY_OBJECT = {} as Record; type ReactLinkConstructor = new (attributes?: dia.Link.Attributes) => dia.Link; /** @@ -91,9 +91,9 @@ function LinkItem({ portalElement, renderLink, }: { - link: GraphLink; + link: FlatLinkData; portalElement: SVGAElement; - renderLink: RenderLink; + renderLink: RenderLink; }) { if (!portalElement) { return null; @@ -125,8 +125,8 @@ function LinkItem({ * } * ``` */ -function PaperBase( - props: PaperProps, +function PaperBase( + props: PaperProps, forwardedRef: React.ForwardedRef ) { const { @@ -231,8 +231,8 @@ function PaperBase( defaultLink: defaultLinkJointJS, }, overWrite, - renderElement: renderElement as RenderElement, - renderLink: renderLink as RenderLink | undefined, + renderElement: renderElement as RenderElement, + renderLink: renderLink as RenderLink | undefined, scale, }); return () => { @@ -518,8 +518,8 @@ function PaperBase( * } * ``` */ -export const Paper = forwardRef(PaperBase) as ( - props: Readonly> & { +export const Paper = forwardRef(PaperBase) as ( + props: Readonly> & { ref?: React.Ref; } ) => ReturnType; diff --git a/packages/joint-react/src/components/paper/paper.types.ts b/packages/joint-react/src/components/paper/paper.types.ts index 3791407531..d5b1bbe00c 100644 --- a/packages/joint-react/src/components/paper/paper.types.ts +++ b/packages/joint-react/src/components/paper/paper.types.ts @@ -1,7 +1,7 @@ import type { dia } from '@joint/core'; -import type { GraphElement } from '../../types/element-types'; +import type { FlatElementData } from '../../types/element-types'; import type { OmitWithoutIndexSignature } from '../../types'; -import type { GraphLink } from '../../types/link-types'; +import type { FlatLinkData } from '../../types/link-types'; import type { OnPaperRenderElement } from '../../hooks/use-element-views'; import type { CSSProperties, PropsWithChildren, ReactNode } from 'react'; import type { PaperEvents } from '../../types/event.types'; @@ -20,23 +20,23 @@ export interface ReactPaperOptions extends ReactPaperOptionsBase { * Default link for the paper - for example if there is new element added, this will be used as default. */ readonly defaultLink?: - | ((cellView: dia.CellView, magnet: SVGElement) => dia.Link | GraphLink) + | ((cellView: dia.CellView, magnet: SVGElement) => dia.Link | FlatLinkData) | dia.Link - | GraphLink; + | FlatLinkData; } -export type RenderElement = ( - element: ElementItem +export type RenderElement = ( + element: ElementData ) => ReactNode; -export type RenderLink = (link: LinkItem) => ReactNode; +export type RenderLink = (link: LinkData) => ReactNode; /** * The props for the Paper component. Extend the `dia.Paper.Options` interface. * For more information, see the JointJS documentation. * @see https://docs.jointjs.com/api/dia/Paper */ -export interface PaperProps +export interface PaperProps extends ReactPaperOptions, PropsWithChildren, PaperEvents { @@ -67,7 +67,7 @@ export interface PaperProps * ``` */ - readonly renderElement?: RenderElement; + readonly renderElement?: RenderElement; /** * A function that renders the link. * @@ -105,7 +105,7 @@ export interface PaperProps * ) * ``` */ - readonly renderLink?: RenderLink; + readonly renderLink?: RenderLink; /** * Event called when all elements are properly measured (has all elements width and height greater than 1 - default). * In react, we cannot detect jointjs paper render:done event properly, so we use this special event to check if all elements are measured. diff --git a/packages/joint-react/src/components/paper/render-element/paper-element-item.tsx b/packages/joint-react/src/components/paper/render-element/paper-element-item.tsx index 86baf4454a..69d4f70086 100644 --- a/packages/joint-react/src/components/paper/render-element/paper-element-item.tsx +++ b/packages/joint-react/src/components/paper/render-element/paper-element-item.tsx @@ -1,10 +1,10 @@ import { useLayoutEffect, useMemo, type CSSProperties, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import typedMemo from '../../../utils/typed-react'; -import type { GraphElement } from '../../../types/element-types'; +import type { FlatElementData } from '../../../types/element-types'; import { useGraphStore, useNodeLayout } from '../../../hooks'; -export interface ElementItemProps { +export interface ElementItemProps { /** * A function that renders the element. It is called every time the element is rendered. */ @@ -19,7 +19,7 @@ export interface ElementItemProps { } // eslint-disable-next-line jsdoc/require-jsdoc -function SVGElementItemComponent( +function SVGElementItemComponent( props: ElementItemProps ) { const { renderElement, portalElement, areElementsMeasured, id, ...rest } = props; @@ -70,14 +70,14 @@ export const SVGElementItem = typedMemo(SVGElementItemComponent); * @returns The rendered element inside the portal. * @internal */ -function HTMLElementItemComponent( +function HTMLElementItemComponent( props: ElementItemProps ) { const { renderElement, portalElement, areElementsMeasured, id, ...rest } = props; const cell = rest as Data; // we must use renderElement and not cell data, because user can select different data, so then, the width and height do not have to be inside the cell data. const element = renderElement(cell); - const { width, height, x, y } = cell; + const { width, height, x, y } = cell as FlatElementData; const graphStore = useGraphStore(); useLayoutEffect(() => { diff --git a/packages/joint-react/src/components/text-node/text-node.tsx b/packages/joint-react/src/components/text-node/text-node.tsx index 3134ad1c43..c6eb9b3b11 100644 --- a/packages/joint-react/src/components/text-node/text-node.tsx +++ b/packages/joint-react/src/components/text-node/text-node.tsx @@ -1,6 +1,6 @@ -import type { dia } from '@joint/core'; import { util, V, type Vectorizer } from '@joint/core'; import React, { forwardRef, useEffect, type SVGTextElementAttributes } from 'react'; +import type { CellId } from '../../types/cell-id'; import { useCombinedRef } from '../../hooks/use-combined-ref'; import { isNumber } from '../../utils/is'; import { useCellId, useGraph } from '../../hooks'; @@ -8,7 +8,7 @@ import { useCellId, useGraph } from '../../hooks'; interface BreakTextWidthOptions { readonly width: number | undefined; readonly graph: ReturnType; - readonly cellId: dia.Cell.ID; + readonly cellId: CellId; } interface TextWrapStylesOptions { diff --git a/packages/joint-react/src/context/index.ts b/packages/joint-react/src/context/index.ts index 60811f76c0..1d0a1fa1bd 100644 --- a/packages/joint-react/src/context/index.ts +++ b/packages/joint-react/src/context/index.ts @@ -1,12 +1,12 @@ export * from './port-group-context'; import { createContext } from 'react'; -import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import type { GraphStore, PaperStore } from '../store'; export type StoreContext = GraphStore; export const GraphStoreContext = createContext(null); export const PaperStoreContext = createContext(null); -export const CellIdContext = createContext(undefined); +export const CellIdContext = createContext(undefined); export const CellIndexContext = createContext(undefined); export interface OverWriteResult { diff --git a/packages/joint-react/src/hooks/__tests__/use-links.test.ts b/packages/joint-react/src/hooks/__tests__/use-links.test.ts index 29b1aac4b8..90bb471a7d 100644 --- a/packages/joint-react/src/hooks/__tests__/use-links.test.ts +++ b/packages/joint-react/src/hooks/__tests__/use-links.test.ts @@ -1,15 +1,15 @@ import { renderHook, waitFor } from '@testing-library/react'; import { graphProviderWrapper } from '../../utils/test-wrappers'; import { useLinks } from '../use-links'; -import type { GraphLink } from '../../types/link-types'; -import type { dia } from '@joint/core'; +import type { FlatLinkData } from '../../types/link-types'; +import type { CellId } from '../../types/cell-id'; // Extract link source ID - source can be ID (string/number) or EndJSON object -function getLinkSourceId(link: GraphLink) { +function getLinkSourceId(link: FlatLinkData) { if (typeof link.source === 'object' && link.source != null && 'id' in link.source) { return link.source.id; } - return link.source as dia.Cell.ID; + return link.source as CellId; } describe('use-links', () => { diff --git a/packages/joint-react/src/hooks/use-cell-actions.ts b/packages/joint-react/src/hooks/use-cell-actions.ts index 026ae2bb35..86c7263083 100644 --- a/packages/joint-react/src/hooks/use-cell-actions.ts +++ b/packages/joint-react/src/hooks/use-cell-actions.ts @@ -1,17 +1,17 @@ import { useMemo } from 'react'; -import type { dia } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; -import type { GraphLink } from '../types/link-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatElementData } from '../types/element-types'; +import type { FlatLinkData } from '../types/link-types'; import type { GraphStoreSnapshot } from '../store'; import { useGraphStore } from './use-graph-store'; /** - * Normalizes element attributes to the GraphElement format. + * Normalizes element attributes to the FlatElementData format. * Converts nested JointJS format (position: {x, y}, size: {width, height}) * to flat format (x, y, width, height). * @param attributes */ -function normalizeElementAttributes(attributes: T): T { +function normalizeElementAttributes(attributes: T): T { const { position, size, @@ -25,14 +25,14 @@ function normalizeElementAttributes(attributes: T): T { // Convert position to flat x, y (prefer position over existing x, y) if (position !== undefined) { - (normalized as GraphElement).x = position.x; - (normalized as GraphElement).y = position.y; + (normalized as FlatElementData).x = position.x; + (normalized as FlatElementData).y = position.y; } // Convert size to flat width, height (prefer size over existing width, height) if (size !== undefined) { - (normalized as GraphElement).width = size.width; - (normalized as GraphElement).height = size.height; + (normalized as FlatElementData).width = size.width; + (normalized as FlatElementData).height = size.height; } return normalized; @@ -42,7 +42,7 @@ function normalizeElementAttributes(attributes: T): T { * Actions for manipulating cells (elements and links) in the graph. * @template Attributes - The type of cell attributes, which can be an element or a link */ -interface CellActions { +interface CellActions { /** * Sets or updates a cell in the graph. * Can be called in two ways: @@ -50,12 +50,12 @@ interface CellActions { * 2. With ID and updater: `set('1', (prev) => ({ ...prev, label: 'New' }))` * If the cell doesn't exist, it will be added. */ - set: (id: dia.Cell.ID, attributesOrUpdater: Attributes | ((previous: Attributes) => Attributes)) => void; + set: (id: CellId, attributesOrUpdater: Attributes | ((previous: Attributes) => Attributes)) => void; /** * Removes a cell from the graph by its ID. * @param id - The ID of the cell to remove */ - remove: (id: dia.Cell.ID) => void; + remove: (id: CellId) => void; } /** @@ -63,7 +63,7 @@ interface CellActions { * @param cell - The cell to check. * @returns True if the cell is a link, false otherwise. */ -function isLink(cell: GraphElement | GraphLink): cell is GraphLink { +function isLink(cell: FlatElementData | FlatLinkData): cell is FlatLinkData { return 'source' in cell && 'target' in cell; } @@ -89,7 +89,7 @@ function isLink(cell: GraphElement | GraphLink): cell is GraphLink { * @returns An object containing methods to set and remove cells * @example * ```tsx - * const { set, remove } = useCellActions>(); + * const { set, remove } = useCellActions>(); * * // Add or update element with ID and attributes * set('1', { x: 100, y: 150, width: 100, height: 50 }); @@ -102,29 +102,30 @@ function isLink(cell: GraphElement | GraphLink): cell is GraphLink { * ``` */ export function useCellActions< - Attributes extends GraphElement | GraphLink, + Attributes = FlatElementData | FlatLinkData, >(): CellActions { const { graph, publicState } = useGraphStore(); return useMemo( (): CellActions => ({ set( - id: dia.Cell.ID, + id: CellId, attributesOrUpdater: Attributes | ((previousAttributes: Attributes) => Attributes) ) { let attributes: Attributes; const { elements, links } = publicState.getSnapshot(); if (typeof attributesOrUpdater === 'function') { - const cell: GraphElement | GraphLink | undefined = elements[id] || links[id]; + const cell: FlatElementData | FlatLinkData | undefined = elements[id] || links[id]; if (!cell) throw new Error(`Cell with id "${id}" not found.`); - attributes = attributesOrUpdater(cell as Attributes); + attributes = (attributesOrUpdater as (previous: Attributes) => Attributes)(cell as Attributes); } else { attributes = attributesOrUpdater; } - const areAttributesLink = isLink(attributes); + const cellData = attributes as FlatElementData | FlatLinkData; + const areAttributesLink = isLink(cellData); const targetId = id; const hasElement = targetId in elements; @@ -135,14 +136,14 @@ export function useCellActions< const newLinks = { ...links }; if (hasElement) { - newElements[targetId] = normalizeElementAttributes(attributes); + newElements[targetId] = normalizeElementAttributes(cellData as FlatElementData); } else if (hasLink) { - newLinks[targetId] = attributes as GraphLink; + newLinks[targetId] = cellData as FlatLinkData; } else if (!isFound) { if (areAttributesLink) { - newLinks[targetId] = attributes as GraphLink; + newLinks[targetId] = cellData as FlatLinkData; } else { - newElements[targetId] = normalizeElementAttributes(attributes); + newElements[targetId] = normalizeElementAttributes(cellData as FlatElementData); } } diff --git a/packages/joint-react/src/hooks/use-cell-id.ts b/packages/joint-react/src/hooks/use-cell-id.ts index ee533bdc39..5799b2e8b5 100644 --- a/packages/joint-react/src/hooks/use-cell-id.ts +++ b/packages/joint-react/src/hooks/use-cell-id.ts @@ -1,4 +1,4 @@ -import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import { useContext } from 'react'; import { CellIdContext } from '../context'; @@ -13,7 +13,7 @@ import { CellIdContext } from '../context'; * const cellId = useCellId(); * ``` */ -export function useCellId(): dia.Cell.ID { +export function useCellId(): CellId { const id = useContext(CellIdContext); if (id === undefined) { throw new Error('useCellId must be used inside Paper renderElement'); diff --git a/packages/joint-react/src/hooks/use-element-views.ts b/packages/joint-react/src/hooks/use-element-views.ts index 1224ec9240..b06b1a1e18 100644 --- a/packages/joint-react/src/hooks/use-element-views.ts +++ b/packages/joint-react/src/hooks/use-element-views.ts @@ -1,4 +1,5 @@ import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import { useCallback, useState } from 'react'; export type OnPaperRenderElement = (elementView: dia.ElementView) => void; @@ -16,7 +17,7 @@ export type OnPaperRenderElement = (elementView: dia.ElementView) => void; * @internal */ export function useElementViews(idExtractor?: (elementView: dia.ElementView) => string) { - const [elementViews, setElements] = useState>({}); + const [elementViews, setElements] = useState>({}); const onRenderElement: OnPaperRenderElement = useCallback( (elementView) => { diff --git a/packages/joint-react/src/hooks/use-element.stories.tsx b/packages/joint-react/src/hooks/use-element.stories.tsx index 91b9395e4b..d5eb5f259b 100644 --- a/packages/joint-react/src/hooks/use-element.stories.tsx +++ b/packages/joint-react/src/hooks/use-element.stories.tsx @@ -4,7 +4,7 @@ import type { Meta } from '@storybook/react-vite'; import { HookTester, type TesterHookStory } from '../stories/utils/hook-tester'; import { makeRootDocumentation, makeStory } from '../stories/utils/make-story'; import { getAPILink } from '../stories/utils/get-api-documentation-link'; -import type { GraphElement } from '../types/element-types'; +import type { FlatElementData } from '../types/element-types'; const API_URL = getAPILink('useElement'); @@ -68,7 +68,7 @@ type Story = TesterHookStory; export const WithId = makeStory({ args: { useHook: useElement, - hookArgs: [(data: GraphElement) => data.id] as never, + hookArgs: [(data: FlatElementData) => data.id] as never, }, apiURL: API_URL, code: `import { useElement } from '@joint/react' @@ -86,7 +86,7 @@ function Component() { export const WithCoordinates = makeStory({ args: { useHook: useElement, - hookArgs: [(data: GraphElement) => ({ x: data.x, y: data.y })] as never, + hookArgs: [(data: FlatElementData) => ({ x: data.x, y: data.y })] as never, }, apiURL: API_URL, code: `import { useElement } from '@joint/react' diff --git a/packages/joint-react/src/hooks/use-element.ts b/packages/joint-react/src/hooks/use-element.ts index ce3e73680b..2b512abf23 100644 --- a/packages/joint-react/src/hooks/use-element.ts +++ b/packages/joint-react/src/hooks/use-element.ts @@ -1,6 +1,6 @@ import { util } from '@joint/core'; import { useCellId } from './use-cell-id'; -import type { GraphElement } from '../types/element-types'; +import type { FlatElementData } from '../types/element-types'; import { useGraphStoreSelector } from './use-graph-store-selector'; /** @@ -31,14 +31,14 @@ import { useGraphStoreSelector } from './use-graph-store-selector'; * @param isEqual The function used to check equality. @default util.isEqual * @returns The selected element based on the current cell id. */ -export function useElement( - selector: (item: Element) => ReturnedElements = (item) => item as unknown as ReturnedElements, +export function useElement( + selector: (item: ElementData) => ReturnedElements = (item) => item as unknown as ReturnedElements, isEqual: (a: ReturnedElements, b: ReturnedElements) => boolean = util.isEqual ): ReturnedElements { const id = useCellId(); return useGraphStoreSelector((store) => { - const element = store.elements[id] as Element | undefined; + const element = store.elements[id] as ElementData | undefined; if (!element) { return undefined as ReturnedElements; } diff --git a/packages/joint-react/src/hooks/use-elements.stories.tsx b/packages/joint-react/src/hooks/use-elements.stories.tsx index 3c427aa58e..09ea5e86fe 100644 --- a/packages/joint-react/src/hooks/use-elements.stories.tsx +++ b/packages/joint-react/src/hooks/use-elements.stories.tsx @@ -3,6 +3,7 @@ import { DataRenderer, SimpleGraphDecorator } from '../../.storybook/decorators/ import type { Meta } from '@storybook/react-vite'; import { HookTester, type TesterHookStory } from '../stories/utils/hook-tester'; import { useElements } from './use-elements'; +import type { FlatElementData } from '../types/element-types'; import { PAPER_CLASSNAME, PRIMARY } from 'storybook-config/theme'; import { getAPILink } from '../stories/utils/get-api-documentation-link'; import { makeRootDocumentation, makeStory } from '../stories/utils/make-story'; @@ -30,7 +31,7 @@ The **useElements** hook provides access to all elements in the graph. It suppor \`\`\`tsx import { useElements } from '@joint/react'; -// Get all elements (returns Record) +// Get all elements (returns Record) function Component() { const elements = useElements(); return
Total elements: {Object.keys(elements).length}
; @@ -91,7 +92,7 @@ export const Default = makeStory({ code: `import { useElements } from '@joint/react' function Component() { - const elements = useElements(); // returns Record + const elements = useElements(); // returns Record return
elements are: {JSON.stringify(elements)}
; }`, description: 'Get all elements as a Record keyed by ID.', @@ -100,7 +101,7 @@ function Component() { export const WithSelectedJustIds = makeStory({ args: { useHook: useElements, - hookArgs: [(elements) => Object.values(elements).map((element) => element.id)], + hookArgs: [(elements) => Object.values(elements).map((element) => (element as FlatElementData).id)], render: (result) => ( ({ hookArgs: [ (elements) => Object.values(elements).map((element) => ({ - x: element.x, - y: element.y, + x: (element as FlatElementData).x, + y: (element as FlatElementData).y, })), ], render: (result) => ( @@ -189,8 +190,8 @@ export const WithJustPositionButNotReRenderBecauseCompareFN = makeStory({ hookArgs: [ (elements) => Object.values(elements).map((element) => ({ - x: element.x, - y: element.y, + x: (element as FlatElementData).x, + y: (element as FlatElementData).y, })), (_previous, _next) => true, ], @@ -225,7 +226,7 @@ export const WithAdditionalData = makeStory({ useHook: useElements, hookArgs: [ (elements) => - Object.values(elements).map((element) => ({ id: element.id, other: 'something' })), + Object.values(elements).map((element) => ({ id: (element as FlatElementData).id, other: 'something' })), ], render: (result) => (
diff --git a/packages/joint-react/src/hooks/use-elements.ts b/packages/joint-react/src/hooks/use-elements.ts index a54b11def1..1484d1d8eb 100644 --- a/packages/joint-react/src/hooks/use-elements.ts +++ b/packages/joint-react/src/hooks/use-elements.ts @@ -1,6 +1,6 @@ -import type { dia } from '@joint/core'; import { util } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatElementData } from '../types/element-types'; import { useGraphStoreSelector } from './use-graph-store-selector'; /** @@ -8,9 +8,9 @@ import { useGraphStoreSelector } from './use-graph-store-selector'; * @param items - The items to select from. * @returns - The selected items. */ -function defaultSelector( - items: Record -): Record { +function defaultSelector( + items: Record +): Record { return items; } @@ -32,7 +32,7 @@ function defaultSelector( * Using without a selector (returns all elements as a Record): * ```tsx * const elements = useElements(); - * // elements is Record + * // elements is Record * ``` * @example * Using with a selector (extract part of each element): @@ -58,16 +58,16 @@ function defaultSelector( * @returns - The selected elements. */ export function useElements< - Elements extends GraphElement = GraphElement, - SelectorReturnType = Record, + ElementData = FlatElementData, + SelectorReturnType = Record, >( - selector: (items: Record) => SelectorReturnType = defaultSelector as () => SelectorReturnType, + selector: (items: Record) => SelectorReturnType = defaultSelector as () => SelectorReturnType, isEqual: (a: SelectorReturnType, b: SelectorReturnType) => boolean = util.isEqual as ( a: SelectorReturnType, b: SelectorReturnType ) => boolean ): SelectorReturnType { return useGraphStoreSelector((snapshot) => { - return selector(snapshot.elements as Record); + return selector(snapshot.elements as Record); }, isEqual); } diff --git a/packages/joint-react/src/hooks/use-graph-store-selector.ts b/packages/joint-react/src/hooks/use-graph-store-selector.ts index db7ec04048..026e00ecad 100644 --- a/packages/joint-react/src/hooks/use-graph-store-selector.ts +++ b/packages/joint-react/src/hooks/use-graph-store-selector.ts @@ -2,8 +2,8 @@ import type { GraphStoreSnapshot, GraphStoreInternalSnapshot, } from '../store'; -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 type { ExternalStoreLike, MarkDeepReadOnly } from '../utils/create-state'; import { useGraphStore } from './use-graph-store'; import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'; @@ -37,15 +37,15 @@ export function useStoreSelector( */ export function useGraphStoreSelector< Selection, - Element extends GraphElement = GraphElement, - Link extends GraphLink = GraphLink, + ElementData = FlatElementData, + LinkData = FlatLinkData, >( - selector: (snapshot: MarkDeepReadOnly>) => Selection, + selector: (snapshot: MarkDeepReadOnly>) => Selection, isEqual?: (a: Selection, b: Selection) => boolean ): Selection { const { publicState } = useGraphStore(); return useStoreSelector( - publicState as unknown as ExternalStoreLike>, + publicState as unknown as ExternalStoreLike>, selector, isEqual ); diff --git a/packages/joint-react/src/hooks/use-link-layout.ts b/packages/joint-react/src/hooks/use-link-layout.ts index d93f1f9a34..387037c822 100644 --- a/packages/joint-react/src/hooks/use-link-layout.ts +++ b/packages/joint-react/src/hooks/use-link-layout.ts @@ -6,7 +6,7 @@ import type { LinkLayout } from '../store/graph-store'; // Re-export LinkLayout for convenience export type { LinkLayout } from '../store/graph-store'; -import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import { CellIdContext } from '../context'; /** @@ -45,9 +45,9 @@ import { CellIdContext } from '../context'; * } * ``` */ -export function useLinkLayout( - id?: Id -): Id extends dia.Cell.ID ? LinkLayout | undefined : LinkLayout | undefined { +export function useLinkLayout(): LinkLayout; +export function useLinkLayout(id: CellId): LinkLayout | undefined; +export function useLinkLayout(id?: CellId): LinkLayout | undefined { const contextId = useContext(CellIdContext); const { layoutState } = useGraphStore(); const { paperId } = usePaperStoreContext(); diff --git a/packages/joint-react/src/hooks/use-links.stories.tsx b/packages/joint-react/src/hooks/use-links.stories.tsx index 27a4df7eb4..12538aff16 100644 --- a/packages/joint-react/src/hooks/use-links.stories.tsx +++ b/packages/joint-react/src/hooks/use-links.stories.tsx @@ -29,7 +29,7 @@ The **useLinks** hook provides access to all links in the graph. It supports sel \`\`\`tsx import { useLinks } from '@joint/react'; -// Get all links (returns Record) +// Get all links (returns Record) function Component() { const links = useLinks(); return
Total links: {Object.keys(links).length}
; @@ -74,7 +74,7 @@ export const GetAllLinks: Story = makeStory({ code: `import { useLinks } from '@joint/react' function Component() { - const links = useLinks(); // returns Record + const links = useLinks(); // returns Record return (

Total links: {Object.keys(links).length}

diff --git a/packages/joint-react/src/hooks/use-links.ts b/packages/joint-react/src/hooks/use-links.ts index af07e8922c..a8223e5aa4 100644 --- a/packages/joint-react/src/hooks/use-links.ts +++ b/packages/joint-react/src/hooks/use-links.ts @@ -1,6 +1,6 @@ -import type { dia } from '@joint/core'; import { util } from '@joint/core'; -import type { GraphLink } from '../types/link-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatLinkData } from '../types/link-types'; import { useGraphStoreSelector } from './use-graph-store-selector'; /** @@ -11,9 +11,9 @@ import { useGraphStoreSelector } from './use-graph-store-selector'; * @returns The same links record * @internal */ -function defaultSelector( - items: Record -): Record { +function defaultSelector( + items: Record +): Record { return items; } @@ -45,7 +45,7 @@ function defaultSelector( * ```ts * // Get all links as a Record * const links = useLinks(); - * // links is Record + * // links is Record * ``` * @example * ```ts @@ -66,11 +66,11 @@ function defaultSelector( * ); * ``` */ -export function useLinks>( - selector: (items: Record) => SelectorReturnType = defaultSelector as () => SelectorReturnType, +export function useLinks>( + selector: (items: Record) => SelectorReturnType = defaultSelector as () => SelectorReturnType, isEqual: (a: SelectorReturnType, b: SelectorReturnType) => boolean = util.isEqual ): SelectorReturnType { return useGraphStoreSelector((snapshot) => { - return selector(snapshot.links as Record); + return selector(snapshot.links as Record); }, isEqual); } diff --git a/packages/joint-react/src/hooks/use-node-layout.ts b/packages/joint-react/src/hooks/use-node-layout.ts index 225cdd26ba..39b3836c0f 100644 --- a/packages/joint-react/src/hooks/use-node-layout.ts +++ b/packages/joint-react/src/hooks/use-node-layout.ts @@ -2,7 +2,7 @@ import { useContext } from 'react'; import { useGraphStore } from './use-graph-store'; import { useStoreSelector } from './use-graph-store-selector'; import type { NodeLayout } from '../store/graph-store'; -import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import { CellIdContext } from '../context'; /** @@ -43,9 +43,9 @@ import { CellIdContext } from '../context'; * } * ``` */ -export function useNodeLayout( - id?: Id -): Id extends dia.Cell.ID ? NodeLayout | undefined : NodeLayout { +export function useNodeLayout(): NodeLayout; +export function useNodeLayout(id: CellId): NodeLayout | undefined; +export function useNodeLayout(id?: CellId): NodeLayout | undefined { const contextId = useContext(CellIdContext); const { layoutState } = useGraphStore(); const actualId = id ?? contextId; diff --git a/packages/joint-react/src/hooks/use-state-to-external-store.ts b/packages/joint-react/src/hooks/use-state-to-external-store.ts index 60e84c6485..311c54c116 100644 --- a/packages/joint-react/src/hooks/use-state-to-external-store.ts +++ b/packages/joint-react/src/hooks/use-state-to-external-store.ts @@ -6,29 +6,28 @@ import { type Dispatch, type SetStateAction, } from 'react'; -import type { GraphElement } from '../types/element-types'; -import type { GraphLink } from '../types/link-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatElementData } from '../types/element-types'; +import type { FlatLinkData } from '../types/link-types'; import type { ExternalStoreLike } from '../utils/create-state'; import type { GraphStoreSnapshot } from '../store'; import { isUpdater } from '../utils/is'; import { util } from '@joint/core'; -import type { dia } from '@joint/core'; - /** * Options for converting React state to an external store interface. * @template Element - The type of elements * @template Link - The type of links */ -interface Options { +interface Options { /** Current elements Record from React state, keyed by cell ID */ - readonly elements?: Record; + readonly elements?: Record; /** Current links Record from React state, keyed by cell ID */ - readonly links?: Record; + readonly links?: Record; /** Callback function called when elements change */ - readonly onElementsChange?: Dispatch>>; + readonly onElementsChange?: Dispatch>>; /** Callback function called when links change */ - readonly onLinksChange?: Dispatch>>; + readonly onLinksChange?: Dispatch>>; } /** @@ -46,12 +45,12 @@ interface Options { * @param options - The options containing elements, links, and their change handlers * @returns An external store-like interface compatible with GraphStore, or undefined if uncontrolled */ -export function useStateToExternalStore( - options: Options -): ExternalStoreLike> | undefined { +export function useStateToExternalStore( + options: Options +): ExternalStoreLike> | undefined { const { elements = {}, links = {}, onElementsChange, onLinksChange } = options; const subscribers = useRef void>>(new Set()); - const snapshot = useRef>({ elements, links }); + const snapshot = useRef>({ elements, links }); const hasOnChange = typeof onElementsChange === 'function' || typeof onLinksChange === 'function'; const notifySubscribers = useRef(() => { @@ -76,7 +75,7 @@ export function useStateToExternalStore> | undefined => { + const store = useMemo((): ExternalStoreLike> | undefined => { if (!hasOnChange) { return undefined; } diff --git a/packages/joint-react/src/index.ts b/packages/joint-react/src/index.ts index 395c390c35..1602ab4ad8 100644 --- a/packages/joint-react/src/index.ts +++ b/packages/joint-react/src/index.ts @@ -15,6 +15,7 @@ export * from './models/react-link'; export { ReactPaper } from './models/react-paper'; export type { IReactPaper } from './models/react-paper.types'; +export * from './types/cell-id'; export * from './types/element-types'; export * from './types/paper.types'; export * from './types/link-types'; diff --git a/packages/joint-react/src/models/react-paper.ts b/packages/joint-react/src/models/react-paper.ts index 88dc9514a1..74525d6070 100644 --- a/packages/joint-react/src/models/react-paper.ts +++ b/packages/joint-react/src/models/react-paper.ts @@ -1,4 +1,5 @@ import { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import type { GraphStore } from '../store/graph-store'; import type { ReactPaperOptions } from './react-paper.types'; import type { ReactElementViewCache, ReactLinkViewCache } from '../types/paper.types'; @@ -31,7 +32,7 @@ export class ReactPaper extends dia.Paper { public reactLinkCache!: ReactLinkViewCache; /** Links waiting for source/target elements to render */ - private pendingLinks: Set = new Set(); + private pendingLinks: Set = new Set(); constructor(options: ReactPaperOptions) { super(options); @@ -43,7 +44,7 @@ export class ReactPaper extends dia.Paper { * @param elementId - The element ID to check * @returns true if element view exists and has children */ - private isElementReady(elementId: dia.Cell.ID | undefined): boolean { + private isElementReady(elementId: CellId | undefined): boolean { if (!elementId) return false; const elementView = this.reactElementCache.elementViews[elementId]; return !!elementView?.el && elementView.el.children.length > 0; @@ -57,7 +58,7 @@ export class ReactPaper extends dia.Paper { */ private isLinkEndReady(end: dia.Link.EndJSON): boolean { if (!end.id) return true; - return this.isElementReady(end.id as dia.Cell.ID); + return this.isElementReady(end.id as CellId); } /** @@ -67,7 +68,7 @@ export class ReactPaper extends dia.Paper { public checkPendingLinks(): void { if (this.pendingLinks.size === 0) return; - const linksToShow: dia.Cell.ID[] = []; + const linksToShow: CellId[] = []; for (const linkId of this.pendingLinks) { const linkView = this.reactLinkCache.linkViews[linkId]; @@ -100,7 +101,7 @@ export class ReactPaper extends dia.Paper { * @param cell - The cell to remove from cache */ private removeFromCache(cell: dia.Cell): void { - const cellId = cell.id; + const cellId = cell.id as CellId; if (cell.isElement()) { const newElementViews = { ...this.reactElementCache.elementViews }; @@ -125,7 +126,7 @@ export class ReactPaper extends dia.Paper { // Call parent implementation first super.insertView(view, isInitialInsert); - const cellId = view.model.id; + const cellId = view.model.id as CellId; if (view.model.isElement()) { // Add to element views cache diff --git a/packages/joint-react/src/models/react-paper.types.ts b/packages/joint-react/src/models/react-paper.types.ts index 439a9cf083..f5477bc3b2 100644 --- a/packages/joint-react/src/models/react-paper.types.ts +++ b/packages/joint-react/src/models/react-paper.types.ts @@ -1,4 +1,5 @@ import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import type { GraphStore } from '../store/graph-store'; /** @@ -17,11 +18,11 @@ export interface ReactPaperOptions extends dia.Paper.Options { export interface IReactPaper extends dia.Paper { /** Cache for element views - managed by ReactPaper */ readonly reactElementCache: { - elementViews: Record; + elementViews: Record; }; /** Cache for link views - managed by ReactPaper */ readonly reactLinkCache: { - linkViews: Record; + linkViews: Record; linksData: Record; }; } diff --git a/packages/joint-react/src/state/__tests__/data-mapper.test.ts b/packages/joint-react/src/state/__tests__/data-mapper.test.ts index e448876ad9..0b0a19f541 100644 --- a/packages/joint-react/src/state/__tests__/data-mapper.test.ts +++ b/packages/joint-react/src/state/__tests__/data-mapper.test.ts @@ -4,23 +4,23 @@ import { dia, shapes } from '@joint/core'; import { ReactElement } from '../../models/react-element'; import { ReactLink, REACT_LINK_TYPE } from '../../models/react-link'; -import type { GraphElement, GraphElementPort } from '../../types/element-types'; -import type { GraphLink } from '../../types/link-types'; +import type { FlatElementData, FlatElementPort } from '../../types/element-types'; +import type { FlatLinkData } from '../../types/link-types'; import type { ElementToGraphOptions, LinkToGraphOptions, GraphToLinkOptions } from '../graph-state-selectors'; import { defaultMapDataToElementAttributes, defaultMapDataToLinkAttributes, defaultMapElementAttributesToData, defaultMapLinkAttributesToData } from '../data-mapping'; const DEFAULT_CELL_NAMESPACE = { ...shapes, ReactElement, ReactLink }; -function elementToGraphOpts(id: string, data: GraphElement, graph: dia.Graph): ElementToGraphOptions { +function elementToGraphOpts(id: string, data: FlatElementData, graph: dia.Graph): ElementToGraphOptions { return { id, data, graph, toAttributes: (d) => defaultMapDataToElementAttributes({ id, data: d }) }; } -function graphToElementOpts(id: string, cell: dia.Element, graph: dia.Graph, previousData?: GraphElement) { +function graphToElementOpts(id: string, cell: dia.Element, graph: dia.Graph, previousData?: FlatElementData) { return { id, cell, graph, previousData, toData: () => defaultMapElementAttributesToData({ cell }) }; } -function linkToGraphOpts(id: string, data: GraphLink, graph: dia.Graph): LinkToGraphOptions { +function linkToGraphOpts(id: string, data: FlatLinkData, graph: dia.Graph): LinkToGraphOptions { return { id, data, graph, toAttributes: (d) => defaultMapDataToLinkAttributes({ id, data: d }) }; } -function graphToLinkOpts(id: string, cell: dia.Link, graph: dia.Graph, previousData?: GraphLink): GraphToLinkOptions { +function graphToLinkOpts(id: string, cell: dia.Link, graph: dia.Graph, previousData?: FlatLinkData): GraphToLinkOptions { return { id, cell, graph, previousData, toData: () => defaultMapLinkAttributesToData({ cell }) }; } @@ -38,7 +38,7 @@ describe('dataMapper', () => { describe('element round-trip', () => { it('should convert element data to JointJS and back', () => { const id = 'el-1'; - const data: GraphElement = { x: 10, y: 20, width: 100, height: 50 }; + const data: FlatElementData = { x: 10, y: 20, width: 100, height: 50 }; const cellJson = defaultMapDataToElementAttributes(elementToGraphOpts(id, data, graph)); expect(cellJson.position).toEqual({ x: 10, y: 20 }); @@ -53,7 +53,7 @@ describe('dataMapper', () => { }); it('should store user data in cell.data and extract on reverse', () => { - type MyElement = GraphElement & { label: string; color: string }; + type MyElement = FlatElementData & { label: string; color: string }; const id = 'el-1'; const data: MyElement = { x: 0, y: 0, width: 50, height: 50, label: 'Hello', color: 'red' }; @@ -80,7 +80,7 @@ describe('dataMapper', () => { graph.addCell(cellJson); const cell = graph.getCell(id) as dia.Element; - type E = GraphElement & { known?: string; extra?: string }; + type E = FlatElementData & { known?: string; extra?: string }; const previousData: E = { x: 0, y: 0, width: 0, height: 0, known: undefined }; const result = defaultMapElementAttributesToData(graphToElementOpts(id, cell, graph, previousData)); @@ -92,11 +92,11 @@ describe('dataMapper', () => { describe('element ports conversion', () => { it('should convert simplified ports to JointJS format', () => { - const ports: GraphElementPort[] = [ + const ports: FlatElementPort[] = [ { cx: 0, cy: 0.5, width: 10, height: 10, color: 'blue', id: 'p1' }, ]; const id = 'el-1'; - const data: GraphElement = { x: 0, y: 0, width: 100, height: 100, ports }; + const data: FlatElementData = { x: 0, y: 0, width: 100, height: 100, ports }; const cellJson = defaultMapDataToElementAttributes(elementToGraphOpts(id, data, graph)); expect(cellJson.ports).toBeDefined(); @@ -106,11 +106,11 @@ describe('dataMapper', () => { }); it('should convert port with label', () => { - const ports: GraphElementPort[] = [ + const ports: FlatElementPort[] = [ { cx: 0, cy: 0.5, label: 'Port A', labelPosition: 'outside', labelColor: 'red', id: 'p1' }, ]; const id = 'el-1'; - const data: GraphElement = { x: 0, y: 0, width: 100, height: 100, ports }; + const data: FlatElementData = { x: 0, y: 0, width: 100, height: 100, ports }; const cellJson = defaultMapDataToElementAttributes(elementToGraphOpts(id, data, graph)); const [port] = cellJson.ports.items; @@ -120,11 +120,11 @@ describe('dataMapper', () => { }); it('should handle rect shape ports', () => { - const ports: GraphElementPort[] = [ + const ports: FlatElementPort[] = [ { cx: 0, cy: 0, width: 20, height: 10, shape: 'rect', id: 'p1' }, ]; const id = 'el-1'; - const data: GraphElement = { x: 0, y: 0, width: 100, height: 100, ports }; + const data: FlatElementData = { x: 0, y: 0, width: 100, height: 100, ports }; const cellJson = defaultMapDataToElementAttributes(elementToGraphOpts(id, data, graph)); const portMarkup = cellJson.ports.items[0].markup; @@ -135,7 +135,7 @@ describe('dataMapper', () => { describe('link round-trip', () => { it('should convert link data to JointJS and back', () => { const id = 'link-1'; - const data: GraphLink = { source: 'el-1', target: 'el-2' }; + const data: FlatLinkData = { source: 'el-1', target: 'el-2' }; const cellJson = defaultMapDataToLinkAttributes(linkToGraphOpts(id, data, graph)); expect(cellJson.source).toEqual({ id: 'el-1' }); @@ -153,7 +153,7 @@ describe('dataMapper', () => { it('should apply theme defaults', () => { const id = 'link-1'; - const data: GraphLink = { source: 'a', target: 'b' }; + const data: FlatLinkData = { source: 'a', target: 'b' }; const cellJson = defaultMapDataToLinkAttributes(linkToGraphOpts(id, data, graph)); expect(cellJson.attrs?.line?.stroke).toBe('#333333'); @@ -165,7 +165,7 @@ describe('dataMapper', () => { it('should apply custom theme props', () => { const id = 'link-1'; - const data: GraphLink = { source: 'a', target: 'b', color: 'red', width: 4, pattern: '5 5' }; + const data: FlatLinkData = { source: 'a', target: 'b', color: 'red', width: 4, pattern: '5 5' }; const cellJson = defaultMapDataToLinkAttributes(linkToGraphOpts(id, data, graph)); expect(cellJson.attrs?.line?.stroke).toBe('red'); @@ -174,7 +174,7 @@ describe('dataMapper', () => { }); it('should store user data in cell.data alongside theme props', () => { - type MyLink = GraphLink & { weight: number }; + type MyLink = FlatLinkData & { weight: number }; const id = 'link-1'; const data: MyLink = { source: 'a', target: 'b', weight: 5 }; @@ -195,7 +195,7 @@ describe('dataMapper', () => { graph.addCell(cellJson); const cell = graph.getCell(id) as dia.Link; - type L = GraphLink & { known?: string; extra?: string }; + type L = FlatLinkData & { known?: string; extra?: string }; const previousData: L = { source: 'a', target: 'b', known: undefined }; // previousData is passed through but the default mapper does not filter by it @@ -206,7 +206,7 @@ describe('dataMapper', () => { it('should handle source/target with ports', () => { const id = 'link-1'; - const data: GraphLink = { + const data: FlatLinkData = { source: 'el-1', target: 'el-2', sourcePort: 'p1', diff --git a/packages/joint-react/src/state/__tests__/graph-state-selectors.test.ts b/packages/joint-react/src/state/__tests__/graph-state-selectors.test.ts index bed9078da2..37367731bb 100644 --- a/packages/joint-react/src/state/__tests__/graph-state-selectors.test.ts +++ b/packages/joint-react/src/state/__tests__/graph-state-selectors.test.ts @@ -4,8 +4,8 @@ import { dia, shapes } from '@joint/core'; import { ReactElement } from '../../models/react-element'; import { ReactLink, REACT_LINK_TYPE } from '../../models/react-link'; -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 { defaultMapDataToElementAttributes, defaultMapDataToLinkAttributes, @@ -22,7 +22,7 @@ import type { const DEFAULT_CELL_NAMESPACE = { ...shapes, ReactElement, ReactLink }; // Helper functions to create options (no more defaultAttributes) -const createElementToGraphOptions = ( +const createElementToGraphOptions = ( id: string, data: E, graph: dia.Graph @@ -33,7 +33,7 @@ const createElementToGraphOptions = ( toAttributes: (newData) => defaultMapDataToElementAttributes({ id, data: newData }), }); -const createGraphToElementOptions = ( +const createGraphToElementOptions = ( id: string, cell: dia.Element, graph: dia.Graph, @@ -46,7 +46,7 @@ const createGraphToElementOptions = ( toData: () => defaultMapElementAttributesToData({ cell }), }); -const createLinkToGraphOptions = ( +const createLinkToGraphOptions = ( id: string, data: L, graph: dia.Graph @@ -57,7 +57,7 @@ const createLinkToGraphOptions = ( toAttributes: (newData) => defaultMapDataToLinkAttributes({ id, data: newData }), }); -const createGraphToLinkOptions = ( +const createGraphToLinkOptions = ( id: string, cell: dia.Link, graph: dia.Graph, @@ -84,7 +84,7 @@ describe('graph-state-selectors', () => { describe('defaultMapDataToElementAttributes', () => { it('should map element to graph cell JSON', () => { const id = 'element-1'; - const data: GraphElement = { + const data: FlatElementData = { x: 10, y: 20, width: 100, @@ -121,7 +121,7 @@ describe('graph-state-selectors', () => { it('should handle element without type (defaults to REACT_TYPE)', () => { const id = 'element-1'; - const data: GraphElement = { + const data: FlatElementData = { x: 10, y: 20, width: 100, @@ -152,7 +152,7 @@ describe('graph-state-selectors', () => { it('should preserve all element properties', () => { const id = 'element-1'; - const data: GraphElement = { + const data: FlatElementData = { x: 10, y: 20, width: 100, @@ -247,7 +247,7 @@ describe('graph-state-selectors', () => { graph.addCell(elementAsGraphJson); const cell = graph.getCell('element-1') as dia.Element; - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { customProp?: string; extraProp?: string; }; @@ -289,7 +289,7 @@ describe('graph-state-selectors', () => { graph.addCell(elementAsGraphJson); const cell = graph.getCell('element-1') as dia.Element; - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { customProp?: string; }; @@ -360,7 +360,7 @@ describe('graph-state-selectors', () => { describe('defaultMapDataToLinkAttributes', () => { it('should map link to graph cell JSON', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', type: REACT_LINK_TYPE, @@ -392,7 +392,7 @@ describe('graph-state-selectors', () => { it('should handle link with ports', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', sourcePort: 'port-1', @@ -428,7 +428,7 @@ describe('graph-state-selectors', () => { it('should use theme color property for stroke', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', color: 'red', @@ -444,7 +444,7 @@ describe('graph-state-selectors', () => { it('should explicitly set targetMarker to null in line attrs when set to none', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', targetMarker: 'none', @@ -458,7 +458,7 @@ describe('graph-state-selectors', () => { it('should normalize custom targetMarker with only d to path marker', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', targetMarker: { d: 'M 0 0 7 5 7 -5' } as dia.SVGMarkerJSON, @@ -475,7 +475,7 @@ describe('graph-state-selectors', () => { it('should not include sourceMarker in line attrs when set to none', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', sourceMarker: 'none', @@ -489,7 +489,7 @@ describe('graph-state-selectors', () => { it('should hide default targetMarker after round-trip through graph when set to none', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', targetMarker: 'none', @@ -509,7 +509,7 @@ describe('graph-state-selectors', () => { it('should preserve all link properties', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', type: REACT_LINK_TYPE, @@ -568,7 +568,7 @@ describe('graph-state-selectors', () => { graph.addCell(linkAsGraphJson); const linkCell = graph.getCell('link-1') as dia.Link; - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { customProp?: string; extraProp?: string; }; @@ -608,7 +608,7 @@ describe('graph-state-selectors', () => { graph.addCell(linkAsGraphJson); const linkCell = graph.getCell('link-1') as dia.Link; - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { customProp?: string; }; @@ -714,7 +714,7 @@ describe('graph-state-selectors', () => { const cell = graph.getCell('element-1') as dia.Element; // previousData state only has specific properties - const previousData: GraphElement = { + const previousData: FlatElementData = { x: 5, y: 15, width: 80, @@ -742,7 +742,7 @@ describe('graph-state-selectors', () => { graph.addCell(elementAsGraphJson); const cell = graph.getCell('element-1') as dia.Element; - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { customProp?: string; }; @@ -766,7 +766,7 @@ describe('graph-state-selectors', () => { describe('integration: links round-trip', () => { it('should handle link round-trip', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', type: REACT_LINK_TYPE, @@ -800,7 +800,7 @@ describe('graph-state-selectors', () => { it('should handle link with complex properties', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', sourcePort: 'port-1', @@ -842,7 +842,7 @@ describe('graph-state-selectors', () => { }); it('should include all cell.data properties regardless of previousData', () => { - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { customProp?: string; extraProp?: string; anotherProp?: number; @@ -894,7 +894,7 @@ describe('graph-state-selectors', () => { }); it('should handle multiple links with previousData state', () => { - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { label?: string; metadata?: Record; }; @@ -987,7 +987,7 @@ describe('graph-state-selectors', () => { }); it('should only return properties that exist in cell.data', () => { - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { optionalProp?: string; anotherOptionalProp?: number; }; @@ -1027,7 +1027,7 @@ describe('graph-state-selectors', () => { }); it('should handle link updates including all cell.data properties', () => { - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { status?: string; weight?: number; newProp?: string; @@ -1093,7 +1093,7 @@ describe('graph-state-selectors', () => { it('should handle link with attrs merging', () => { const id = 'link-1'; - const link: GraphLink = { + const link: FlatLinkData = { source: 'element-1', target: 'element-2', type: REACT_LINK_TYPE, @@ -1128,7 +1128,7 @@ describe('graph-state-selectors', () => { describe('integration: new properties defined in state type', () => { it('should return new element property when it exists in previousData state (even if undefined)', () => { - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { newProperty?: string; anotherNewProperty?: number; }; @@ -1172,7 +1172,7 @@ describe('graph-state-selectors', () => { }); it('should return new link property when it exists in previousData state (even if undefined)', () => { - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { newLinkProperty?: string; priority?: number; metadata?: Record; @@ -1217,7 +1217,7 @@ describe('graph-state-selectors', () => { }); it('should return multiple new element properties when all are defined in previousData state', () => { - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { status?: string; category?: string; tags?: string[]; @@ -1267,7 +1267,7 @@ describe('graph-state-selectors', () => { }); it('should return new link properties with complex nested structures when defined in previousData state', () => { - type ExtendedLink = GraphLink & { + type ExtendedLink = FlatLinkData & { config?: { style?: string; animation?: boolean; @@ -1327,7 +1327,7 @@ describe('graph-state-selectors', () => { }); it('should return all cell.data properties regardless of whether they are in previousData', () => { - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { definedProperty?: string; undefinedProperty?: number; notInPreviousDataProperty?: boolean; @@ -1373,8 +1373,8 @@ describe('graph-state-selectors', () => { expect((elementFromGraph as ExtendedElement).notInPreviousDataProperty).toBe(true); }); - it('should return new link properties when mixed with existing GraphLink properties', () => { - type ExtendedLink = GraphLink & { + it('should return new link properties when mixed with existing FlatLinkData properties', () => { + type ExtendedLink = FlatLinkData & { customLabel?: string; weight?: number; }; @@ -1384,8 +1384,8 @@ describe('graph-state-selectors', () => { source: 'element-1', target: 'element-2', type: REACT_LINK_TYPE, - z: 5, // Existing GraphLink property - markup: [{ tagName: 'path' }], // Existing GraphLink property + z: 5, // Existing FlatLinkData property + markup: [{ tagName: 'path' }], // Existing FlatLinkData property customLabel: 'Custom', // New property weight: 10, // New property }; @@ -1418,7 +1418,7 @@ describe('graph-state-selectors', () => { }); it('should properly return new properties after element update', () => { - type ExtendedElement = GraphElement & { + type ExtendedElement = FlatElementData & { version?: number; lastModified?: string; }; @@ -1481,7 +1481,7 @@ describe('graph-state-selectors', () => { describe('custom selector usage', () => { it('should allow using flatMapper functions directly', () => { - type CustomElement = GraphElement & { label: string }; + type CustomElement = FlatElementData & { label: string }; const id = 'node1'; const element: CustomElement = { x: 100, @@ -1498,7 +1498,7 @@ describe('graph-state-selectors', () => { }); it('should allow custom selector to modify flatMapper result', () => { - type CustomElement = GraphElement & { label: string }; + type CustomElement = FlatElementData & { label: string }; const id = 'node1'; const element: CustomElement = { x: 100, @@ -1521,7 +1521,7 @@ describe('graph-state-selectors', () => { }); it('should allow custom selector to ignore flatMapper and return custom result', () => { - type CustomElement = GraphElement & { label: string }; + type CustomElement = FlatElementData & { label: string }; const id = 'node1'; const element: CustomElement = { x: 100, @@ -1549,7 +1549,7 @@ describe('graph-state-selectors', () => { }); it('should allow custom link selector to use flatMapper', () => { - type CustomLink = GraphLink & { weight: number }; + type CustomLink = FlatLinkData & { weight: number }; const id = 'link1'; const data: CustomLink = { source: 'node1', @@ -1578,7 +1578,7 @@ describe('graph-state-selectors', () => { }); it('should allow custom link selector to modify flatMapper result', () => { - type CustomLink = GraphLink & { weight: number }; + type CustomLink = FlatLinkData & { weight: number }; const id = 'link1'; const data: CustomLink = { source: 'node1', diff --git a/packages/joint-react/src/state/__tests__/state-sync.test.ts b/packages/joint-react/src/state/__tests__/state-sync.test.ts index 939b8855ad..3401bf5f13 100644 --- a/packages/joint-react/src/state/__tests__/state-sync.test.ts +++ b/packages/joint-react/src/state/__tests__/state-sync.test.ts @@ -4,8 +4,8 @@ import { DEFAULT_CELL_NAMESPACE, type GraphStoreSnapshot } from '../../store/gra import { stateSync } from '../state-sync'; import { updateGraph } from '../update-graph'; import { createState } from '../../utils/create-state'; -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 { defaultMapDataToElementAttributes, defaultMapDataToLinkAttributes, @@ -20,7 +20,7 @@ import { Scheduler } from '../../utils/scheduler'; import type { GraphSchedulerData } from '../../types/scheduler.types'; // Helper to create ElementToGraphOptions -function createElementToGraphOptions( +function createElementToGraphOptions( id: string, element: E, graph: dia.Graph @@ -34,7 +34,7 @@ function createElementToGraphOptions( } // Helper to create LinkToGraphOptions -function _createLinkToGraphOptions( +function _createLinkToGraphOptions( id: string, data: L, graph: dia.Graph @@ -83,12 +83,12 @@ describe('stateSync', () => { it('should sync elements from state to graph on initialization', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, '2': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -106,16 +106,16 @@ describe('stateSync', () => { it('should sync links from state to graph on initialization', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, '2': { width: 100, height: 100, type: 'ReactElement' }, }; - const initialLinks: Record = { + const initialLinks: Record = { link1: { source: '1', target: '2' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: initialLinks }), name: 'elements', }); @@ -134,7 +134,7 @@ describe('stateSync', () => { it('should schedule element updates when graph element is added', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -144,7 +144,7 @@ describe('stateSync', () => { // Track state updates via scheduler flush setFlushCallback((data) => { if (data.elementsToUpdate) { - const newElements: Record = { ...state.getSnapshot().elements }; + const newElements: Record = { ...state.getSnapshot().elements }; for (const [id, element] of data.elementsToUpdate) { newElements[id] = element; } @@ -173,11 +173,11 @@ describe('stateSync', () => { it('should schedule element deletion when graph element is removed', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -215,12 +215,12 @@ describe('stateSync', () => { it('should schedule link updates when graph link is added', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, '2': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -261,11 +261,11 @@ describe('stateSync', () => { it('should update graph when state elements change', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -294,12 +294,12 @@ describe('stateSync', () => { it('should remove graph elements when state elements are removed', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, '2': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -329,7 +329,7 @@ describe('stateSync', () => { it('should not trigger state update when graph is updated from state (isUpdateFromReact flag)', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -374,7 +374,7 @@ describe('stateSync', () => { graph.syncCells(elementItems, { remove: true }); // Create empty store - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -419,11 +419,11 @@ describe('stateSync', () => { graph.syncCells(elementItems, { remove: true }); // Create store with different elements - const storeElements: Record = { + const storeElements: Record = { 'store-element': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: storeElements, links: {} }), name: 'elements', }); @@ -450,7 +450,7 @@ describe('stateSync', () => { it('should properly cleanup all listeners on cleanup()', () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -477,12 +477,12 @@ describe('stateSync', () => { it('should handle graph reset correctly', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, '2': { width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -540,7 +540,7 @@ describe('stateSync - comprehensive edge cases', () => { it('should batch multiple element updates into single scheduler call', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -570,7 +570,7 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle rapid add/remove cycles', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -619,11 +619,11 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle element position updates', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { x: 0, y: 0, width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -632,7 +632,7 @@ describe('stateSync - comprehensive edge cases', () => { setFlushCallback((data) => { if (data.elementsToUpdate) { - const newElements: Record = { ...state.getSnapshot().elements }; + const newElements: Record = { ...state.getSnapshot().elements }; for (const [id, element] of data.elementsToUpdate) { newElements[id] = element; } @@ -656,11 +656,11 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle element resize updates', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { x: 0, y: 0, width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -669,7 +669,7 @@ describe('stateSync - comprehensive edge cases', () => { setFlushCallback((data) => { if (data.elementsToUpdate) { - const newElements: Record = { ...state.getSnapshot().elements }; + const newElements: Record = { ...state.getSnapshot().elements }; for (const [id, element] of data.elementsToUpdate) { newElements[id] = element; } @@ -693,17 +693,17 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle link source/target updates', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, '2': { width: 100, height: 100, type: 'ReactElement' }, '3': { width: 100, height: 100, type: 'ReactElement' }, }; - const initialLinks: Record = { + const initialLinks: Record = { link1: { source: '1', target: '2' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: initialLinks }), name: 'elements', }); @@ -712,7 +712,7 @@ describe('stateSync - comprehensive edge cases', () => { setFlushCallback((data) => { if (data.linksToUpdate) { - const newLinks: Record = { ...state.getSnapshot().links }; + const newLinks: Record = { ...state.getSnapshot().links }; for (const [id, link] of data.linksToUpdate) { newLinks[id] = link; } @@ -738,7 +738,7 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle empty graph gracefully', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -757,7 +757,7 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle removing non-existent elements gracefully', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -779,7 +779,7 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle multiple cleanup calls gracefully', () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const state = createState>({ + const state = createState>({ newState: () => ({ elements: {}, links: {} }), name: 'elements', }); @@ -800,11 +800,11 @@ describe('stateSync - comprehensive edge cases', () => { it('should handle simultaneous graph and state updates', async () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const initialElements: Record = { + const initialElements: Record = { '1': { x: 0, y: 0, width: 100, height: 100, type: 'ReactElement' }, }; - const state = createState>({ + const state = createState>({ newState: () => ({ elements: initialElements, links: {} }), name: 'elements', }); @@ -813,7 +813,7 @@ describe('stateSync - comprehensive edge cases', () => { setFlushCallback((data) => { if (data.elementsToUpdate) { - const newElements: Record = { ...state.getSnapshot().elements }; + const newElements: Record = { ...state.getSnapshot().elements }; for (const [id, element] of data.elementsToUpdate) { newElements[id] = element; } @@ -848,7 +848,7 @@ describe('updateGraph', () => { it('should update graph when elements differ', () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const elements: Record = { + const elements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, }; @@ -871,7 +871,7 @@ describe('updateGraph', () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); // Include x and y as they are returned by defaultMapElementAttributesToData - const elements: Record = { + const elements: Record = { '1': { width: 100, height: 100, x: 0, y: 0, type: 'ReactElement' }, }; @@ -910,7 +910,7 @@ describe('updateGraph', () => { it('should return false when graph has active batch', () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const elements: Record = { + const elements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, }; @@ -937,7 +937,7 @@ describe('updateGraph', () => { it('should use isUpdateFromReact flag when syncing', () => { const graph = new dia.Graph({}, { cellNamespace: DEFAULT_CELL_NAMESPACE }); - const elements: Record = { + const elements: Record = { '1': { width: 100, height: 100, type: 'ReactElement' }, }; diff --git a/packages/joint-react/src/state/data-mapping/convert-labels.ts b/packages/joint-react/src/state/data-mapping/convert-labels.ts index 39578787e2..7151c9429a 100644 --- a/packages/joint-react/src/state/data-mapping/convert-labels.ts +++ b/packages/joint-react/src/state/data-mapping/convert-labels.ts @@ -1,15 +1,15 @@ import { type dia } from '@joint/core'; -import type { GraphLinkLabel } from '../../types/link-types'; +import type { FlatLinkLabel } from '../../types/link-types'; import { defaultLinkTheme, type LinkTheme } from '../../theme/link-theme'; /** - * Converts a simplified GraphLinkLabel into a JointJS dia.Link.Label + * Converts a simplified FlatLinkLabel into a JointJS dia.Link.Label * using the ReactLink's defaultLabel selectors (labelText, labelBody). * @param label - The simplified label definition * @param theme - The link theme providing label styling defaults * @returns The full JointJS label definition */ -export function convertLabel(label: GraphLinkLabel, theme: LinkTheme = defaultLinkTheme): dia.Link.Label { +export function convertLabel(label: FlatLinkLabel, theme: LinkTheme = defaultLinkTheme): dia.Link.Label { const { text, position = theme.labelPosition, diff --git a/packages/joint-react/src/state/data-mapping/convert-ports.ts b/packages/joint-react/src/state/data-mapping/convert-ports.ts index f701d3bca9..1e07af42ca 100644 --- a/packages/joint-react/src/state/data-mapping/convert-ports.ts +++ b/packages/joint-react/src/state/data-mapping/convert-ports.ts @@ -1,13 +1,13 @@ import { type dia } from '@joint/core'; -import type { GraphElementPort } from '../../types/element-types'; +import type { FlatElementPort } from '../../types/element-types'; import { defaultElementTheme } from '../../theme/element-theme'; /** - * Converts a simplified GraphElementPort to a full JointJS port definition. + * Converts a simplified FlatElementPort to a full JointJS port definition. * @param port - The simplified port definition * @returns The full JointJS port definition */ -function convertPort(port: GraphElementPort): dia.Element.Port { +function convertPort(port: FlatElementPort): dia.Element.Port { const { id, cx, @@ -101,11 +101,11 @@ export function createPortDefaults(): { groups: Record; items: dia.Element.Port[]; } { diff --git a/packages/joint-react/src/state/data-mapping/element-mapper.ts b/packages/joint-react/src/state/data-mapping/element-mapper.ts index 8836d4634c..f63cc6b62a 100644 --- a/packages/joint-react/src/state/data-mapping/element-mapper.ts +++ b/packages/joint-react/src/state/data-mapping/element-mapper.ts @@ -1,5 +1,5 @@ import { type dia } from '@joint/core'; -import type { GraphElement } from '../../types/element-types'; +import type { FlatElementData } from '../../types/element-types'; import { REACT_TYPE } from '../../models/react-element'; import type { ElementToGraphOptions, @@ -28,7 +28,7 @@ import { resolveCellDefaults } from './resolve-cell-defaults'; * @param options - The element id and data to convert * @returns The JointJS cell JSON attributes */ -export function defaultMapDataToElementAttributes( +export function defaultMapDataToElementAttributes( options: Pick, 'id' | 'data'> ): dia.Cell.JSON { const { id, data } = options; @@ -102,7 +102,7 @@ export function defaultMapDataToElementAttributes( * @param options - The JointJS cell and optional previous data for shape preservation * @returns The flat element data */ -export function defaultMapElementAttributesToData( +export function defaultMapElementAttributesToData( options: Pick, 'cell' | 'previousData'> ): Element { const { cell } = options; diff --git a/packages/joint-react/src/state/data-mapping/link-attributes.ts b/packages/joint-react/src/state/data-mapping/link-attributes.ts index 85cec39f58..5300cc7adf 100644 --- a/packages/joint-react/src/state/data-mapping/link-attributes.ts +++ b/packages/joint-react/src/state/data-mapping/link-attributes.ts @@ -1,5 +1,6 @@ import type { anchors, attributes, connectionPoints, dia } from '@joint/core'; -import type { GraphLinkEnd } from '../../types/link-types'; +import type { CellId } from '../../types/cell-id'; +import type { FlatLinkEnd } from '../../types/link-types'; import type { MarkerPreset } from '../../theme/markers'; import { resolveMarker } from '../../theme/markers'; @@ -21,7 +22,7 @@ export interface LinkEndAttributeOptions { * @param options */ export function toLinkEndAttribute( - end: GraphLinkEnd, + end: FlatLinkEnd, options?: LinkEndAttributeOptions, ): dia.Link.EndJSON { const base = (typeof end === 'string' ? { id: end } : end) as dia.Link.EndJSON; @@ -39,7 +40,7 @@ export function toLinkEndAttribute( } export interface LinkEndData { - end: GraphLinkEnd; + end: FlatLinkEnd; port?: string; anchor?: anchors.AnchorJSON; connectionPoint?: connectionPoints.ConnectionPointJSON; @@ -60,9 +61,9 @@ export interface LinkEndData { export function toLinkEndData(end: dia.Link.EndJSON): LinkEndData { const { port, anchor, connectionPoint, magnet } = end; - const endData: GraphLinkEnd = 'x' in end && 'y' in end + const endData: FlatLinkEnd = 'x' in end && 'y' in end ? { x: end.x!, y: end.y! } - : end.id!; + : end.id as CellId; const result: LinkEndData = { end: endData }; if (port !== undefined) result.port = port; diff --git a/packages/joint-react/src/state/data-mapping/link-mapper.ts b/packages/joint-react/src/state/data-mapping/link-mapper.ts index 89f76de6d4..9f9eac6b05 100644 --- a/packages/joint-react/src/state/data-mapping/link-mapper.ts +++ b/packages/joint-react/src/state/data-mapping/link-mapper.ts @@ -1,5 +1,5 @@ import { type dia } from '@joint/core'; -import type { GraphLink } from '../../types/link-types'; +import type { FlatLinkData } from '../../types/link-types'; import { defaultLinkTheme, type LinkTheme } from '../../theme/link-theme'; import { REACT_LINK_TYPE } from '../../models/react-link'; import type { @@ -31,7 +31,7 @@ import { resolveCellDefaults } from './resolve-cell-defaults'; * @param options - The link id, data, and optional theme to convert * @returns The JointJS cell JSON attributes */ -export function defaultMapDataToLinkAttributes( +export function defaultMapDataToLinkAttributes( options: Pick, 'id' | 'data'> & { readonly theme?: LinkTheme } ): dia.Cell.JSON { const { id, data, theme = defaultLinkTheme } = options; @@ -133,7 +133,7 @@ export function defaultMapDataToLinkAttributes( * @param options - The JointJS cell and optional previous data for shape preservation * @returns The flat link data */ -export function defaultMapLinkAttributesToData( +export function defaultMapLinkAttributesToData( options: Pick, 'cell' | 'previousData'> ): Link { const { cell } = options; diff --git a/packages/joint-react/src/state/graph-state-selectors.ts b/packages/joint-react/src/state/graph-state-selectors.ts index a33d7f1eba..d73fe16853 100644 --- a/packages/joint-react/src/state/graph-state-selectors.ts +++ b/packages/joint-react/src/state/graph-state-selectors.ts @@ -1,6 +1,6 @@ import { type dia } from '@joint/core'; -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 type { LinkTheme } from '../theme/link-theme'; /** @@ -10,43 +10,43 @@ export interface ToLinkAttributesOptions { readonly theme?: LinkTheme; } -export interface ElementToGraphOptions { +export interface ElementToGraphOptions { readonly id: string; readonly data: ElementData; readonly graph: dia.Graph; readonly toAttributes: (data: ElementData) => dia.Cell.JSON; } -export interface GraphToElementOptions { +export interface GraphToElementOptions { readonly id: string; readonly cell: dia.Element; readonly previousData?: ElementData; readonly graph: dia.Graph; - readonly toData: () => GraphElement; + readonly toData: () => FlatElementData; } -export interface LinkToGraphOptions { +export interface LinkToGraphOptions { readonly id: string; readonly data: LinkData; readonly graph: dia.Graph; readonly toAttributes: (data: LinkData, options?: ToLinkAttributesOptions) => dia.Cell.JSON; } -export interface GraphToLinkOptions { +export interface GraphToLinkOptions { readonly id: string; readonly cell: dia.Link; readonly previousData?: LinkData; readonly graph: dia.Graph; - readonly toData: () => GraphLink; + readonly toData: () => FlatLinkData; } -export type LinkFromGraphSelector = ( - options: GraphToLinkOptions -) => Link; +export type LinkFromGraphSelector = ( + options: GraphToLinkOptions +) => LinkData; -export interface GraphStateSelectors { - readonly mapDataToElementAttributes?: (options: ElementToGraphOptions) => dia.Cell.JSON; - readonly mapDataToLinkAttributes?: (options: LinkToGraphOptions) => dia.Cell.JSON; - readonly mapElementAttributesToData?: (options: GraphToElementOptions) => Element; - readonly mapLinkAttributesToData?: (options: GraphToLinkOptions) => Link; +export interface GraphStateSelectors { + readonly mapDataToElementAttributes?: (options: ElementToGraphOptions) => dia.Cell.JSON; + readonly mapDataToLinkAttributes?: (options: LinkToGraphOptions) => dia.Cell.JSON; + readonly mapElementAttributesToData?: (options: GraphToElementOptions) => ElementData; + readonly mapLinkAttributesToData?: (options: GraphToLinkOptions) => LinkData; } diff --git a/packages/joint-react/src/state/state-sync.ts b/packages/joint-react/src/state/state-sync.ts index 4396b4b4da..17ee800f32 100644 --- a/packages/joint-react/src/state/state-sync.ts +++ b/packages/joint-react/src/state/state-sync.ts @@ -2,8 +2,9 @@ import type { GraphStoreSnapshot } from '../store/graph-store'; import { listenToCellChange, type OnChangeOptions } from '../utils/cell/listen-to-cell-change'; import { removeDeepReadOnly, type ExternalStoreLike } from '../utils/create-state'; import type { dia } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; -import type { GraphLink } from '../types/link-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatElementData } from '../types/element-types'; +import type { FlatLinkData } from '../types/link-types'; import type { ElementToGraphOptions, GraphToElementOptions, @@ -27,18 +28,18 @@ export interface StateSync { interface StateSyncOptions< Graph extends dia.Graph, - Element extends GraphElement, - Link extends GraphLink, -> extends GraphStateSelectors { + ElementData = FlatElementData, + LinkData = FlatLinkData, +> extends GraphStateSelectors { readonly graph: Graph; - readonly store: Omit>, 'setState'>; + readonly store: Omit>, 'setState'>; readonly scheduler: Scheduler; readonly graphToElementSelector?: ( - options: GraphToElementOptions & { readonly graph: Graph } - ) => Element; + options: GraphToElementOptions & { readonly graph: Graph } + ) => ElementData; readonly graphToLinkSelector?: ( - options: GraphToLinkOptions & { readonly graph: Graph } - ) => Link; + options: GraphToLinkOptions & { readonly graph: Graph } + ) => LinkData; /** * Callback invoked after the graph is successfully updated from the store. * Used to trigger layout updates when changes come from React state (e.g., useCellActions). @@ -80,24 +81,25 @@ function mapDelete(map: Map | undefined, key: K): Map { */ function mapCellsToData< Graph extends dia.Graph, - Element extends GraphElement, - Link extends GraphLink, + ElementData = FlatElementData, + LinkData = FlatLinkData, >( cells: dia.Cell[], graph: Graph, - elementSelector: (options: GraphToElementOptions & { readonly graph: Graph }) => Element, - linkSelector: (options: GraphToLinkOptions & { readonly graph: Graph }) => Link, - previousElements?: Record, - previousLinks?: Record -): { elements: Map; links: Map } { - const elements = new Map(); - const links = new Map(); + elementSelector: (options: GraphToElementOptions & { readonly graph: Graph }) => ElementData, + linkSelector: (options: GraphToLinkOptions & { readonly graph: Graph }) => LinkData, + previousElements?: Record, + previousLinks?: Record +): { elements: Map; links: Map } { + const elements = new Map(); + const links = new Map(); for (const cell of cells) { + const cellId = cell.id as CellId; if (cell.isElement()) { - elements.set(cell.id, mapGraphElement(cell, graph, elementSelector, previousElements?.[cell.id])); + elements.set(cellId, mapGraphElement(cell, graph, elementSelector, previousElements?.[cellId])); } else if (cell.isLink()) { - links.set(cell.id, mapGraphLink(cell, graph, linkSelector, previousLinks?.[cell.id])); + links.set(cellId, mapGraphLink(cell, graph, linkSelector, previousLinks?.[cellId])); } } @@ -112,9 +114,9 @@ function mapCellsToData< */ function findIdsToDelete( currentIds: string[], - newIds: Set, - existingDeletes: Map | undefined -): Map { + newIds: Set, + existingDeletes: Map | undefined +): Map { const toDelete = new Map(existingDeletes); for (const id of currentIds) { if (!newIds.has(id)) { @@ -131,9 +133,9 @@ function findIdsToDelete( */ export function stateSync< Graph extends dia.Graph, - Element extends GraphElement, - Link extends GraphLink, ->(options: StateSyncOptions): StateSync { + ElementData = FlatElementData, + LinkData = FlatLinkData, +>(options: StateSyncOptions): StateSync { const { graph, store, @@ -144,37 +146,38 @@ export function stateSync< } = options; const elementSelector = (options.graphToElementSelector ?? defaultMapElementAttributesToData) as ( - options: GraphToElementOptions & { readonly graph: Graph } - ) => Element; + options: GraphToElementOptions & { readonly graph: Graph } + ) => ElementData; const linkSelector = (options.graphToLinkSelector ?? defaultMapLinkAttributesToData) as ( - options: GraphToLinkOptions & { readonly graph: Graph } - ) => Link; + options: GraphToLinkOptions & { readonly graph: Graph } + ) => LinkData; // --- Scheduling --- const scheduleCellUpdate = (cell: dia.Cell) => { scheduler.scheduleData((data) => { + const id = cell.id as CellId; const snapshot = store.getSnapshot(); if (cell.isElement()) { - const previousData = snapshot.elements[cell.id] as Element | undefined; + const previousData = snapshot.elements[id] as ElementData | undefined; return { ...data, elementsToUpdate: mapSet( data.elementsToUpdate, - cell.id, - mapGraphElement(cell, graph, elementSelector, previousData) + id, + mapGraphElement(cell, graph, elementSelector, previousData) as FlatElementData ), }; } if (cell.isLink()) { - const previousData = snapshot.links[cell.id] as Link | undefined; + const previousData = snapshot.links[id] as LinkData | undefined; return { ...data, linksToUpdate: mapSet( data.linksToUpdate, - cell.id, - mapGraphLink(cell, graph, linkSelector, previousData) + id, + mapGraphLink(cell, graph, linkSelector, previousData) as FlatLinkData ), }; } @@ -184,18 +187,19 @@ export function stateSync< const scheduleCellDelete = (cell: dia.Cell) => { scheduler.scheduleData((data) => { + const id = cell.id as CellId; if (cell.isElement()) { return { ...data, - elementsToUpdate: mapDelete(data.elementsToUpdate, cell.id), - elementsToDelete: mapSet(data.elementsToDelete, cell.id, true), + elementsToUpdate: mapDelete(data.elementsToUpdate, id), + elementsToDelete: mapSet(data.elementsToDelete, id, true), }; } if (cell.isLink()) { return { ...data, - linksToUpdate: mapDelete(data.linksToUpdate, cell.id), - linksToDelete: mapSet(data.linksToDelete, cell.id, true), + linksToUpdate: mapDelete(data.linksToUpdate, id), + linksToDelete: mapSet(data.linksToDelete, id, true), }; } return data; @@ -210,14 +214,14 @@ export function stateSync< graph, elementSelector, linkSelector, - snapshot.elements as Record, - snapshot.links as Record + snapshot.elements as Record, + snapshot.links as Record ); return { ...data, - elementsToUpdate: elements, - linksToUpdate: links, + elementsToUpdate: elements as Map, + linksToUpdate: links as Map, elementsToDelete: findIdsToDelete( Object.keys(snapshot.elements), new Set(elements.keys()), @@ -240,13 +244,13 @@ export function stateSync< graph, elementSelector, linkSelector, - snapshot.elements as Record, - snapshot.links as Record + snapshot.elements as Record, + snapshot.links as Record ); return { ...data, - elementsToUpdate: new Map([...(data.elementsToUpdate ?? []), ...elements]), - linksToUpdate: new Map([...(data.linksToUpdate ?? []), ...links]), + elementsToUpdate: new Map([...(data.elementsToUpdate ?? []), ...(elements as Map)]), + linksToUpdate: new Map([...(data.linksToUpdate ?? []), ...(links as Map)]), }; }); }; @@ -287,15 +291,15 @@ export function stateSync< const wasUpdated = updateGraph({ graph, - elements: elements as Record, - links: links as Record, + elements: elements as Record, + links: links as Record, graphToElementSelector: elementSelector, graphToLinkSelector: linkSelector, mapDataToElementAttributes: mapDataToElementAttributes as ( - options: ElementToGraphOptions & { readonly graph: Graph } + options: ElementToGraphOptions & { readonly graph: Graph } ) => dia.Cell.JSON, mapDataToLinkAttributes: mapDataToLinkAttributes as ( - options: LinkToGraphOptions & { readonly graph: Graph } + options: LinkToGraphOptions & { readonly graph: Graph } ) => dia.Cell.JSON, isUpdateFromReact: true, }); diff --git a/packages/joint-react/src/state/update-graph.ts b/packages/joint-react/src/state/update-graph.ts index 18f72b40f8..a0fe8817b3 100644 --- a/packages/joint-react/src/state/update-graph.ts +++ b/packages/joint-react/src/state/update-graph.ts @@ -1,6 +1,7 @@ import type { dia } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; -import type { GraphLink } from '../types/link-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatElementData } from '../types/element-types'; +import type { FlatLinkData } from '../types/link-types'; import type { ElementToGraphOptions, GraphToElementOptions, @@ -23,30 +24,30 @@ import { fastElementArrayEqual, isPositionOnlyUpdate } from '../utils/fast-equal */ export interface UpdateGraphOptions< Graph extends dia.Graph, - Element extends GraphElement, - Link extends GraphLink, + ElementData = FlatElementData, + LinkData = FlatLinkData, > { /** The JointJS graph instance to update */ readonly graph: Graph; /** The elements to sync to the graph (Record keyed by cell ID) */ - readonly elements: Record; + readonly elements: Record; /** The links to sync to the graph (Record keyed by cell ID) */ - readonly links: Record; + readonly links: Record; /** Selector to convert graph elements to Element format for comparison */ readonly graphToElementSelector: ( - options: GraphToElementOptions & { readonly graph: Graph } - ) => Element; + options: GraphToElementOptions & { readonly graph: Graph } + ) => ElementData; /** Selector to convert graph links to Link format for comparison */ readonly graphToLinkSelector: ( - options: GraphToLinkOptions & { readonly graph: Graph } - ) => Link; + options: GraphToLinkOptions & { readonly graph: Graph } + ) => LinkData; /** Selector to convert Element to JointJS Cell JSON format */ readonly mapDataToElementAttributes: ( - options: ElementToGraphOptions & { readonly graph: Graph } + options: ElementToGraphOptions & { readonly graph: Graph } ) => dia.Cell.JSON; /** Selector to convert Link to JointJS Cell JSON format */ readonly mapDataToLinkAttributes: ( - options: LinkToGraphOptions & { readonly graph: Graph } + options: LinkToGraphOptions & { readonly graph: Graph } ) => dia.Cell.JSON; readonly isUpdateFromReact?: boolean; @@ -59,12 +60,12 @@ export interface UpdateGraphOptions< * @param selector * @param previousData */ -export function mapGraphElement( +export function mapGraphElement( cell: dia.Element, graph: Graph, - selector: (options: GraphToElementOptions & { readonly graph: Graph }) => Element, - previousData?: Element -): Element { + selector: (options: GraphToElementOptions & { readonly graph: Graph }) => ElementData, + previousData?: ElementData +): ElementData { const id = cell.id as string; return selector({ id, cell, graph, previousData, @@ -79,12 +80,12 @@ export function mapGraphElement( +export function mapGraphLink( cell: dia.Link, graph: Graph, - selector: (options: GraphToLinkOptions & { readonly graph: Graph }) => Link, - previousData?: Link -): Link { + selector: (options: GraphToLinkOptions & { readonly graph: Graph }) => LinkData, + previousData?: LinkData +): LinkData { const id = cell.id as string; return selector({ id, cell, graph, previousData, @@ -100,11 +101,11 @@ export function mapGraphLink( * @param elements * @param links */ -function isGraphInSync( - graphElements: Element[], - graphLinks: Link[], - elements: Element[], - links: Link[] +function isGraphInSync( + graphElements: ElementData[], + graphLinks: LinkData[], + elements: ElementData[], + links: LinkData[] ): boolean { // Fast path: Check if arrays have same length first if (elements.length !== graphElements.length || links.length !== graphLinks.length) { @@ -112,14 +113,18 @@ function isGraphInSync( } // Position-only update: use fast equality check - if (isPositionOnlyUpdate(graphElements, elements)) { + if (isPositionOnlyUpdate(graphElements as FlatElementData[], elements as FlatElementData[])) { return ( - fastElementArrayEqual(elements, graphElements) && fastElementArrayEqual(links, graphLinks) + fastElementArrayEqual(elements as Array>, graphElements as Array>) && + fastElementArrayEqual(links as Array>, graphLinks as Array>) ); } // General equality check - return fastElementArrayEqual(elements, graphElements) && fastElementArrayEqual(links, graphLinks); + return ( + fastElementArrayEqual(elements as Array>, graphElements as Array>) && + fastElementArrayEqual(links as Array>, graphLinks as Array>) + ); } /** @@ -133,9 +138,9 @@ function isGraphInSync( */ export function updateGraph< Graph extends dia.Graph, - Element extends GraphElement, - Link extends GraphLink, ->(options: UpdateGraphOptions): boolean { + ElementData = FlatElementData, + LinkData = FlatLinkData, +>(options: UpdateGraphOptions): boolean { const { graph, elements: elementsRecord, @@ -175,7 +180,7 @@ export function updateGraph< const elementItems = Object.entries(elementsRecord).map(([id, data]) => { const attributes = mapDataToElementAttributes({ id, data, graph, - toAttributes: (newData) => defaultMapDataToElementAttributes({ id, data: newData }), + toAttributes: (newData) => defaultMapDataToElementAttributes({ id, data: newData as FlatElementData }), }); if ('id' in attributes && attributes.id !== id) { throw new Error( @@ -190,7 +195,7 @@ export function updateGraph< const linkItems = Object.entries(linksRecord).map(([id, data]) => { const attributes = mapDataToLinkAttributes({ id, data, graph, - toAttributes: (newData, attributeOptions) => defaultMapDataToLinkAttributes({ id, data: newData, ...attributeOptions }), + toAttributes: (newData, attributeOptions) => defaultMapDataToLinkAttributes({ id, data: newData as FlatLinkData, ...attributeOptions }), }); if ('id' in attributes && attributes.id !== id) { throw new Error( diff --git a/packages/joint-react/src/store/__tests__/create-elements-size-observer.test.ts b/packages/joint-react/src/store/__tests__/create-elements-size-observer.test.ts index 45bcfb7d9e..6bdfd57f67 100644 --- a/packages/joint-react/src/store/__tests__/create-elements-size-observer.test.ts +++ b/packages/joint-react/src/store/__tests__/create-elements-size-observer.test.ts @@ -2,8 +2,9 @@ /* eslint-disable sonarjs/no-nested-functions */ /* eslint-disable @typescript-eslint/no-require-imports */ import type { dia } from '@joint/core'; +import type { CellId } from '../../types/cell-id'; import type { GraphStoreSnapshot } from '../graph-store'; -import type { GraphElement } from '../../types/element-types'; +import type { FlatElementData } from '../../types/element-types'; import type { GraphStoreObserver } from '../create-elements-size-observer'; // Mock ResizeObserver for testing @@ -85,7 +86,7 @@ describe('createElementsSizeObserver', () => { let mockOnBatchUpdate: jest.Mock; let mockGetCellTransform: jest.Mock; let mockGetPublicSnapshot: jest.Mock; - let mockElements: Record; + let mockElements: Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any let createElementsSizeObserver: any; @@ -106,7 +107,7 @@ describe('createElementsSizeObserver', () => { }; mockOnBatchUpdate = jest.fn(); - mockGetCellTransform = jest.fn((id: dia.Cell.ID) => ({ + mockGetCellTransform = jest.fn((id: CellId) => ({ width: 1, height: 1, x: 0, diff --git a/packages/joint-react/src/store/__tests__/graph-store.test.ts b/packages/joint-react/src/store/__tests__/graph-store.test.ts index 8aad13dcc8..59d8b4d539 100644 --- a/packages/joint-react/src/store/__tests__/graph-store.test.ts +++ b/packages/joint-react/src/store/__tests__/graph-store.test.ts @@ -3,8 +3,8 @@ import { dia, shapes } from '@joint/core'; import { GraphStore } from '../graph-store'; import { ReactElement } from '../../models/react-element'; -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 { defaultMapDataToElementAttributes, defaultMapDataToLinkAttributes, @@ -40,7 +40,7 @@ describe('GraphStore', () => { }); it('should initialize with initialElements', () => { - const initialElements: Record = { + const initialElements: Record = { 'element-1': { x: 10, y: 20, @@ -58,7 +58,7 @@ describe('GraphStore', () => { }); it('should initialize with initialLinks', () => { - const initialLinks: Record = { + const initialLinks: Record = { 'link-1': { source: 'element-1', target: 'element-2', type: 'standard.Link' }, }; const store = new GraphStore({ initialLinks }); @@ -68,7 +68,7 @@ describe('GraphStore', () => { }); it('should initialize with both initialElements and initialLinks', () => { - const initialElements: Record = { + const initialElements: Record = { 'element-1': { x: 10, y: 20, @@ -77,7 +77,7 @@ describe('GraphStore', () => { type: 'ReactElement', }, }; - const initialLinks: Record = { + const initialLinks: Record = { 'link-1': { source: 'element-1', target: 'element-2', type: 'standard.Link' }, }; const store = new GraphStore({ initialElements, initialLinks }); @@ -107,10 +107,10 @@ describe('GraphStore', () => { }); it('should use custom selectors when provided', () => { - const customElementToGraph = jest.fn((options: ElementToGraphOptions) => { + const customElementToGraph = jest.fn((options: ElementToGraphOptions) => { return defaultMapDataToElementAttributes(options); }); - const customLinkToGraph = jest.fn((options: LinkToGraphOptions) => { + const customLinkToGraph = jest.fn((options: LinkToGraphOptions) => { return defaultMapDataToLinkAttributes(options); }); @@ -121,7 +121,7 @@ describe('GraphStore', () => { // Add an element to trigger the selector const id = 'test-element'; - const data: GraphElement = { + const data: FlatElementData = { x: 10, y: 20, width: 100, @@ -150,7 +150,7 @@ describe('GraphStore', () => { graph.addCell(existingElement); const cellCountBefore = graph.getCells().length; - const initialElements: Record = { + const initialElements: Record = { 'new-element': { x: 10, y: 20, @@ -361,7 +361,7 @@ describe('GraphStore', () => { it('should return true for measured nodes', () => { const store = new GraphStore({}); const id = 'measured-element'; - const data: GraphElement = { + const data: FlatElementData = { x: 10, y: 20, width: 100, @@ -388,7 +388,7 @@ describe('GraphStore', () => { it('should register a node for measurement and return cleanup', () => { const store = new GraphStore({}); const id = 'measured-element'; - const element: GraphElement = { + const element: FlatElementData = { x: 10, y: 20, width: 100, @@ -519,7 +519,7 @@ describe('GraphStore', () => { it('should sync state changes to graph', (done) => { const store = new GraphStore({}); const id = 'sync-element'; - const element: GraphElement = { + const element: FlatElementData = { x: 10, y: 20, width: 100, diff --git a/packages/joint-react/src/store/batch-cache.ts b/packages/joint-react/src/store/batch-cache.ts index f64ed5a49b..c0bf874440 100644 --- a/packages/joint-react/src/store/batch-cache.ts +++ b/packages/joint-react/src/store/batch-cache.ts @@ -1,10 +1,10 @@ -import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import type { ClearViewCacheEntry } from './clear-view'; /** * Generic batch cache for collecting updates before flushing. * Provides a unified pattern for link, and other batched updates. - * @template K - Key type (usually dia.Cell.ID) + * @template K - Key type (usually CellId) * @template V - Value type (cache entry) */ export class BatchCache { @@ -100,8 +100,8 @@ export class BatchCache { * @param scheduler - Function to call when updates are scheduled * @returns A new BatchCache instance */ -function createCellCache(scheduler: () => void): BatchCache { - return new BatchCache({ +function createCellCache(scheduler: () => void): BatchCache { + return new BatchCache({ scheduler, defaultEntry: () => ({}) as V, }); @@ -113,6 +113,6 @@ function createCellCache(scheduler: () => void): BatchCache void -): BatchCache { +): BatchCache { return createCellCache(scheduler); } diff --git a/packages/joint-react/src/store/clear-view.ts b/packages/joint-react/src/store/clear-view.ts index f7a8d978bf..a912238dab 100644 --- a/packages/joint-react/src/store/clear-view.ts +++ b/packages/joint-react/src/store/clear-view.ts @@ -1,4 +1,5 @@ import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; /** * Cache entry for batched clearView updates. @@ -54,7 +55,7 @@ export function mergeClearViewValidators( */ export function shouldClearLink( link: dia.Link, - cellId: dia.Cell.ID, + cellId: CellId, onValidateLink?: (link: dia.Link) => boolean ): boolean { const target = link.target(); @@ -73,7 +74,7 @@ export function shouldClearLink( export function clearConnectedLinkViews( paper: dia.Paper, graph: dia.Graph, - cellId: dia.Cell.ID, + cellId: CellId, onValidateLink?: (link: dia.Link) => boolean ): void { const cell = graph.getCell(cellId); @@ -110,7 +111,7 @@ export function clearConnectedLinkViews( export function executeClearViewForCell( papers: Iterable<{ readonly paper?: dia.Paper }>, graph: dia.Graph, - cellId: dia.Cell.ID, + cellId: CellId, onValidateLink?: (link: dia.Link) => boolean ): void { for (const paperStore of papers) { diff --git a/packages/joint-react/src/store/create-elements-size-observer.ts b/packages/joint-react/src/store/create-elements-size-observer.ts index 44835d4a93..e4e0133318 100644 --- a/packages/joint-react/src/store/create-elements-size-observer.ts +++ b/packages/joint-react/src/store/create-elements-size-observer.ts @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/cognitive-complexity */ import type { dia } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatElementData } from '../types/element-types'; import type { GraphStoreSnapshot, NodeLayout } from './graph-store'; import type { MarkDeepReadOnly } from '../utils/create-state'; @@ -26,7 +27,7 @@ export interface NodeLayoutOptionalXY { export interface TransformOptions extends Required { /** The JointJS element instance */ readonly element: dia.Element; - readonly id: dia.Cell.ID; + readonly id: CellId; } /** @@ -44,7 +45,7 @@ export interface SetMeasuredNodeOptions { /** Optional callback to handle size updates before they're applied */ readonly transform?: OnTransformElement; /** The ID of the cell in the graph that corresponds to this DOM element */ - readonly id: dia.Cell.ID; + readonly id: CellId; } interface ObservedElement { @@ -70,12 +71,12 @@ interface Options { readonly resizeObserverOptions?: ResizeObserverOptions; /** Function to get the current size of a cell from the graph */ readonly getCellTransform: ( - id: dia.Cell.ID + id: CellId ) => NodeLayoutOptionalXY & { element: dia.Element; angle: number }; /** Function to get the current public snapshot containing all elements */ readonly getPublicSnapshot: () => MarkDeepReadOnly; /** Callback function called when a batch of elements needs to be updated */ - readonly onBatchUpdate: (elements: Record) => void; + readonly onBatchUpdate: (elements: Record) => void; } /** @@ -99,7 +100,7 @@ export interface GraphStoreObserver { * @param id - The ID of the cell to check * @returns True if the node is being observed */ - readonly has: (id: dia.Cell.ID) => boolean; + readonly has: (id: CellId) => boolean; } /** @@ -115,12 +116,12 @@ function roundToTwoDecimals(value: number) { * Options for processing a single element's size change. */ interface ProcessSizeChangeOptions { - readonly cellId: dia.Cell.ID; + readonly cellId: CellId; readonly measuredWidth: number; readonly measuredHeight: number; readonly observedElement: ObservedElement | Partial; readonly getCellTransform: Options['getCellTransform']; - readonly updatedElements: Record; + readonly updatedElements: Record; } /** @@ -217,11 +218,11 @@ export function createElementsSizeObserver(options: Options): GraphStoreObserver onBatchUpdate, getPublicSnapshot, } = options; - const observedElementsByCellId = new Map(); - const cellIdByDomElement = new Map(); + const observedElementsByCellId = new Map(); + const cellIdByDomElement = new Map(); // Pending immediate measurements to batch - const pendingImmediateMeasurements = new Map(); + const pendingImmediateMeasurements = new Map(); let isImmediateBatchScheduled = false; /** @@ -234,8 +235,8 @@ export function createElementsSizeObserver(options: Options): GraphStoreObserver } const publicSnapshot = getPublicSnapshot(); - const elementsRecord = publicSnapshot.elements as Record; - const updatedElements: Record = { ...elementsRecord }; + const elementsRecord = publicSnapshot.elements as Record; + const updatedElements: Record = { ...elementsRecord }; let hasAnySizeChange = false; for (const [cellId, { width, height }] of pendingImmediateMeasurements) { @@ -269,7 +270,7 @@ export function createElementsSizeObserver(options: Options): GraphStoreObserver /** * Schedules an immediate measurement to be processed in the current microtask batch. */ - function scheduleImmediateMeasurement(cellId: dia.Cell.ID, width: number, height: number) { + function scheduleImmediateMeasurement(cellId: CellId, width: number, height: number) { pendingImmediateMeasurements.set(cellId, { width, height }); if (!isImmediateBatchScheduled) { @@ -284,8 +285,8 @@ export function createElementsSizeObserver(options: Options): GraphStoreObserver let hasAnySizeChange = false; const publicSnapshot = getPublicSnapshot(); // Convert Record to array for batch update (preserving compatibility) - const elementsRecord = publicSnapshot.elements as Record; - const updatedElements: Record = { ...elementsRecord }; + const elementsRecord = publicSnapshot.elements as Record; + const updatedElements: Record = { ...elementsRecord }; for (const entry of entries) { // We must be careful to not mutate the snapshot data. @@ -367,7 +368,7 @@ export function createElementsSizeObserver(options: Options): GraphStoreObserver isImmediateBatchScheduled = false; observer.disconnect(); }, - has(id: dia.Cell.ID) { + has(id: CellId) { return observedElementsByCellId.has(id); }, }; diff --git a/packages/joint-react/src/store/graph-store.ts b/packages/joint-react/src/store/graph-store.ts index 0161b089f9..cb13d9d65a 100644 --- a/packages/joint-react/src/store/graph-store.ts +++ b/packages/joint-react/src/store/graph-store.ts @@ -1,6 +1,7 @@ import { dia, shapes, util } from '@joint/core'; -import type { GraphLink } from '../types/link-types'; -import type { GraphElement } from '../types/element-types'; +import type { CellId } from '../types/cell-id'; +import type { FlatLinkData } from '../types/link-types'; +import type { FlatElementData } from '../types/element-types'; import type { AddPaperOptions, PaperStoreSnapshot } from './paper-store'; import { PaperStore } from './paper-store'; import { @@ -52,11 +53,11 @@ export type GraphState = State; * Public snapshot of the graph store containing elements and links. */ export interface GraphStoreSnapshot< - Element extends GraphElement = GraphElement, - Link extends GraphLink = GraphLink, + ElementData = FlatElementData, + LinkData = FlatLinkData, > { - readonly elements: Record; - readonly links: Record; + readonly elements: Record; + readonly links: Record; } /** @@ -85,8 +86,8 @@ export interface LinkLayout { * Snapshot containing layout data for all nodes and links (per paper). */ export interface GraphStoreLayoutSnapshot { - readonly elements: Record; - readonly links: Record>; + readonly elements: Record; + readonly links: Record>; readonly wasEverMeasured: boolean; } @@ -101,14 +102,14 @@ export interface GraphStoreInternalSnapshot { * Configuration options for creating a GraphStore instance. */ export interface GraphStoreOptions< - Element extends GraphElement = GraphElement, - Link extends GraphLink = GraphLink, -> extends GraphStateSelectors { + ElementData = FlatElementData, + LinkData = FlatLinkData, +> extends GraphStateSelectors { readonly graph?: dia.Graph; readonly cellNamespace?: unknown; readonly cellModel?: typeof dia.Cell; - readonly initialElements?: Record; - readonly initialLinks?: Record; + readonly initialElements?: Record; + readonly initialLinks?: Record; readonly externalStore?: ExternalGraphStore; } @@ -126,7 +127,7 @@ export class GraphStore { private observer: GraphStoreObserver; private stateSync: StateSync; - private clearViewCache: BatchCache; + private clearViewCache: BatchCache; private readonly scheduler: Scheduler; private paperUpdateCallbacks = new Set<() => void>(); private isGraphUpdateScheduled = false; @@ -134,20 +135,20 @@ export class GraphStore { private readonly graphToElementSelector: ( options: { readonly id: string; readonly cell: dia.Element; readonly graph: dia.Graph } & { - readonly previousData?: GraphElement; + readonly previousData?: FlatElementData; } - ) => GraphElement; + ) => FlatElementData; private readonly graphToLinkSelector: ( options: { readonly id: string; readonly cell: dia.Link; readonly graph: dia.Graph } & { - readonly previousData?: GraphLink; + readonly previousData?: FlatLinkData; } - ) => GraphLink; + ) => FlatLinkData; public readonly mapDataToElementAttributes: (options: { - readonly data: GraphElement; + readonly data: FlatElementData; readonly graph: dia.Graph; }) => dia.Cell.JSON; private readonly mapDataToLinkAttributes: (options: { - readonly data: GraphLink; + readonly data: FlatLinkData; readonly graph: dia.Graph; }) => dia.Cell.JSON; @@ -378,7 +379,7 @@ export class GraphStore { }); } - public updatePaperElementView(paperId: string, cellId: dia.Cell.ID, view: dia.ElementView) { + public updatePaperElementView(paperId: string, cellId: CellId, view: dia.ElementView) { this.updatePaperSnapshot(paperId, (current) => { const base = current ?? { paperElementViews: {} }; if (base.paperElementViews?.[cellId] === view) return base; @@ -386,7 +387,7 @@ export class GraphStore { }); } - public updatePaperLinkView(paperId: string, linkId: dia.Cell.ID, view: dia.LinkView) { + public updatePaperLinkView(paperId: string, linkId: CellId, view: dia.LinkView) { this.updatePaperSnapshot(paperId, (current) => { const base = current ?? { linkViews: {}, linksData: {} }; if (base.linkViews?.[linkId] === view) return base; @@ -425,7 +426,7 @@ export class GraphStore { * @param id - The ID of the node to check. * @returns True if the node is being observed, false otherwise. */ - public hasMeasuredNode = (id: dia.Cell.ID) => this.observer.has(id); + public hasMeasuredNode = (id: CellId) => this.observer.has(id); /** * Registers a node to be observed for size changes. The observer will call the provided callback with batches of size updates, which are then synced to the graph and trigger layout updates. * @param options - Configuration options for the measured node, including its ID and a callback to receive size updates. @@ -478,7 +479,7 @@ export class GraphStore { // --- ClearView API --- public scheduleClearView = (options: { - readonly cellId: dia.Cell.ID; + readonly cellId: CellId; readonly onValidateLink?: (link: dia.Link) => boolean; }) => { // check clear-view.ts for more info diff --git a/packages/joint-react/src/store/paper-store.ts b/packages/joint-react/src/store/paper-store.ts index e303a4e9da..9dd29101b2 100644 --- a/packages/joint-react/src/store/paper-store.ts +++ b/packages/joint-react/src/store/paper-store.ts @@ -1,8 +1,9 @@ import { dia, g } from '@joint/core'; +import type { CellId } from '../types/cell-id'; import type { OverWriteResult } from '../context'; import type { RenderElement, RenderLink } from '../components'; -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 type { ReactPaper as ReactPaperType } from '../types/paper.types'; import type { GraphStore } from './graph-store'; import { ReactPaper } from '../models/react-paper'; @@ -100,9 +101,9 @@ export interface AddPaperOptions { /** Optional initial scale for the paper */ readonly scale?: number; /** Optional custom renderer for elements */ - readonly renderElement?: RenderElement; + readonly renderElement?: RenderElement; /** Optional custom renderer for links */ - readonly renderLink?: RenderLink; + readonly renderLink?: RenderLink; } /** @@ -122,9 +123,9 @@ export interface PaperStoreOptions extends AddPaperOptions { */ export interface PaperStoreSnapshot { /** Map of cell IDs to their element views in this paper */ - paperElementViews?: Record; + paperElementViews?: Record; /** Map of link IDs to their link views in this paper */ - linkViews?: Record; + linkViews?: Record; /** Map of link label IDs to their SVG elements */ linksData?: Record; } @@ -145,9 +146,9 @@ export class PaperStore { /** Reference to the overwrite result if custom rendering is used */ public overWriteResultRef?: OverWriteResult; /** Optional custom element renderer */ - public renderElement?: RenderElement; + public renderElement?: RenderElement; /** Optional custom link renderer */ - public renderLink?: RenderLink; + public renderLink?: RenderLink; /** * Cleanup function to unregister paper update callback from GraphStore. @@ -184,8 +185,8 @@ export class PaperStore { this.renderElement = renderElement; this.renderLink = renderLink; const cache: { - elementViews: Record; - linkViews: Record; + elementViews: Record; + linkViews: Record; linksData: Record; } = { elementViews: {}, @@ -333,7 +334,7 @@ export class PaperStore { * @param labelIndex - The index of the label in the labels array * @returns A unique identifier for the link label */ - public getLinkLabelId(linkId: dia.Cell.ID, labelIndex: number) { + public getLinkLabelId(linkId: CellId, labelIndex: number) { return `${linkId}-label-${labelIndex}`; } diff --git a/packages/joint-react/src/store/state-flush.ts b/packages/joint-react/src/store/state-flush.ts index ec510d0694..f4531e0c58 100644 --- a/packages/joint-react/src/store/state-flush.ts +++ b/packages/joint-react/src/store/state-flush.ts @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/cognitive-complexity */ import type { dia } from '@joint/core'; import { startTransition } from 'react'; +import type { CellId } from '../types/cell-id'; import type { GraphSchedulerData } from '../types/scheduler.types'; import type { ExternalStoreLike, State } from '../utils/create-state'; import type { @@ -112,8 +113,8 @@ function isLinkLayoutEqual(a: LinkLayout | undefined, b: LinkLayout): boolean { */ export function flushLayoutState(options: FlushLayoutStateOptions): void { const { graph, layoutState, papers } = options; - const elementLayouts: Record = {}; - const linkLayoutsPerPaper: Record> = {}; + const elementLayouts: Record = {}; + const linkLayoutsPerPaper: Record> = {}; const elements = graph.getElements(); const previousSnapshot = layoutState.getSnapshot(); const previousElementLayouts = previousSnapshot.elements; @@ -151,7 +152,7 @@ export function flushLayoutState(options: FlushLayoutStateOptions): void { const { paper, paperId } = paperStore; if (!paper) continue; - const paperLinkLayouts: Record = {}; + const paperLinkLayouts: Record = {}; const previousPaperLinkLayouts = previousLinkLayouts[paperId] ?? {}; for (const link of links) { diff --git a/packages/joint-react/src/stories/demos/flowchart/code.tsx b/packages/joint-react/src/stories/demos/flowchart/code.tsx index ea0f6e8ae2..583407f147 100644 --- a/packages/joint-react/src/stories/demos/flowchart/code.tsx +++ b/packages/joint-react/src/stories/demos/flowchart/code.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-perf/jsx-no-new-function-as-prop */ /* eslint-disable react-perf/jsx-no-new-object-as-prop */ import './index.css'; -import type { GraphLink, RenderElement, TransformOptions } from '@joint/react'; +import type { FlatLinkData, RenderElement, TransformOptions } from '@joint/react'; import { GraphProvider, Paper, useHighlighter, useNodeSize } from '@joint/react'; import { PAPER_CLASSNAME, PRIMARY, SECONDARY } from 'storybook-config/theme'; import { dia, linkTools } from '@joint/core'; @@ -79,7 +79,7 @@ const flowchartNodes: Record = { cy: 460, }, }; -interface FlowchartLinkOptions extends GraphLink { +interface FlowchartLinkOptions extends FlatLinkData { readonly label?: string; } diff --git a/packages/joint-react/src/stories/demos/introduction-demo/code.tsx b/packages/joint-react/src/stories/demos/introduction-demo/code.tsx index ea52d6a90b..b2aa4f8544 100644 --- a/packages/joint-react/src/stories/demos/introduction-demo/code.tsx +++ b/packages/joint-react/src/stories/demos/introduction-demo/code.tsx @@ -17,9 +17,10 @@ import { useHighlighter, useNodeSize, useLinks, - type GraphElement, - type GraphElementPort, - type GraphLink, + type CellId, + type FlatElementData, + type FlatElementPort, + type FlatLinkData, type ReactPaper, type PaperProps, useNodeLayout, @@ -31,7 +32,7 @@ import { getMessageNodeClassName } from './get-message-node-class-name'; import { isCellSelected } from './is-cell-selected'; // Define types for the elements -interface ElementBase extends GraphElement { +interface ElementBase extends FlatElementData { readonly elementType: 'alert' | 'info' | 'table'; } @@ -50,7 +51,7 @@ interface TableElement extends ElementBase { type Element = MessageElement | TableElement; -type ElementWithSelected = { readonly selectedId: dia.Cell.ID | null } & T; +type ElementWithSelected = { readonly selectedId: CellId | null } & T; const BUTTON_CLASSNAME = 'bg-blue-500 cursor-pointer hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-sm flex items-center'; @@ -58,7 +59,7 @@ const BUTTON_CLASSNAME = const ROW_HEIGHT_OFFSET = 45; const PORT_START_Y = 65; -function buildTablePorts(rows: string[][]): GraphElementPort[] { +function buildTablePorts(rows: string[][]): FlatElementPort[] { return rows.map((_, index) => ({ id: `out-3-${index}`, cx: 400, @@ -127,7 +128,7 @@ const elements: Record = { }; // Create initial links from table element port to another element as Record -const links: Record = { +const links: Record = { link2: { source: '3', // Port from table element sourcePort: 'out-3-0', @@ -338,8 +339,8 @@ const toolsView = new dia.ToolsView({ interface ToolbarProps { readonly onToggleMinimap: (visible: boolean) => void; readonly isMinimapVisible: boolean; - readonly selectedId: dia.Cell.ID | null; - readonly setSelectedId: (id: dia.Cell.ID | null) => void; + readonly selectedId: CellId | null; + readonly setSelectedId: (id: CellId | null) => void; readonly showElementsInfo: boolean; readonly setShowElementsInfo: (show: boolean) => void; readonly paperCtxRef: React.RefObject; @@ -391,7 +392,7 @@ function ToolBar(props: Readonly) { const clone = cell.clone(); clone.translate(20, 20); graph.addCell(clone); - setSelectedId(clone.id); + setSelectedId(clone.id as CellId); }} > @@ -472,7 +473,7 @@ function ElementsInfo() { // Define main view component and render elements function Main() { const [isMinimapVisible, setIsMinimapVisible] = useState(false); - const [selectedElement, setSelectedElement] = useState(null); + const [selectedElement, setSelectedElement] = useState(null); const [showElementsInfo, setShowElementsInfo] = useState(false); const paperCtxRef = useRef(null); @@ -532,7 +533,7 @@ function Main() { className={PAPER_CLASSNAME} onCellPointerClick={({ cellView }) => { const cell = cellView.model; - setSelectedElement(cell.id ?? null); + setSelectedElement((cell.id as CellId) ?? null); }} onLinkPointerClick={() => { setSelectedElement(null); diff --git a/packages/joint-react/src/stories/demos/introduction-demo/is-cell-selected.ts b/packages/joint-react/src/stories/demos/introduction-demo/is-cell-selected.ts index 79f8af3a51..d2f7be8418 100644 --- a/packages/joint-react/src/stories/demos/introduction-demo/is-cell-selected.ts +++ b/packages/joint-react/src/stories/demos/introduction-demo/is-cell-selected.ts @@ -1,8 +1,8 @@ -import type { dia } from '@joint/core'; +import type { CellId } from '../../../types/cell-id'; export function isCellSelected( - cellId: dia.Cell.ID | null | undefined, - selectedId: dia.Cell.ID | null + cellId: CellId | null | undefined, + selectedId: CellId | null ): boolean { if (cellId == null || selectedId == null) { return false; diff --git a/packages/joint-react/src/stories/demos/pulsing-port/code.tsx b/packages/joint-react/src/stories/demos/pulsing-port/code.tsx index 06260dfd63..b8e9113074 100644 --- a/packages/joint-react/src/stories/demos/pulsing-port/code.tsx +++ b/packages/joint-react/src/stories/demos/pulsing-port/code.tsx @@ -3,7 +3,7 @@ import { useRef } from 'react'; import { dia, highlighters, linkTools, V } from '@joint/core'; import { shapes } from '@joint/core'; -import type { GraphElement, GraphElementPort } from '@joint/react'; +import type { FlatElementData, FlatElementPort } from '@joint/react'; import { PAPER_CLASSNAME, PRIMARY, LIGHT, BG } from 'storybook-config/theme'; import { getCellId, @@ -61,18 +61,18 @@ const Pulse = dia.HighlighterView.extend({ }, }); -const NODE_PORTS: GraphElementPort[] = [ +const NODE_PORTS: FlatElementPort[] = [ { id: 'in', cx: NODE_WIDTH / 2, cy: 0, width: PORT_SIZE, height: PORT_SIZE, color: LIGHT, passive: true }, { id: 'out', cx: NODE_WIDTH / 2, cy: NODE_HEIGHT, width: PORT_SIZE, height: PORT_SIZE, color: LIGHT }, ]; -const elements: Record = { +const elements: Record = { '1': { x: 50, y: 50, ports: NODE_PORTS }, '2': { x: 350, y: 50, ports: NODE_PORTS }, '3': { x: 150, y: 250, ports: NODE_PORTS }, }; -function NodeElement(_props: Readonly) { +function NodeElement(_props: Readonly) { const id = useCellId(); const rectRef = useRef(null); const { width, height } = useNodeSize(rectRef); diff --git a/packages/joint-react/src/stories/demos/user-flow/code.tsx b/packages/joint-react/src/stories/demos/user-flow/code.tsx index 7393e917c1..ab2d89b327 100644 --- a/packages/joint-react/src/stories/demos/user-flow/code.tsx +++ b/packages/joint-react/src/stories/demos/user-flow/code.tsx @@ -5,9 +5,10 @@ import { GraphProvider, Paper, useCellId, + type CellId, type ElementToGraphOptions, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, } from '@joint/react'; import type { dia } from '@joint/core'; import { useCallback, useState } from 'react'; @@ -21,7 +22,7 @@ import { type OutputPort, } from './port-utilities'; -type NodeType = GraphElement & { +type NodeType = FlatElementData & { readonly title: string; readonly description: string; readonly nodeType: 'user-action' | 'entity' | 'confirm' | 'message'; @@ -65,7 +66,7 @@ const nodes: Record = { }, }; -const links: Record = { +const links: Record = { link1: { source: '1', sourcePort: '1', @@ -90,7 +91,7 @@ const links: Record = { }; const mapDataToElementAttributes = ( - options: ElementToGraphOptions + options: ElementToGraphOptions ): dia.Cell.JSON => { const result = options.toAttributes(options.data); const { outputPorts } = options.data as NodeType; @@ -101,7 +102,7 @@ const mapDataToElementAttributes = ( }; interface RenderElementProps extends NodeType { - readonly onAddPort: (id: dia.Cell.ID) => void; + readonly onAddPort: (id: CellId) => void; } function RenderElement({ title, description, nodeType, outputPorts, onAddPort }: Readonly) { @@ -173,10 +174,10 @@ function RenderElement({ title, description, nodeType, outputPorts, onAddPort }: } function Main() { - const [elements, setElements] = useState>(nodes); - const [controlledLinks, setControlledLinks] = useState>(links); + const [elements, setElements] = useState>(nodes); + const [controlledLinks, setControlledLinks] = useState>(links); - const onAddPort = useCallback((id: dia.Cell.ID) => { + const onAddPort = useCallback((id: CellId) => { setElements((previous) => { const node = previous[id] as NodeType | undefined; if (!node) return previous; diff --git a/packages/joint-react/src/stories/examples/stress/code.tsx b/packages/joint-react/src/stories/examples/stress/code.tsx index 6bb4156f2d..d4e336995b 100644 --- a/packages/joint-react/src/stories/examples/stress/code.tsx +++ b/packages/joint-react/src/stories/examples/stress/code.tsx @@ -3,15 +3,12 @@ import { GraphProvider, Paper, - useLinkLayout, - type GraphElement, - type GraphLink, - type RenderLink, + type FlatElementData, + type FlatLinkData, } from '@joint/react'; import '../index.css'; import React, { useCallback, useRef, useState, startTransition, memo } from 'react'; -import { PAPER_CLASSNAME, PRIMARY, LIGHT } from 'storybook-config/theme'; -import { REACT_LINK_TYPE } from '../../../models/react-link'; +import { PAPER_CLASSNAME, PRIMARY } from 'storybook-config/theme'; function initialElements(xNodes = 15, yNodes = 30) { const nodes: Record< @@ -28,7 +25,6 @@ function initialElements(xNodes = 15, yNodes = 30) { const edges: Record< string, { - type: string; source: string; target: string; z: number; @@ -55,7 +51,6 @@ function initialElements(xNodes = 15, yNodes = 30) { if (recentNodeId !== null && nodeId <= xNodes * yNodes) { const edgeIdString = `edge-${edgeId.toString()}`; edges[edgeIdString] = { - type: REACT_LINK_TYPE, source: `stress-${recentNodeId.toString()}`, target: `stress-${nodeId.toString()}`, z: -1, @@ -94,31 +89,20 @@ const RenderElement = memo(function RenderElement({ ); }); -function StressLinkPath() { - const layout = useLinkLayout(); - if (!layout) return null; - return ; -} - function Main({ setElements, }: Readonly<{ - setElements: React.Dispatch>>; + setElements: React.Dispatch>>; }>) { - // Memoize the renderElement function to prevent unnecessary re-renders const renderElement = useCallback( (element: BaseElementWithData) => , [] ); - const renderLink: RenderLink = useCallback(() => , []); - const updatePos = useCallback(() => { - // Use startTransition to mark this as a non-urgent update - // This allows React to keep the UI responsive during the update startTransition(() => { setElements((previousElements) => { - const newElements: Record = {}; + const newElements: Record = {}; for (const [id, node] of Object.entries(previousElements)) { newElements[id] = { ...node, @@ -139,7 +123,6 @@ function Main({ className={PAPER_CLASSNAME} height={600} renderElement={renderElement} - renderLink={renderLink} />
); } export default function App() { + const [useReactLinks, setUseReactLinks] = useState(true); + return ( -
+
+ +
+
); } diff --git a/packages/joint-react/src/stories/examples/with-render-link/docs.mdx b/packages/joint-react/src/stories/examples/with-render-link/docs.mdx new file mode 100644 index 0000000000..950ae90666 --- /dev/null +++ b/packages/joint-react/src/stories/examples/with-render-link/docs.mdx @@ -0,0 +1,23 @@ +import { Meta, Story, Canvas, Controls, Markdown } from '@storybook/addon-docs/blocks'; +import * as Stories from './story'; +import Code from './code?raw'; + + + +# Render Link Example + +This example demonstrates how to use `renderLink` to render custom React components for links. Each link is rendered as an SVG `` with a label placed at the midpoint using a ``. + +The `useLinkLayout` hook provides layout data (source/target coordinates and path `d` attribute) for positioning the link path and label. + +### Demo + + + +### Code + + +{`\`\`\`tsx +${Code} +\`\`\``} + diff --git a/packages/joint-react/src/stories/examples/with-render-link/story.tsx b/packages/joint-react/src/stories/examples/with-render-link/story.tsx new file mode 100644 index 0000000000..8dd6e7636f --- /dev/null +++ b/packages/joint-react/src/stories/examples/with-render-link/story.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import '../index.css'; +import Code from './code'; +export type Story = StoryObj; +import { makeRootDocumentation } from '../../utils/make-story'; + +import CodeRaw from './code?raw'; + +export default { + title: 'Examples/Render Link', + component: Code, + tags: ['example'], + parameters: makeRootDocumentation({ + code: CodeRaw, + }), +} satisfies Meta; + +export const Default: Story = {}; diff --git a/packages/joint-react/src/stories/examples/with-selection/code.tsx b/packages/joint-react/src/stories/examples/with-selection/code.tsx index 7f69fc56a2..8041ff7c56 100644 --- a/packages/joint-react/src/stories/examples/with-selection/code.tsx +++ b/packages/joint-react/src/stories/examples/with-selection/code.tsx @@ -10,8 +10,9 @@ import { usePaper, useNodeSize, useNodeLayout, - type GraphElement, - type GraphLink, + type CellId, + type FlatElementData, + type FlatLinkData, type PaperProps, type RenderElement, // ReactLinkView, @@ -28,14 +29,14 @@ const PAPER_CLASSNAME = 'border-1 border-gray-300 rounded-lg shadow-md overflow- const MINIMAP_WIDTH = 200; const MINIMAP_HEIGHT = 150; -interface ElementData extends GraphElement { +interface ElementData extends FlatElementData { readonly type?: 'default' | 'error' | 'info'; readonly title?: string; readonly color?: string; readonly jjType?: string; } -interface LinkData extends GraphLink { +interface LinkData extends FlatLinkData { readonly className?: string; readonly jjType?: string; } @@ -240,7 +241,7 @@ function MiniMap({ paper }: Readonly<{ paper: dia.Paper }>) { // Selection // ============================================================================ -function Selection({ selectedId }: { selectedId: dia.Cell.ID | null }) { +function Selection({ selectedId }: { selectedId: CellId | null }) { const paper = usePaper(); const graph = useGraph(); @@ -308,7 +309,7 @@ function RenderElementWithBadge({ function Main() { const [paper, setPaper] = useState(null); const [showMinimap, setShowMinimap] = useState(false); - const [selectedElement, setSelectedElement] = useState(null); + const [selectedElement, setSelectedElement] = useState(null); const renderElement = useCallback((data: ElementData) => { return ; @@ -331,7 +332,7 @@ function Main() { validateMagnet={(_, magnet) => magnet.getAttribute('magnet') !== 'passive'} linkPinning={false} onElementPointerClick={({ elementView }) => - setSelectedElement(elementView.model.id ?? null) + setSelectedElement((elementView.model.id as CellId) ?? null) } onElementPointerDblClick={({ elementView }) => { const cell = elementView.model; diff --git a/packages/joint-react/src/stories/examples/with-shape-animations/code.tsx b/packages/joint-react/src/stories/examples/with-shape-animations/code.tsx index ca44ac46a7..97a4c69d07 100644 --- a/packages/joint-react/src/stories/examples/with-shape-animations/code.tsx +++ b/packages/joint-react/src/stories/examples/with-shape-animations/code.tsx @@ -5,8 +5,8 @@ import { Paper, useCellActions, useElements, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, TextNode, } from '@joint/react'; import { BG, LIGHT, PAPER_CLASSNAME, PRIMARY, SECONDARY, TEXT } from 'storybook-config/theme'; @@ -22,14 +22,14 @@ const ShapeTypes = { bulb: 'bulb', } as const; -interface GeneratorElement extends GraphElement { +interface GeneratorElement extends FlatElementData { readonly type: typeof ShapeTypes.generator; readonly width: number; readonly height: number; readonly power: number; } -interface BulbElement extends GraphElement { +interface BulbElement extends FlatElementData { readonly type: typeof ShapeTypes.bulb; readonly width: number; readonly height: number; @@ -105,7 +105,7 @@ const wireAppearance = { z: -1, }; -const initialLinks: Record = { +const initialLinks: Record = { wire1: { source: 'generator', target: 'bulb1', diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-jotai.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-jotai.tsx index 53d8f526e9..a0892586f4 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-jotai.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-jotai.tsx @@ -35,8 +35,8 @@ import { GraphProvider, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, Paper, type ExternalGraphStore, } from '@joint/react'; @@ -54,14 +54,14 @@ import type { Update } from '../../../utils/create-state'; /** * Custom element type with a label property. */ -type CustomElement = GraphElement & { label: string }; +type CustomElement = FlatElementData & { label: string }; const defaultElements: Record = { '1': { label: 'Hello', x: 100, y: 0, width: 100, height: 50 }, '2': { label: 'World', x: 100, y: 200, width: 100, height: 50 }, }; -const defaultLinks: Record = { +const defaultLinks: Record = { 'e1-2': { source: '1', target: '2', @@ -96,14 +96,14 @@ const jotaiStore = createStore(); * Jotai atom for graph elements. * Atoms are the building blocks of Jotai - they hold state. */ -const elementsAtom = atom>( - defaultElements as Record +const elementsAtom = atom>( + defaultElements as Record ); /** * Jotai atom for graph links. */ -const linksAtom = atom>(defaultLinks as Record); +const linksAtom = atom>(defaultLinks as Record); // ============================================================================ // STEP 4: Create Jotai Adapter Hook @@ -241,7 +241,7 @@ function PaperApp({ store }: Readonly) { // This will automatically sync to the graph and update Jotai atoms store.setState({ elements: { ...currentState.elements, [newId]: newElement }, - links: currentState.links as Record, + links: currentState.links as Record, }); }} > @@ -269,7 +269,7 @@ function PaperApp({ store }: Readonly) { const { [removedElementId]: _removed, ...newElements } = currentState.elements; // Remove links connected to the removed element - const newLinks: Record = {}; + const newLinks: Record = {}; for (const [id, link] of Object.entries(currentState.links)) { if (link.source !== removedElementId && link.target !== removedElementId) { newLinks[id] = link; diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-peerjs.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-peerjs.tsx index 657b77eb54..8db5f6fe62 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-peerjs.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-peerjs.tsx @@ -40,8 +40,8 @@ import { GraphProvider, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, Paper, type ExternalGraphStore, } from '@joint/react'; @@ -59,14 +59,14 @@ import type { Update } from '../../../utils/create-state'; /** * Custom element type with a label property. */ -type CustomElement = GraphElement & { label: string }; +type CustomElement = FlatElementData & { label: string }; const defaultElements: Record = { '1': { label: 'Hello', x: 100, y: 0, width: 100, height: 50 }, '2': { label: 'World', x: 100, y: 200, width: 100, height: 50 }, }; -const defaultLinks: Record = { +const defaultLinks: Record = { 'e1-2': { source: '1', target: '2', @@ -97,8 +97,8 @@ function RenderItem(props: CustomElement) { */ interface StateSyncMessage { type: 'state-update'; - elements: Record; - links: Record; + elements: Record; + links: Record; } /** @@ -117,8 +117,8 @@ interface StateSyncMessage { type ConnectionStatus = 'disconnected' | 'connecting' | 'connected'; function createPeerJSStore( - initialElements: Record, - initialLinks: Record, + initialElements: Record, + initialLinks: Record, callbacks: { onPeerIdChange: (id: string | null) => void; onConnectionStatusChange: (status: ConnectionStatus) => void; @@ -470,7 +470,7 @@ function PaperApp({ store }: Readonly) { const { [removedElementId]: _removed, ...newElements } = currentState.elements; // Remove links connected to the removed element - const newLinks: Record = {}; + const newLinks: Record = {}; for (const [id, link] of Object.entries(currentState.links)) { if (link.source !== removedElementId && link.target !== removedElementId) { newLinks[id] = link; diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-redux.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-redux.tsx index 22f09f76c4..eb483fbacd 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-redux.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-redux.tsx @@ -6,8 +6,8 @@ import { GraphProvider, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, Paper, type ExternalGraphStore, } from '@joint/react'; @@ -73,9 +73,9 @@ import type { Update } from '../../../utils/create-state'; */ interface GraphState { /** Record of all elements (nodes) in the graph keyed by ID */ - readonly elements: Record; + readonly elements: Record; /** Record of all links (edges) in the graph keyed by ID */ - readonly links: Record; + readonly links: Record; } // ============================================================================ @@ -85,7 +85,7 @@ interface GraphState { /** * Custom element type with a label property. */ -type CustomElement = GraphElement & { label: string }; +type CustomElement = FlatElementData & { label: string }; /** * Initial elements for the graph. @@ -98,7 +98,7 @@ const defaultElements: Record = { /** * Initial links for the graph. */ -const defaultLinks: Record = { +const defaultLinks: Record = { 'e1-2': { source: '1', target: '2', @@ -114,15 +114,15 @@ const defaultLinks: Record = { const graphSlice = createSlice({ name: 'graph', initialState: { - elements: defaultElements as Record, - links: defaultLinks as Record, + elements: defaultElements as Record, + links: defaultLinks as Record, } satisfies GraphState, reducers: { /** * Adds a new element to the graph. * The element must include an 'id' property that will be used as the key. */ - addElement: (state, action: PayloadAction<{ id: string } & GraphElement>) => { + addElement: (state, action: PayloadAction<{ id: string } & FlatElementData>) => { const { id, ...element } = action.payload; state.elements[id] = element; }, diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-zustand.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-zustand.tsx index 1478d31bf3..023f6833c0 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-zustand.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode-zustand.tsx @@ -35,8 +35,8 @@ import { GraphProvider, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, Paper, type ExternalGraphStore, } from '@joint/react'; @@ -54,14 +54,14 @@ import type { Update } from '../../../utils/create-state'; /** * Custom element type with a label property. */ -type CustomElement = GraphElement & { label: string }; +type CustomElement = FlatElementData & { label: string }; const defaultElements: Record = { '1': { label: 'Hello', x: 100, y: 0, width: 100, height: 50 }, '2': { label: 'World', x: 100, y: 200, width: 100, height: 50 }, }; -const defaultLinks: Record = { +const defaultLinks: Record = { 'e1-2': { source: '1', target: '2', @@ -91,11 +91,11 @@ function RenderItem(props: CustomElement) { */ interface GraphStore { /** Record of all elements (nodes) in the graph keyed by ID */ - elements: Record; + elements: Record; /** Record of all links (edges) in the graph keyed by ID */ - links: Record; + links: Record; /** Action to add a new element */ - addElement: (id: string, data: GraphElement) => void; + addElement: (id: string, data: FlatElementData) => void; /** Action to remove the last element */ removeLastElement: () => void; /** Action to update the graph state (used by adapter) */ @@ -107,10 +107,10 @@ interface GraphStore { * Zustand stores are simple - just define state and actions in one place. */ const useGraphStore = create((set) => ({ - elements: defaultElements as Record, - links: defaultLinks as Record, + elements: defaultElements as Record, + links: defaultLinks as Record, - addElement: (id: string, element: GraphElement) => { + addElement: (id: string, element: FlatElementData) => { set((state) => ({ elements: { ...state.elements, [id]: element }, })); @@ -129,7 +129,7 @@ const useGraphStore = create((set) => ({ // eslint-disable-next-line sonarjs/no-unused-vars const { [removedElementId]: _removed, ...newElements } = state.elements; - const newLinks: Record = {}; + const newLinks: Record = {}; for (const [id, link] of Object.entries(state.links)) { if (link.source !== removedElementId && link.target !== removedElementId) { newLinks[id] = link; diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode.tsx index b0a71629a0..00312de20e 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-controlled-mode.tsx @@ -40,8 +40,8 @@ import { GraphProvider, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, Paper, } from '@joint/react'; import '../../examples/index.css'; @@ -54,15 +54,15 @@ import { useState, type Dispatch, type SetStateAction } from 'react'; /** * Custom element type with a label property. - * Extends GraphElement with our custom 'label' property. + * Extends FlatElementData with our custom 'label' property. */ -type CustomElement = GraphElement & { label: string }; +type CustomElement = FlatElementData & { label: string }; /** * Custom link type. - * Uses GraphLink as the base type for our links. + * Uses FlatLinkData as the base type for our links. */ -type CustomLink = GraphLink; +type CustomLink = FlatLinkData; /** * Initial elements (nodes) for the graph. @@ -383,8 +383,8 @@ function PaperApp({ onElementsChange, onLinksChange }: Readonly) function Main(props: Readonly) { // Create React state for elements and links // These are the single source of truth for the graph - const [elements, setElements] = useState>(defaultElements); - const [links, setLinks] = useState>(defaultLinks); + const [elements, setElements] = useState>(defaultElements); + const [links, setLinks] = useState>(defaultLinks); return ( ) { {/* Pass state setters to child component so it can update the graph by updating React state. The type assertions are needed because - GraphElement/GraphLink are more generic than CustomElement/CustomLink. + FlatElementData/FlatLinkData are more generic than CustomElement/CustomLink. */} >>} diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-html-renderer.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-html-renderer.tsx index 5ef61c11ed..69f765f878 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-html-renderer.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-html-renderer.tsx @@ -5,14 +5,14 @@ import { usePaper, useNodeSize, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, } from '@joint/react'; import '../../examples/index.css'; import { BUTTON_CLASSNAME } from 'storybook-config/theme'; // Define element type with custom properties -type CustomElement = GraphElement & { data: { label: string } }; +type CustomElement = FlatElementData & { data: { label: string } }; // Define initial elements as Record const initialElements: Record = { @@ -21,7 +21,7 @@ const initialElements: Record = { }; // Define initial edges as Record -const initialEdges: Record = { +const initialEdges: Record = { 'e1-2': { source: '1', target: '2', diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-html.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-html.tsx index ff474d556a..6099031935 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-html.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-html.tsx @@ -4,15 +4,15 @@ import { GraphProvider, Paper, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, useNodeSize, } from '@joint/react'; import '../../examples/index.css'; import { PAPER_CLASSNAME, PRIMARY } from 'storybook-config/theme'; // define element type with custom properties -type CustomElement = GraphElement & { label: string }; +type CustomElement = FlatElementData & { label: string }; // define initial elements as Record const initialElements: Record = { @@ -21,7 +21,7 @@ const initialElements: Record = { }; // define initial edges as Record -const initialEdges: Record = { +const initialEdges: Record = { 'e1-2': { source: '1', target: '2', diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/code-svg.tsx b/packages/joint-react/src/stories/tutorials/step-by-step/code-svg.tsx index 21624a2276..7f90205343 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/code-svg.tsx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/code-svg.tsx @@ -5,12 +5,12 @@ import { GraphProvider, Paper, type GraphProps, - type GraphElement, - type GraphLink, + type FlatElementData, + type FlatLinkData, } from '@joint/react'; // define element type with custom properties -type CustomElement = GraphElement & { color: string }; +type CustomElement = FlatElementData & { color: string }; // define initial elements as Record const initialElements: Record = { @@ -19,7 +19,7 @@ const initialElements: Record = { }; // define initial edges as Record -const initialEdges: Record = { +const initialEdges: Record = { 'e1-2': { source: '1', target: '2', diff --git a/packages/joint-react/src/stories/tutorials/step-by-step/docs.mdx b/packages/joint-react/src/stories/tutorials/step-by-step/docs.mdx index 7551d89d74..7a368f0495 100644 --- a/packages/joint-react/src/stories/tutorials/step-by-step/docs.mdx +++ b/packages/joint-react/src/stories/tutorials/step-by-step/docs.mdx @@ -133,7 +133,7 @@ If you need full HTML support, enable the experimental `useHTMLOverlay` prop to Ports allow you to define named connection points directly on element data and map them via `mapDataToElementAttributes`. ```tsx -import { GraphProvider, type ElementToGraphOptions, type GraphElement } from '@joint/react'; +import { GraphProvider, type ElementToGraphOptions, type FlatElementData } from '@joint/react'; import type { dia } from '@joint/core'; const elements = { @@ -152,7 +152,7 @@ const elements = { const mapDataToElementAttributes = ({ data, defaultAttributes, -}: ElementToGraphOptions): dia.Cell.JSON => ({ +}: ElementToGraphOptions): dia.Cell.JSON => ({ ...defaultAttributes(), ...(data.ports && { ports: data.ports }), }); @@ -219,8 +219,8 @@ ${CodeControlledMode} 1. **State Management**: Use `useState` to manage elements and links as Records: ```tsx - const [elements, setElements] = useState>(defaultElements); - const [links, setLinks] = useState>(defaultLinks); + const [elements, setElements] = useState>(defaultElements); + const [links, setLinks] = useState>(defaultLinks); ``` 2. **Controlled Mode**: Pass state and change handlers to `GraphProvider`: diff --git a/packages/joint-react/src/types/cell-id.ts b/packages/joint-react/src/types/cell-id.ts new file mode 100644 index 0000000000..14a06506a4 --- /dev/null +++ b/packages/joint-react/src/types/cell-id.ts @@ -0,0 +1,6 @@ +/** + * Unique identifier for a cell (element or link) in the graph. + * Always a string in the React bindings. + */ +// eslint-disable-next-line sonarjs/redundant-type-aliases -- Semantic alias for readability +export type CellId = string; diff --git a/packages/joint-react/src/types/element-types.ts b/packages/joint-react/src/types/element-types.ts index 5715f92df1..4572a8753e 100644 --- a/packages/joint-react/src/types/element-types.ts +++ b/packages/joint-react/src/types/element-types.ts @@ -3,7 +3,7 @@ * Converted to full JointJS port format by the default element mapper. * @group Graph */ -export interface GraphElementPort { +export interface FlatElementPort { /** * Unique port identifier. */ @@ -77,11 +77,11 @@ export interface GraphElementPort { readonly labelClassName?: string; } -export interface GraphElement extends Record { +export interface FlatElementData extends Record { /** * Ports of the element. */ - ports?: GraphElementPort[]; + ports?: FlatElementPort[]; /** * X position of the element. */ diff --git a/packages/joint-react/src/types/link-types.ts b/packages/joint-react/src/types/link-types.ts index 6dba239f1e..5617886eb3 100644 --- a/packages/joint-react/src/types/link-types.ts +++ b/packages/joint-react/src/types/link-types.ts @@ -1,5 +1,6 @@ import type { anchors, connectionPoints, dia, shapes } from '@joint/core'; import type { MarkerPreset } from '../theme/link-theme'; +import type { CellId } from './cell-id'; /** * Link endpoint definition. @@ -8,11 +9,11 @@ import type { MarkerPreset } from '../theme/link-theme'; * - An object with `x` and `y` connects to a fixed point on the canvas. * * Port, anchor, connectionPoint and magnet are specified via separate - * top-level properties on {@link GraphLink} (e.g. `sourcePort`, `sourceAnchor`). + * top-level properties on {@link FlatLinkData} (e.g. `sourcePort`, `sourceAnchor`). * @group Graph */ -export type GraphLinkEnd = - | dia.Cell.ID +export type FlatLinkEnd = + | CellId | { readonly x: number; readonly y: number }; export interface StandardLinkShapesTypeMapper { @@ -27,7 +28,7 @@ export type StandardLinkShapesType = keyof StandardLinkShapesTypeMapper; * Simplified label definition for graph links. * @group Graph */ -export interface GraphLinkLabel { +export interface FlatLinkLabel { /** * Label text content. */ @@ -91,15 +92,15 @@ export interface GraphLinkLabel { * @group Graph * @see @see https://docs.jointjs.com/learn/features/shapes/links/#dialink */ -export interface GraphLink extends Record { +export interface FlatLinkData extends Record { /** * Source element id or point. */ - readonly source: GraphLinkEnd; + readonly source: FlatLinkEnd; /** * Target element id or point. */ - readonly target: GraphLinkEnd; + readonly target: FlatLinkEnd; /** * Source port id. */ @@ -154,7 +155,7 @@ export interface GraphLink extends Record { /** * Link labels. */ - readonly labels?: GraphLinkLabel[]; + readonly labels?: FlatLinkLabel[]; /** * Link vertices (waypoints). */ diff --git a/packages/joint-react/src/types/paper.types.ts b/packages/joint-react/src/types/paper.types.ts index 305cf88731..b31613a4ed 100644 --- a/packages/joint-react/src/types/paper.types.ts +++ b/packages/joint-react/src/types/paper.types.ts @@ -1,11 +1,12 @@ import type { dia } from '@joint/core'; +import type { CellId } from './cell-id'; import type { GraphState } from '../store/graph-store'; /** * Cache interface for element view state. */ export interface ReactElementViewCache { - elementViews: Record; + elementViews: Record; } /** @@ -20,7 +21,7 @@ export interface ReactElementViewGraphStoreRef { * Cache interface for link view state. */ export interface ReactLinkViewCache { - linkViews: Record; + linkViews: Record; linksData: Record; } @@ -36,7 +37,7 @@ export interface ReactLinkViewGraphStoreRef { * PaperStore reference interface for link view. */ export interface ReactLinkViewPaperStoreRef { - getLinkLabelId: (linkId: dia.Cell.ID, labelIndex: number) => string; + getLinkLabelId: (linkId: CellId, labelIndex: number) => string; } /** diff --git a/packages/joint-react/src/types/scheduler.types.ts b/packages/joint-react/src/types/scheduler.types.ts index 39b1dcadfa..69a95a68d1 100644 --- a/packages/joint-react/src/types/scheduler.types.ts +++ b/packages/joint-react/src/types/scheduler.types.ts @@ -1,6 +1,7 @@ import type { dia } from '@joint/core'; -import type { GraphElement } from './element-types'; -import type { GraphLink } from './link-types'; +import type { CellId } from './cell-id'; +import type { FlatElementData } from './element-types'; +import type { FlatLinkData } from './link-types'; /** * Unified scheduler data structure for batching all JointJS to React updates. @@ -9,31 +10,31 @@ import type { GraphLink } from './link-types'; */ export interface GraphSchedulerData { // Elements - readonly elementsToUpdate?: Map; - readonly elementsToDelete?: Map; + readonly elementsToUpdate?: Map; + readonly elementsToDelete?: Map; // Links - readonly linksToUpdate?: Map; - readonly linksToDelete?: Map; + readonly linksToUpdate?: Map; + readonly linksToDelete?: Map; // Ports (nested by element ID) - readonly portsToUpdate?: Map>; - readonly portsToDelete?: Map>; + readonly portsToUpdate?: Map>; + readonly portsToDelete?: Map>; // Port Groups (nested by element ID) - readonly portGroupsToUpdate?: Map>; - readonly portGroupsToDelete?: Map>; + readonly portGroupsToUpdate?: Map>; + readonly portGroupsToDelete?: Map>; // Link attributes - readonly linkAttrsToUpdate?: Map>; + readonly linkAttrsToUpdate?: Map>; // Labels (nested by link ID) - readonly labelsToUpdate?: Map>; - readonly labelsToDelete?: Map>; + readonly labelsToUpdate?: Map>; + readonly labelsToDelete?: Map>; // Views (for React paper updates) - readonly viewsToUpdate?: Map; - readonly viewsToDelete?: Map; + readonly viewsToUpdate?: Map; + readonly viewsToDelete?: Map; // Paper update trigger readonly shouldUpdatePaper?: boolean; diff --git a/packages/joint-react/src/utils/__tests__/get-cell.test.ts b/packages/joint-react/src/utils/__tests__/get-cell.test.ts index c4f10dd950..83fdef0b49 100644 --- a/packages/joint-react/src/utils/__tests__/get-cell.test.ts +++ b/packages/joint-react/src/utils/__tests__/get-cell.test.ts @@ -4,7 +4,7 @@ import { defaultMapLinkAttributesToData } from '../../state/data-mapping'; import { ReactElement } from '../../models/react-element'; import { ReactLink, REACT_LINK_TYPE } from '../../models/react-link'; import type { GraphToLinkOptions } from '../../state/graph-state-selectors'; -import type { GraphLink } from '../../types/link-types'; +import type { FlatLinkData } from '../../types/link-types'; const DEFAULT_CELL_NAMESPACE = { ...shapes, ReactElement, ReactLink }; @@ -37,7 +37,7 @@ describe('graph-state-selectors link mapping', () => { id, cell, graph, - } as unknown as GraphToLinkOptions); + } as unknown as GraphToLinkOptions); expect(link).toMatchObject({ source: 'source-id', diff --git a/packages/joint-react/src/utils/__tests__/is-react-element.test.ts b/packages/joint-react/src/utils/__tests__/is-react-element.test.ts deleted file mode 100644 index 66b18cf520..0000000000 --- a/packages/joint-react/src/utils/__tests__/is-react-element.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ReactElement } from '../../models/react-element'; -import { isReactElement } from '../is-react-element'; -import { dia } from '@joint/core'; - -describe('is-react-element', () => { - it('should return true for ReactElement instance', () => { - const element = new ReactElement({ - id: '1', - position: { x: 10, y: 20 }, - size: { width: 100, height: 50 }, - }); - - expect(isReactElement(element)).toBe(true); - }); - - it('should return false for standard dia.Element', () => { - const element = new dia.Element({ - id: '1', - position: { x: 10, y: 20 }, - size: { width: 100, height: 50 }, - }); - - expect(isReactElement(element)).toBe(false); - }); - - it('should return false for dia.Link', () => { - const link = new dia.Link({ - id: '1', - source: { id: 'a' }, - target: { id: 'b' }, - }); - - expect(isReactElement(link)).toBe(false); - }); - - it('should return false for non-cell values', () => { - expect(isReactElement(null)).toBe(false); - expect(isReactElement(undefined as never)).toBe(false); - expect(isReactElement('string')).toBe(false); - expect(isReactElement(123)).toBe(false); - expect(isReactElement({})).toBe(false); - expect(isReactElement([])).toBe(false); - }); -}); - - - - - - - - - - - - - - - diff --git a/packages/joint-react/src/utils/__tests__/is.test.ts b/packages/joint-react/src/utils/__tests__/is.test.ts index 8100e9c720..a6179cc273 100644 --- a/packages/joint-react/src/utils/__tests__/is.test.ts +++ b/packages/joint-react/src/utils/__tests__/is.test.ts @@ -1,33 +1,9 @@ /* eslint-disable unicorn/consistent-function-scoping */ /* eslint-disable unicorn/no-useless-undefined */ -import { dia } from '@joint/core'; import type { FunctionComponent, JSX } from 'react'; import * as is from '../is'; describe('is.ts utility functions', () => { - test('isSetter', () => { - expect(is.isSetter((x: number) => x + 1)).toBe(true); - expect(is.isSetter(() => {})).toBe(false); - expect(is.isSetter(123)).toBe(false); - }); - - test('isDiaId', () => { - expect(is.isDiaId('cell-id')).toBe(true); - expect(is.isDiaId(123)).toBe(true); - expect(is.isDiaId({})).toBe(false); - }); - - test('isDefined', () => { - expect(is.isDefined(0)).toBe(true); - expect(is.isDefined(undefined)).toBe(false); - expect(is.isDefined(null)).toBe(true); - }); - - test('isAttribute', () => { - expect(is.isAttribute<{ foo: string }>('foo')).toBe(true); - expect(is.isAttribute<{ foo: string }>(123)).toBe(false); - }); - test('isRecord', () => { expect(is.isRecord({})).toBe(true); expect(is.isRecord([])).toBe(true); @@ -35,30 +11,6 @@ describe('is.ts utility functions', () => { expect(is.isRecord(null)).toBe(false); }); - test('isGraphCell', () => { - const cell = { isElement: true, isLink: false }; - expect(is.isGraphCell(cell)).toBe(true); - expect(is.isGraphCell({})).toBe(false); - expect(is.isGraphCell(null)).toBe(false); - }); - - test('isLinkInstance', () => { - const link = new dia.Link(); - expect(is.isLinkInstance(link)).toBe(true); - expect(is.isLinkInstance({})).toBe(false); - }); - - test('isCellInstance', () => { - const cell = new dia.Cell(); - expect(is.isCellInstance(cell)).toBe(true); - expect(is.isCellInstance({})).toBe(false); - }); - - test('hasChildren', () => { - expect(is.hasChildren({ children: [] })).toBe(true); - expect(is.hasChildren({})).toBe(false); - }); - test('isString', () => { expect(is.isString('abc')).toBe(true); expect(is.isString(123)).toBe(false); diff --git a/packages/joint-react/src/utils/__tests__/link-utilities.test.ts b/packages/joint-react/src/utils/__tests__/link-utilities.test.ts index f32ab03b64..787f1b46ac 100644 --- a/packages/joint-react/src/utils/__tests__/link-utilities.test.ts +++ b/packages/joint-react/src/utils/__tests__/link-utilities.test.ts @@ -12,28 +12,4 @@ describe('link-utilities', () => { expect(linkUtilities.getCellId({})).toBeUndefined(); }); }); - - describe('getLinkPortId', () => { - it('returns port property if passed an object', () => { - expect(linkUtilities.getLinkPortId({ port: 'baz' })).toBe('baz'); - }); - it('returns undefined if object has no port', () => { - expect(linkUtilities.getLinkPortId({})).toBeUndefined(); - }); - it('returns undefined if passed a string', () => { - expect(linkUtilities.getLinkPortId('foo')).toBeUndefined(); - }); - }); - - describe('getLinkMagnet', () => { - it('returns magnet property if passed an object', () => { - expect(linkUtilities.getLinkMagnet({ magnet: 'mag' })).toBe('mag'); - }); - it('returns undefined if object has no magnet', () => { - expect(linkUtilities.getLinkMagnet({})).toBeUndefined(); - }); - it('returns undefined if passed a string', () => { - expect(linkUtilities.getLinkMagnet('foo')).toBeUndefined(); - }); - }); }); diff --git a/packages/joint-react/src/utils/__tests__/noop-selector.test.ts b/packages/joint-react/src/utils/__tests__/noop-selector.test.ts deleted file mode 100644 index e71a22486d..0000000000 --- a/packages/joint-react/src/utils/__tests__/noop-selector.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { noopSelector } from '../noop-selector'; - -describe('noop-selector', () => { - it('should return the value passed as argument', () => { - const value = { test: 'value' }; - const result = noopSelector(value); - - expect(result).toBe(value); - }); - - it('should work with primitive values', () => { - expect(noopSelector(42)).toBe(42); - expect(noopSelector('string')).toBe('string'); - expect(noopSelector(true)).toBe(true); - expect(noopSelector(null)).toBe(null); - expect(noopSelector(undefined as never)).toBe(undefined); - }); - - it('should work with arrays', () => { - const array = [1, 2, 3]; - const result = noopSelector(array); - - expect(result).toBe(array); - }); - - it('should work with objects', () => { - const object = { a: 1, b: 2 }; - const result = noopSelector(object); - - expect(result).toBe(object); - }); -}); - - - - - - - - - - - - - - - diff --git a/packages/joint-react/src/utils/__tests__/object-utilities.test.ts b/packages/joint-react/src/utils/__tests__/object-utilities.test.ts index c83f34aa25..0751cf7985 100644 --- a/packages/joint-react/src/utils/__tests__/object-utilities.test.ts +++ b/packages/joint-react/src/utils/__tests__/object-utilities.test.ts @@ -1,42 +1,6 @@ -import { makeOptions, assignOptions, dependencyExtract } from '../object-utilities'; +import { assignOptions, dependencyExtract } from '../object-utilities'; describe('object-utilities', () => { - describe('makeOptions', () => { - it('should remove undefined values', () => { - const options = makeOptions({ - width: 100, - height: 50, - color: undefined, - }); - - expect(options).toEqual({ - width: 100, - height: 50, - }); - expect(options).not.toHaveProperty('color'); - }); - - it('should keep all defined values', () => { - const options = makeOptions({ - width: 100, - height: 50, - color: 'red', - }); - - expect(options).toEqual({ - width: 100, - height: 50, - color: 'red', - }); - }); - - it('should handle empty object', () => { - const options = makeOptions({}); - - expect(options).toEqual({}); - }); - }); - describe('assignOptions', () => { it('should assign new properties and ignore undefined', () => { const props = { width: 100, height: 50 }; diff --git a/packages/joint-react/src/utils/clear-view.ts b/packages/joint-react/src/utils/clear-view.ts deleted file mode 100644 index 7a2579145b..0000000000 --- a/packages/joint-react/src/utils/clear-view.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { dia } from '@joint/core'; - -interface Options { - readonly graph: dia.Graph; - readonly paper: dia.Paper; - readonly cellId: dia.Cell.ID; -} - -/** - * Clear the view of the cell and the links connected to it. - * @internal - * @group Utils - * @description This function is used to clear the view of the cell and the links connected to it. - * It is used to ensure that the view is recalculated and the links are updated. - * - * NOTE: For internal React components, prefer using graphStore.scheduleClearView() - * which batches multiple calls for the same cell, improving performance when - * multiple ports exist on a single node. - * @param options - The options for the clear view. - */ -export function clearView(options: Options) { - const { graph, paper, cellId } = options; - const elementView = paper.findViewByModel(cellId); - elementView.cleanNodesCache(); - for (const link of graph.getConnectedLinks(elementView.model)) { - const target = link.target(); - const source = link.source(); - const isElementLink = target.id === cellId || source.id === cellId; - if (!isElementLink) { - continue; - } - - const linkView = link.findView(paper); - // @ts-expect-error we use private jointjs api method, it throw error here. - linkView._sourceMagnet = null; - // @ts-expect-error we use private jointjs api method, it throw error here. - linkView._targetMagnet = null; - // @ts-expect-error we use private jointjs api method, it throw error here. - linkView.requestConnectionUpdate({ async: false }); - } -} diff --git a/packages/joint-react/src/utils/fast-equality.ts b/packages/joint-react/src/utils/fast-equality.ts index 6921ee4e6c..3044b9cf48 100644 --- a/packages/joint-react/src/utils/fast-equality.ts +++ b/packages/joint-react/src/utils/fast-equality.ts @@ -1,5 +1,5 @@ import { util } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; +import type { FlatElementData } from '../types/element-types'; /** * Fast equality check for arrays of graph elements. @@ -10,7 +10,7 @@ import type { GraphElement } from '../types/element-types'; * @param compareFunction - Optional deep comparison function. Defaults to util.isEqual * @returns True if arrays are equal, false otherwise */ -export function fastElementArrayEqual( +export function fastElementArrayEqual>( a: T[], b: T[], compareFunction: (a: T, b: T) => boolean = util.isEqual @@ -43,11 +43,11 @@ export function fastElementArrayEqual( * @param data - The element to extract properties from * @returns Record of properties excluding x and y */ -function extractNonPositionProperties(data: GraphElement): Record { +function extractNonPositionProperties(data: FlatElementData): Record { const rest: Record = {}; for (const key in data) { if (key !== 'x' && key !== 'y') { - rest[key] = data[key as keyof GraphElement]; + rest[key] = data[key as keyof FlatElementData]; } } return rest; @@ -60,7 +60,7 @@ function extractNonPositionProperties(data: GraphElement): Record(a: T[], b: T[]): boolean { - if (a.length !== b.length) { - return false; - } - if (a === b) { - return true; - } - for (const [index, element] of a.entries()) { - if (element !== b[index]) { - return false; - } - } - return true; -} diff --git a/packages/joint-react/src/utils/is-react-element.ts b/packages/joint-react/src/utils/is-react-element.ts deleted file mode 100644 index 403a65c9c1..0000000000 --- a/packages/joint-react/src/utils/is-react-element.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -import type { dia } from '@joint/core'; -import { ReactElement } from '../models/react-element'; - -export function isReactElement(value: unknown): value is dia.Cell { - return value instanceof ReactElement; -} diff --git a/packages/joint-react/src/utils/is.ts b/packages/joint-react/src/utils/is.ts index f7fff5df43..0dc24a91af 100644 --- a/packages/joint-react/src/utils/is.ts +++ b/packages/joint-react/src/utils/is.ts @@ -1,51 +1,12 @@ /* eslint-disable jsdoc/require-jsdoc */ -import { dia, util } from '@joint/core'; -import type { GraphElement } from '../types/element-types'; +import { util } from '@joint/core'; import type { FunctionComponent, JSX } from 'react'; -import type { GraphLink } from '../types/link-types'; -/** - * Represents a cell in the graph (either element or link). - */ -export type GraphCell = Element | GraphLink; - -export type Setter = (item: Value) => Value; - -export function isSetter(value: unknown): value is Setter { - // check if value is a function and there is parameter - return typeof value === 'function' && value.length === 1; -} - -export function isDiaId(value: unknown): value is dia.Cell.ID { - return util.isString(value) || util.isNumber(value); -} - -export function isDefined(value: Value | undefined): value is Value { - return value !== undefined; -} - -export function isAttribute(value: unknown): value is keyof Value { - return util.isString(value); -} export function isRecord(value: unknown): value is Record { return util.isObject(value); } -export function isGraphCell( - value: unknown -): value is GraphCell { - return isRecord(value) && 'isElement' in value && 'isLink' in value; -} - -export function isLinkInstance(value: unknown): value is dia.Link { - return value instanceof dia.Link; -} - -export function isCellInstance(value: unknown): value is dia.Cell { - return value instanceof dia.Cell; -} - -export function hasChildren(props: Record) { +function hasChildren(props: Record) { return 'children' in props; } export function isString(value: unknown): value is string { @@ -71,12 +32,6 @@ export function isWithChildren(value: unknown): value is { children: JSX.Element return isRecord(value) && hasChildren(value); } -export function assertGraph(graph?: Graph): asserts graph is Graph { - if (!graph) { - throw new Error('Graph instance is required'); - } -} - export function isUpdater(updater: ((previous: T) => T) | T): updater is (previous: T) => T { return typeof updater === 'function' && 'call' in updater; } diff --git a/packages/joint-react/src/utils/link-utilities.ts b/packages/joint-react/src/utils/link-utilities.ts index 6e02b666c3..c033525361 100644 --- a/packages/joint-react/src/utils/link-utilities.ts +++ b/packages/joint-react/src/utils/link-utilities.ts @@ -1,4 +1,5 @@ import type { dia } from '@joint/core'; +import type { CellId } from '../types/cell-id'; /** * Get the link id from the given id. @@ -15,53 +16,9 @@ import type { dia } from '@joint/core'; * const id2 = getCellId({ id: 'element-1', port: 'port-1' }); // 'element-1' * ``` */ -export function getCellId(id: dia.Cell.ID | dia.Link.EndJSON): dia.Cell.ID | undefined { +export function getCellId(id: CellId | dia.Link.EndJSON): CellId | undefined { if (typeof id === 'object') { - return id.id; + return id.id as CellId | undefined; } return id; } - -/** - * Get the link port id from the given id. - * @param id - The id to get the link port id from. - * @returns The link port id or undefined if not found. - * @example - * ```ts - * import { getLinkPortId } from '@joint/react'; - * - * // With string id - * const port1 = getLinkPortId('element-1'); // undefined - * - * // With object id - * const port2 = getLinkPortId({ id: 'element-1', port: 'port-1' }); // 'port-1' - * ``` - */ -export function getLinkPortId(id: dia.Cell.ID | dia.Link.EndJSON): string | undefined { - if (typeof id === 'object') { - return id.port; - } - return undefined; -} - -/** - * Get the link magnet from the given id. - * @param id - The id to get the link magnet from. - * @returns The link magnet or undefined if not found. - * @example - * ```ts - * import { getLinkMagnet } from '@joint/react'; - * - * // With string id - * const magnet1 = getLinkMagnet('element-1'); // undefined - * - * // With object id - * const magnet2 = getLinkMagnet({ id: 'element-1', magnet: 'magnet-1' }); // 'magnet-1' - * ``` - */ -export function getLinkMagnet(id: dia.Cell.ID | dia.Link.EndJSON): string | undefined { - if (typeof id === 'object') { - return id.magnet; - } - return undefined; -} diff --git a/packages/joint-react/src/utils/noop-selector.ts b/packages/joint-react/src/utils/noop-selector.ts deleted file mode 100644 index cc5ebfa092..0000000000 --- a/packages/joint-react/src/utils/noop-selector.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Application selector that does nothing. - * @param value - The value to return. - * @returns The value passed as an argument. - * @group Utils - */ -export function noopSelector(value: T): T { - return value; -} diff --git a/packages/joint-react/src/utils/object-utilities.ts b/packages/joint-react/src/utils/object-utilities.ts index 1455675565..eaa0ee667e 100644 --- a/packages/joint-react/src/utils/object-utilities.ts +++ b/packages/joint-react/src/utils/object-utilities.ts @@ -2,32 +2,6 @@ import { util } from '@joint/core'; -/** - * Make options and avoid to generate undefined values. - * @param options - An object containing options where keys are strings and values can be of any type. - * @returns - A new object with the same properties as the input options, but without any properties that have undefined - * @example - * ```ts - * import { makeOptions } from '@joint/react'; - * - * const options = makeOptions({ - * width: 100, - * height: 50, - * color: undefined, // This will be removed - * }); - * // Result: { width: 100, height: 50 } - * ``` - */ -export function makeOptions>(options: T): T { - const result: T = {} as T; - for (const key in options) { - if (options[key] !== undefined) { - result[key] = options[key]; - } - } - return result; -} - /** * Assign new properties to an instance, ignoring undefined values. * @param props - The instance to which new properties will be assigned. diff --git a/packages/joint-react/src/utils/scheduler.ts b/packages/joint-react/src/utils/scheduler.ts index 1fded1ac06..738baccba1 100644 --- a/packages/joint-react/src/utils/scheduler.ts +++ b/packages/joint-react/src/utils/scheduler.ts @@ -81,15 +81,3 @@ export class Scheduler { this.onFlush(dataToFlush); }; } - -/** - * Creates a simple scheduler function that batches multiple calls into a single flush. - * @param callback The callback to invoke on flush - * @returns A function to schedule updates - */ -export function createScheduler(callback: () => void): () => void { - const scheduler = new Scheduler>({ - onFlush: callback, - }); - return () => scheduler.scheduleData((previous) => previous); -}