diff --git a/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx b/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx index 4a21477e..f00079b6 100644 --- a/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx +++ b/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx @@ -12,7 +12,7 @@ import "./ActionBlock.css"; export function ActionBlockHtml({ graph, block }: { graph: Graph; block: TGravityActionBlock }) { return ( - {block.anchors.map((anchor) => { + {block.anchors?.map((anchor) => { return ( - {block.meta.description} + {block.meta?.description} diff --git a/apps/storybook/src/stories/Playground/ActionBlock/index.tsx b/apps/storybook/src/stories/Playground/ActionBlock/index.tsx index a5ada043..b092cef2 100644 --- a/apps/storybook/src/stories/Playground/ActionBlock/index.tsx +++ b/apps/storybook/src/stories/Playground/ActionBlock/index.tsx @@ -9,7 +9,7 @@ import { ActionBlockHtml } from "./ActionBlockHtml"; import "./ActionBlock.css"; -function getAnchorY(index) { +function getAnchorY(index: number): number { let y = 18 * index; if (index >= 1) { y += 8 * index; @@ -65,7 +65,7 @@ export class ActionBlock extends CanvasBlock { } protected getAnchorsYOffter(type: EAnchorType) { - const anchors = this.connectedState.$state.value.anchors.filter((a) => a.type === type); + const anchors = (this.connectedState.$state.value.anchors ?? []).filter((a) => a.type === type); const { height } = this.getContentRect(); return (height - getAnchorY(anchors.length - 1)) / 2; } diff --git a/apps/storybook/src/stories/Playground/Editor/index.tsx b/apps/storybook/src/stories/Playground/Editor/index.tsx index fc9aa4be..26f2fc3c 100644 --- a/apps/storybook/src/stories/Playground/Editor/index.tsx +++ b/apps/storybook/src/stories/Playground/Editor/index.tsx @@ -36,18 +36,21 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor( props: ConfigEditorProps, ref: Ref ) { - const [errorMarker, setErrorMarker] = useState[0]>>(null); + const [errorMarker, setErrorMarker] = useState[0]> | null>(null); - const monacoRef = useRef[0]>(null); + const monacoRef = useRef[0] | null>(null); const valueRef = useRef<{ blocks: TBlock[]; connections: TConnection[] }>({ blocks: [], connections: [] }); useImperativeHandle(ref, () => ({ - scrollTo: (blockId: string) => { + scrollTo: (blockId: TBlockId) => { if (!monacoRef.current) { return; } const model = monacoRef.current.getModel(); + if (!model) { + return; + } const range = findBlockPositionsMonaco(model, blockId); if (range?.start.column) { @@ -66,6 +69,9 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor( return; } const model = monacoRef.current.getModel(); + if (!model) { + return; + } const edits = blocks.map((block) => { const range = findBlockPositionsMonaco(model, block.id); const text = JSON.stringify({ block: [block] }, null, 2); @@ -96,7 +102,11 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor( const applyChanges = useFn(() => { try { - const data = JSON.parse(monacoRef.current.getModel().getValue()); + const model = monacoRef.current?.getModel(); + if (!model) { + return; + } + const data = JSON.parse(model.getValue()); props?.onChange?.({ blocks: data.blocks, connections: data.conections }); } catch (e) { console.error(e); @@ -147,7 +157,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor( onClick={() => { monacoRef.current?.revealLinesInCenter(errorMarker.startLineNumber, errorMarker.endLineNumber, 0); - monacoRef.current.setSelection({ + monacoRef.current?.setSelection({ startColumn: errorMarker.startColumn, startLineNumber: errorMarker.startLineNumber, endColumn: errorMarker.endColumn, diff --git a/apps/storybook/src/stories/Playground/Editor/utils.ts b/apps/storybook/src/stories/Playground/Editor/utils.ts index f6e1c48b..b32a2d97 100644 --- a/apps/storybook/src/stories/Playground/Editor/utils.ts +++ b/apps/storybook/src/stories/Playground/Editor/utils.ts @@ -1,4 +1,10 @@ -export function findBlockPositionsMonaco(model, blockId) { +import type { TBlockId } from "@gravity-ui/graph"; +import type { IPosition, editor } from "monaco-editor/esm/vs/editor/editor.api"; + +export function findBlockPositionsMonaco( + model: editor.ITextModel, + blockId: TBlockId +): { start: IPosition; end: IPosition } { const configString = model.getValue(); const blockSearchStr = `"id": "${blockId}"`; const startIndex = configString.indexOf(blockSearchStr); diff --git a/apps/storybook/src/stories/Playground/GraphPlayground.tsx b/apps/storybook/src/stories/Playground/GraphPlayground.tsx index 015698bc..83d407b8 100644 --- a/apps/storybook/src/stories/Playground/GraphPlayground.tsx +++ b/apps/storybook/src/stories/Playground/GraphPlayground.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { ConnectionLayer, ECanDrag, Graph, GraphState, TBlock, TGraphConfig } from "@gravity-ui/graph"; +import { ConnectionLayer, ECanDrag, Graph, GraphState, TBlock, TGraphConfig, TPoint } from "@gravity-ui/graph"; import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph-react"; import { Flex, @@ -25,6 +25,7 @@ import { GravityActionBlockIS, GravityTextBlockIS, TGravityActionBlock, + TGravityActionBlockWithAnchors, TGravityTextBlock, createActionBlock, createTextBlock, @@ -119,7 +120,7 @@ const pointIcon = { }; // Function for drawing connection line -const drawLine = (start, end) => { +const drawLine = (start: TPoint, end: TPoint) => { const path = new Path2D(); path.moveTo(start.x, start.y); path.lineTo(end.x, end.y); @@ -149,7 +150,7 @@ export function GraphPLayground() { const updateVisibleConfig = useFn(() => { const config = graph.rootStore.getAsConfig(); - editorRef?.current.setContent({ + editorRef.current?.setContent({ blocks: config.blocks || [], connections: config.connections || [], }); @@ -170,15 +171,13 @@ export function GraphPLayground() { }); useGraphEvent(graph, "blocks-selection-change", ({ changes }) => { - editorRef?.current.updateBlocks([ - ...changes.add.map((id) => ({ - ...graph.rootStore.blocksList.getBlock(id), - selected: true, - })), - ...changes.removed.map((id) => ({ - ...graph.rootStore.blocksList.getBlock(id), - selected: false, - })), + const withSelection = (id: TBlock["id"], selected: boolean): TBlock[] => { + const block = graph.rootStore.blocksList.getBlock(id); + return block ? [{ ...block, selected }] : []; + }; + editorRef.current?.updateBlocks([ + ...changes.add.flatMap((id) => withSelection(id, true)), + ...changes.removed.flatMap((id) => withSelection(id, false)), ]); }); @@ -215,7 +214,7 @@ export function GraphPLayground() { if (targetBlockId) { return; } - let block: TBlock; + let block: TGravityActionBlockWithAnchors; const pullSourceAnchor = sourceAnchorId === undefined ? undefined @@ -242,7 +241,7 @@ export function GraphPLayground() { } graph.zoomTo([block.id], { transition: 250 }); updateVisibleConfig(); - editorRef?.current.scrollTo(block.id); + editorRef.current?.scrollTo(block.id); }); useLayoutEffect(() => { @@ -266,7 +265,7 @@ export function GraphPLayground() { graph.api.addBlock(block); graph.zoomTo([block.id], { transition: 250 }); updateVisibleConfig(); - editorRef?.current.scrollTo(block.id); + editorRef.current?.scrollTo(block.id); }); const renderBlockFn = useFn((graph: Graph, block: TBlock) => { @@ -286,7 +285,7 @@ export function GraphPLayground() { useGraphEvent(graph, "blocks-selection-change", ({ list }) => { if (list.length === 1) { - editorRef?.current.scrollTo(list[0]); + editorRef.current?.scrollTo(list[0]); } }); @@ -301,7 +300,7 @@ export function GraphPLayground() { return () => document.body.removeEventListener("keydown", fn); }); - const updateGraphSize = useFn, void>((value) => { + const updateGraphSize = useFn>, void>((value) => { let config: TGraphConfig; setGraphSize(value); @@ -325,6 +324,9 @@ export function GraphPLayground() { config = generatePlaygroundActionBlocks(50, 150); break; } + default: { + return; + } } setEntities(config); graph.zoomTo("center", { transition: 500 }); diff --git a/apps/storybook/src/stories/Playground/Settings.tsx b/apps/storybook/src/stories/Playground/Settings.tsx index 7dafe6da..800e4c33 100644 --- a/apps/storybook/src/stories/Playground/Settings.tsx +++ b/apps/storybook/src/stories/Playground/Settings.tsx @@ -26,7 +26,7 @@ const ConnectionArrowsVariants: SegmentedRadioGroupOptionProps[] = [ export function GraphSettings({ className, graph }: { className: string; graph: Graph }) { const rerender = useRerender(); - const settingBtnRef = useRef(); + const settingBtnRef = useRef(null); const [settingsOpened, setSettingsOpened] = useState(false); return ( <> diff --git a/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx b/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx index f9597ccf..a49681d4 100644 --- a/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx +++ b/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx @@ -15,7 +15,7 @@ export function TextBlockHtml({ graph, block }: { graph: Graph; block: TGravityT - {block.meta.text} + {block.meta?.text} {/* */} diff --git a/apps/storybook/src/stories/Playground/TextBlock/index.tsx b/apps/storybook/src/stories/Playground/TextBlock/index.tsx index 3bcc3d3f..e76b74b5 100644 --- a/apps/storybook/src/stories/Playground/TextBlock/index.tsx +++ b/apps/storybook/src/stories/Playground/TextBlock/index.tsx @@ -29,7 +29,7 @@ export class TextBlock extends CanvasBlock { protected renderName(ctx: CanvasRenderingContext2D) { ctx.fillStyle = "rgba(189, 142, 75, 1)"; ctx.textAlign = "center"; - this.renderText(this.state.meta.text, ctx); + this.renderText(this.state.meta?.text ?? "", ctx); } public renderHTML() { diff --git a/apps/storybook/src/stories/Playground/generateLayout.tsx b/apps/storybook/src/stories/Playground/generateLayout.tsx index 7627e816..d49708ec 100644 --- a/apps/storybook/src/stories/Playground/generateLayout.tsx +++ b/apps/storybook/src/stories/Playground/generateLayout.tsx @@ -1,7 +1,9 @@ -import { EAnchorType, TBlock, TGraphConfig } from "@gravity-ui/graph"; +import { EAnchorType, TAnchor, TBlock, TConnection } from "@gravity-ui/graph"; export const GravityActionBlockIS = "block-action"; export type TGravityActionBlock = TBlock<{ description: string }> & { is: typeof GravityActionBlockIS }; +/** Action block created by `createActionBlock`: anchors are always `[in, out]`. */ +export type TGravityActionBlockWithAnchors = TGravityActionBlock & { anchors: [TAnchor, TAnchor] }; export const GravityTextBlockIS = "block-text"; export type TGravityTextBlock = TBlock<{ text: string }> & { is: typeof GravityTextBlockIS }; @@ -10,7 +12,7 @@ function getActionBlockId(num: number): string { return `action_${num}`; } -export function createActionBlock(x: number, y: number, index: number): TGravityActionBlock { +export function createActionBlock(x: number, y: number, index: number): TGravityActionBlockWithAnchors { const blockId = getActionBlockId(index); return { @@ -40,7 +42,7 @@ export function createActionBlock(x: number, y: number, index: number): TGravity }; } -export function createTextBlock(x: number, y: number, width: number, index: number, text): TGravityTextBlock { +export function createTextBlock(x: number, y: number, width: number, index: number, text: string): TGravityTextBlock { const blockId = `text_${index}`; return { @@ -59,12 +61,14 @@ export function createTextBlock(x: number, y: number, width: number, index: numb }; } -function getRandomArbitrary(min, max) { +function getRandomArbitrary(min: number, max: number): number { return (Math.random() * (max - min) + min) | 0; } -export function generatePlaygroundActionBlocks(layersCount: number, connectionsPerLayer: number) { - const config: TGraphConfig = { +type TPlaygroundConfig = { blocks: TGravityActionBlock[]; connections: TConnection[] }; + +export function generatePlaygroundActionBlocks(layersCount: number, connectionsPerLayer: number): TPlaygroundConfig { + const config: TPlaygroundConfig = { blocks: [], connections: [], }; diff --git a/apps/storybook/src/stories/api/updateBlock/updateBlock.stories.tsx b/apps/storybook/src/stories/api/updateBlock/updateBlock.stories.tsx index b89ca9cb..d7d31175 100644 --- a/apps/storybook/src/stories/api/updateBlock/updateBlock.stories.tsx +++ b/apps/storybook/src/stories/api/updateBlock/updateBlock.stories.tsx @@ -20,7 +20,7 @@ const GraphApp = () => { const [height, setHeight] = useState(blockConfig.height.toString()); const onClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.updateBlock({ + graphRef.current?.api.updateBlock({ ...blockConfig, x: Number(x), y: Number(y), diff --git a/apps/storybook/src/stories/api/updateConnection/updateConnection.stories.tsx b/apps/storybook/src/stories/api/updateConnection/updateConnection.stories.tsx index 61517937..3bbaff62 100644 --- a/apps/storybook/src/stories/api/updateConnection/updateConnection.stories.tsx +++ b/apps/storybook/src/stories/api/updateConnection/updateConnection.stories.tsx @@ -51,7 +51,7 @@ const GraphApp = () => { const graphRef = useRef(undefined); const onConnectionChange: TOnConnectionChange = useCallback((connection) => { - graphRef.current.api.updateConnection("OneLeft:TwoRight", connection); + graphRef.current?.api.updateConnection("OneLeft:TwoRight", connection); }, []); const onSourceBlockUpdate: SelectProps["onUpdate"] = useCallback( diff --git a/apps/storybook/src/stories/api/updateGraphColors/updateGraphColors.stories.tsx b/apps/storybook/src/stories/api/updateGraphColors/updateGraphColors.stories.tsx index bd10f0dc..aaacb098 100644 --- a/apps/storybook/src/stories/api/updateGraphColors/updateGraphColors.stories.tsx +++ b/apps/storybook/src/stories/api/updateGraphColors/updateGraphColors.stories.tsx @@ -13,24 +13,24 @@ const GraphApp = () => { const [colors, setColors] = useState(); const graphRef = useRef(undefined); - const updateReactComponentsColors = useCallback((colors) => { + const updateReactComponentsColors = useCallback((colors: TGraphColorsPatch) => { setColors(colors); }, []); const onRedClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.updateGraphColors({ block: { background: "#febcb8" } }); + graphRef.current?.api.updateGraphColors({ block: { background: "#febcb8" } }); updateReactComponentsColors({ block: { background: "#febcb8" } }); }, [updateReactComponentsColors]); const onGreenClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.updateGraphColors({ block: { background: "#b7e6b0" } }); + graphRef.current?.api.updateGraphColors({ block: { background: "#b7e6b0" } }); updateReactComponentsColors({ block: { background: "#b7e6b0" } }); }, [updateReactComponentsColors]); const onYellowClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.updateGraphColors({ block: { background: "#fee0b0" } }); + graphRef.current?.api.updateGraphColors({ block: { background: "#fee0b0" } }); updateReactComponentsColors({ block: { background: "#fee0b0" } }); }, [updateReactComponentsColors]); diff --git a/apps/storybook/src/stories/api/zoomSpeed/zoomSpeed.stories.tsx b/apps/storybook/src/stories/api/zoomSpeed/zoomSpeed.stories.tsx index 0a848af3..b0ed01e7 100644 --- a/apps/storybook/src/stories/api/zoomSpeed/zoomSpeed.stories.tsx +++ b/apps/storybook/src/stories/api/zoomSpeed/zoomSpeed.stories.tsx @@ -19,7 +19,7 @@ const GraphApp = () => { const graphRef = useRef(undefined); const onApplyZoom: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.setConstants({ + graphRef.current?.setConstants({ camera: { SPEED: Number(speed), STEP: Number(step), @@ -28,7 +28,7 @@ const GraphApp = () => { }, [speed, step]); const onApplyPan: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.setConstants({ + graphRef.current?.setConstants({ camera: { PAN_SPEED: Number(panSpeed), }, diff --git a/apps/storybook/src/stories/api/zoomToBlocks/zoomToBlocks.stories.tsx b/apps/storybook/src/stories/api/zoomToBlocks/zoomToBlocks.stories.tsx index 36ad7a17..dc973f21 100644 --- a/apps/storybook/src/stories/api/zoomToBlocks/zoomToBlocks.stories.tsx +++ b/apps/storybook/src/stories/api/zoomToBlocks/zoomToBlocks.stories.tsx @@ -19,7 +19,7 @@ const GraphApp = () => { const graphRef = useRef(undefined); const onClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.zoomToBlocks(blocksId.split(/,\s?/g), { + graphRef.current?.api.zoomToBlocks(blocksId.split(/,\s?/g), { transition: Number(transition), padding: Number(padding), }); diff --git a/apps/storybook/src/stories/api/zoomToRect/zoomToRect.stories.tsx b/apps/storybook/src/stories/api/zoomToRect/zoomToRect.stories.tsx index a3e686d2..b347ccef 100644 --- a/apps/storybook/src/stories/api/zoomToRect/zoomToRect.stories.tsx +++ b/apps/storybook/src/stories/api/zoomToRect/zoomToRect.stories.tsx @@ -22,7 +22,7 @@ const GraphApp = () => { const graphRef = useRef(undefined); const onClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.zoomToRect( + graphRef.current?.api.zoomToRect( { height: Number(height), width: Number(width), x: Number(x), y: Number(y) }, { transition: Number(transition), padding: Number(padding) } ); diff --git a/apps/storybook/src/stories/api/zoomToViewPort/zoomToViewPort.stories.tsx b/apps/storybook/src/stories/api/zoomToViewPort/zoomToViewPort.stories.tsx index 6bab1712..b4f5f12f 100644 --- a/apps/storybook/src/stories/api/zoomToViewPort/zoomToViewPort.stories.tsx +++ b/apps/storybook/src/stories/api/zoomToViewPort/zoomToViewPort.stories.tsx @@ -18,7 +18,7 @@ const GraphApp = () => { const graphRef = useRef(undefined); const onClick: ButtonButtonProps["onClick"] = useCallback(() => { - graphRef.current.api.zoomToViewPort({ transition: Number(transition), padding: Number(padding) }); + graphRef.current?.api.zoomToViewPort({ transition: Number(transition), padding: Number(padding) }); }, [transition, padding]); return ( diff --git a/apps/storybook/src/stories/configurations/generatePretty.ts b/apps/storybook/src/stories/configurations/generatePretty.ts index b5fde3bd..4016965f 100644 --- a/apps/storybook/src/stories/configurations/generatePretty.ts +++ b/apps/storybook/src/stories/configurations/generatePretty.ts @@ -1,10 +1,10 @@ -import { CanvasBlock, TBlock, TGraphConfig } from "@gravity-ui/graph"; +import { CanvasBlock, TBlock, TConnection, TGraphConfig } from "@gravity-ui/graph"; import { random } from "../../storyUtils"; import { storiesSettings } from "./definitions"; -export function createBlock(x: number, y: number, index): TBlock { +export function createBlock(x: number, y: number, index: number | string): TBlock { const blockId = `block_${index}`; return { id: blockId, @@ -19,10 +19,13 @@ export function createBlock(x: number, y: number, index): TBlock { }; } -function getRandomArbitrary(min, max) { +function getRandomArbitrary(min: number, max: number): number { return (Math.random() * (max - min) + min) | 0; } +/** Generated config: `blocks` and `connections` are always present. */ +export type TGeneratedGraphConfig = TGraphConfig & { blocks: TBlock[]; connections: TConnection[] }; + type Props = { layersCount: number; connectionsPerLayer: number; @@ -37,8 +40,8 @@ export function generatePrettyBlocks({ dashedLine, overrideSettings = {}, startIndex = 0, -}: Props) { - const config: TGraphConfig = { +}: Props): TGeneratedGraphConfig { + const config: TGeneratedGraphConfig = { configurationName: "power of 2", blocks: [], connections: [], diff --git a/apps/storybook/src/stories/configurations/verticalGraph.ts b/apps/storybook/src/stories/configurations/verticalGraph.ts index b4ea0bd8..c68afcbe 100644 --- a/apps/storybook/src/stories/configurations/verticalGraph.ts +++ b/apps/storybook/src/stories/configurations/verticalGraph.ts @@ -1,4 +1,4 @@ -import { CanvasBlock as Block, TPoint } from "@gravity-ui/graph"; +import { CanvasBlock as Block, TGraphConfig, TPoint } from "@gravity-ui/graph"; import { storiesSettings } from "./definitions"; @@ -11,7 +11,7 @@ class VerticalBlock extends Block { } } -export const verticalGraphConfig = { +export const verticalGraphConfig: TGraphConfig = { configurationName: "simple vertical graph", blocks: [ { diff --git a/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx b/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx index f62a8304..127a8942 100644 --- a/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx +++ b/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { ConnectionLayer, Graph, TBlock } from "@gravity-ui/graph"; +import { ConnectionLayer, Graph, TBlock, TPoint } from "@gravity-ui/graph"; import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; import { Flex, Hotkey, Switch, Text, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; @@ -24,7 +24,7 @@ const GraphApp = () => { zoomTo("center", { padding: 300 }); }, [graph]); - const connectionLayerRef = useRef(null); + const connectionLayerRef = useRef(null); useLayoutEffect(() => { // Create icon for creating connections @@ -49,7 +49,7 @@ const GraphApp = () => { }; // Function for drawing connection line - const drawLine = (start, end) => { + const drawLine = (start: TPoint, end: TPoint) => { const path = new Path2D(); path.moveTo(start.x, start.y); path.lineTo(end.x, end.y); @@ -95,24 +95,26 @@ const GraphApp = () => { graph.on("connection-create-drop", (event) => { if (!event.detail.targetBlockId) { const source = graph.rootStore.blocksList.getBlock(event.detail.sourceBlockId); - const block = graph.api.addBlock({ - id: `${graph.rootStore.blocksList.$blocksMap.value.size}`, - name: `from ${source.name}`, - x: event.detail.point.x, - y: event.detail.point.y, - width: source.width, - height: source.height, - is: "block", - selected: false, - anchors: [], - }); - graph.api.addConnection({ - id: `${String(source.id)}-${String(block)}`, - sourceBlockId: event.detail.sourceBlockId, - sourceAnchorId: event.detail.sourceAnchorId, - targetBlockId: block, - targetAnchorId: "bottom", - }); + if (source) { + const block = graph.api.addBlock({ + id: `${graph.rootStore.blocksList.$blocksMap.value.size}`, + name: `from ${source.name}`, + x: event.detail.point.x, + y: event.detail.point.y, + width: source.width, + height: source.height, + is: "block", + selected: false, + anchors: [], + }); + graph.api.addConnection({ + id: `${String(source.id)}-${String(block)}`, + sourceBlockId: event.detail.sourceBlockId, + sourceAnchorId: event.detail.sourceAnchorId, + targetBlockId: block, + targetAnchorId: "bottom", + }); + } } console.log("Connection creation completed:", event.detail); }); @@ -120,10 +122,10 @@ const GraphApp = () => { const switchConnectionEnabled = useFn((enabled: boolean) => { if (enabled) { - connectionLayerRef.current.enable(); + connectionLayerRef.current?.enable(); setEnabled(true); } else { - connectionLayerRef.current.disable(); // Обратите внимание, что в оригинале метод назван disabled, а не disable + connectionLayerRef.current?.disable(); // Обратите внимание, что в оригинале метод назван disabled, а не disable setEnabled(false); } }); diff --git a/apps/storybook/src/stories/examples/imageByBlocks/imageByBlocks.stories.tsx b/apps/storybook/src/stories/examples/imageByBlocks/imageByBlocks.stories.tsx index 86207a20..e12e0713 100644 --- a/apps/storybook/src/stories/examples/imageByBlocks/imageByBlocks.stories.tsx +++ b/apps/storybook/src/stories/examples/imageByBlocks/imageByBlocks.stories.tsx @@ -17,7 +17,11 @@ type TBlockMeta = { class SpecificBlockView extends CanvasBlock> { public override renderSchematicView() { - this.context.ctx.fillStyle = this.state.meta.color; + const meta = this.state.meta; + if (!meta) { + return; + } + this.context.ctx.fillStyle = meta.color; this.context.ctx.fillRect(this.state.x, this.state.y, this.state.width, this.state.height); } } @@ -25,8 +29,8 @@ class SpecificBlockView extends CanvasBlock> { const SpecificBlockIs = "some-specific-view"; async function generateByImage() { - return new Promise((resolve) => { - const config: TGraphConfig> = { + return new Promise((resolve, reject) => { + const config: TGraphConfig> & { blocks: TBlock[] } = { configurationName: "power of 2", blocks: [], connections: [], @@ -54,6 +58,10 @@ async function generateByImage() { canvas.width = pic.width; canvas.height = pic.height; const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("imageByBlocks: 2D canvas context is not available")); + return; + } ctx.drawImage(pic, 0, 0); @@ -95,7 +103,9 @@ const GraphApp = () => { }, []); useEffect(() => { - generateByImage().then((newConfig) => setConfig(newConfig)); + generateByImage() + .then((newConfig) => setConfig(newConfig)) + .catch((error: unknown) => console.error(error)); }, []); if (!config) return null; diff --git a/apps/storybook/src/stories/examples/imageInsteadBlock/imageInsteadBlock.stories.tsx b/apps/storybook/src/stories/examples/imageInsteadBlock/imageInsteadBlock.stories.tsx index ce385ebe..05eda94e 100644 --- a/apps/storybook/src/stories/examples/imageInsteadBlock/imageInsteadBlock.stories.tsx +++ b/apps/storybook/src/stories/examples/imageInsteadBlock/imageInsteadBlock.stories.tsx @@ -34,26 +34,30 @@ type TBlockMeta = { class SpecificBlockView extends CanvasBlock> { public override renderSchematicView() { const blockMetaState = this.state.meta; + if (!blockMetaState) { + return; + } if (blockMetaState.shouldInitImage) { - blockMetaState.img = new Image(); + const img = new Image(); + blockMetaState.img = img; - blockMetaState.img.onload = () => { - const hRatio = this.state.width / blockMetaState.img.width; - const vRatio = this.state.height / blockMetaState.img.height; + img.onload = () => { + const hRatio = this.state.width / img.width; + const vRatio = this.state.height / img.height; const imageRatio = Math.min(hRatio, vRatio); - blockMetaState.imageWidth = blockMetaState.img.width * imageRatio; - blockMetaState.imageHeight = blockMetaState.img.width * imageRatio; + blockMetaState.imageWidth = img.width * imageRatio; + blockMetaState.imageHeight = img.width * imageRatio; blockMetaState.imageOffsetX = (this.state.width - blockMetaState.imageWidth) / 2; blockMetaState.imageOffsetY = (this.state.height - blockMetaState.imageHeight) / 2; this.context.ctx.drawImage( - blockMetaState.img, + img, 0, 0, - blockMetaState.img.width, - blockMetaState.img.height, + img.width, + img.height, this.state.x + blockMetaState.imageOffsetX, this.state.y + blockMetaState.imageOffsetY, blockMetaState.imageWidth, @@ -62,8 +66,8 @@ class SpecificBlockView extends CanvasBlock> { blockMetaState.shouldInitImage = false; }; - blockMetaState.img.src = getImageByStatus(this.state.meta.status); - } else { + img.src = getImageByStatus(blockMetaState.status); + } else if (blockMetaState.img) { this.context.ctx.drawImage( blockMetaState.img, 0, @@ -81,7 +85,7 @@ class SpecificBlockView extends CanvasBlock> { const SpecificBlockIs = "some-specific-view"; -function getImageByStatus(status: EBlockStatus) { +function getImageByStatus(status: EBlockStatus | undefined): string { switch (status) { case EBlockStatus.DONE: { return imageDone; @@ -106,35 +110,38 @@ const GraphApp = () => { useEffect(() => { const newConfig = generatePrettyBlocks({ layersCount: 10, connectionsPerLayer: 100, dashedLine: true }); - newConfig.settings.blockComponents = {}; - newConfig.settings.blockComponents[SpecificBlockIs] = SpecificBlockView; + newConfig.settings = { + ...newConfig.settings, + blockComponents: { [SpecificBlockIs]: SpecificBlockView }, + }; - newConfig.blocks.forEach((block: TBlock) => { + newConfig.blocks.forEach((block) => { block.is = SpecificBlockIs; const randomVal = Math.floor(Math.random() * 4) + 1; - block.meta = { + const meta: TBlockMeta = { shouldInitImage: true, imageWidth: 1, imageHeight: 1, imageOffsetX: 1, imageOffsetY: 1, }; + block.meta = meta; switch (randomVal) { case 1: { - block.meta.status = EBlockStatus.DONE; + meta.status = EBlockStatus.DONE; break; } case 2: { - block.meta.status = EBlockStatus.FAIL; + meta.status = EBlockStatus.FAIL; break; } case 3: { - block.meta.status = EBlockStatus.RUNNING; + meta.status = EBlockStatus.RUNNING; break; } case 4: default: { - block.meta.status = EBlockStatus.WAITING; + meta.status = EBlockStatus.WAITING; break; } } diff --git a/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx b/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx index 772bb7f9..157f03d9 100644 --- a/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx +++ b/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx @@ -20,7 +20,7 @@ const GraphApp = () => { zoomTo("center", { padding: 300 }); }, [graph]); - const newBlockLayerRef = useRef(null); + const newBlockLayerRef = useRef(null); useLayoutEffect(() => { newBlockLayerRef.current = addLayer(NewBlockLayer, {}); @@ -33,10 +33,10 @@ const GraphApp = () => { const switchNewBlockEnabled = useFn((addEnabled: boolean) => { if (addEnabled) { - newBlockLayerRef.current.enable(); + newBlockLayerRef.current?.enable(); setEnabled(true); } else { - newBlockLayerRef.current.disable(); + newBlockLayerRef.current?.disable(); setEnabled(false); } }); diff --git a/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx b/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx index a96ba26e..a10179a9 100644 --- a/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx +++ b/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx @@ -9,6 +9,7 @@ import { IPortConnectionMeta, PortConnectionLayer, TBlock, + TPoint, createAnchorPortId, } from "@gravity-ui/graph"; import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; @@ -239,7 +240,7 @@ const GraphApp = () => { zoomTo("center", { padding: 300 }); }, [graph]); - const layerRef = useRef(null); + const layerRef = useRef(null); useLayoutEffect(() => { // Create icon for creating connections @@ -264,7 +265,7 @@ const GraphApp = () => { }; // Function for drawing connection line - const drawLine = (start, end) => { + const drawLine = (start: TPoint, end: TPoint) => { const path = new Path2D(); path.moveTo(start.x, start.y); path.lineTo(end.x, end.y); diff --git a/apps/storybook/src/stories/examples/themeChange/themeChange.stories.tsx b/apps/storybook/src/stories/examples/themeChange/themeChange.stories.tsx index b7904f8e..b312d335 100644 --- a/apps/storybook/src/stories/examples/themeChange/themeChange.stories.tsx +++ b/apps/storybook/src/stories/examples/themeChange/themeChange.stories.tsx @@ -52,11 +52,11 @@ const graphColors: TGraphColorsPatch = { }; function parseTheme(colors: TGraphColorsPatch): TGraphColorsPatch { - const parsed: TGraphColorsPatch = {}; + const parsed: Record> = {}; - for (const element in colors) { + for (const [element, group] of Object.entries(colors)) { parsed[element] = {}; - Object.keys(colors[element]).forEach((key) => (parsed[element][key] = computeCssVariable(colors[element][key]))); + Object.entries(group ?? {}).forEach(([key, value]) => (parsed[element][key] = computeCssVariable(value))); } return parsed; @@ -77,7 +77,7 @@ const GraphApp = () => { // The values of css variables occur after the theme is changed. useEffect(() => { - graphRef.current.api.updateGraphColors(parseTheme(graphColors)); + graphRef.current?.api.updateGraphColors(parseTheme(graphColors)); }, [theme]); return ( diff --git a/apps/storybook/src/stories/main/GraphEditor.stories.tsx b/apps/storybook/src/stories/main/GraphEditor.stories.tsx index d3dd60cb..fdb557fc 100644 --- a/apps/storybook/src/stories/main/GraphEditor.stories.tsx +++ b/apps/storybook/src/stories/main/GraphEditor.stories.tsx @@ -91,40 +91,40 @@ export default meta; type Story = StoryObj; export const OneBlock: Story = { - render: (args) => , + render: (args) => , }; export const OneStraightConnection: Story = { args: { useBezierConnections: false, }, - render: (args) => , + render: (args) => , }; export const OneBezierConnection: Story = { - render: (args) => , + render: (args) => , }; export const ColoredConnections: Story = { - render: (args) => , + render: (args) => , }; export const WithAnchors: Story = { - render: (args) => , + render: (args) => , }; export const HundredBlocks: Story = { render: (args) => ( ), }; export const ThousandBlocks: Story = { render: (args) => ( - + ), }; @@ -133,7 +133,7 @@ export const FiveThousandsBlocks: Story = { useBezierConnections: false, }, render: (args) => ( - + ), }; @@ -144,12 +144,12 @@ export const GraphStressTest: Story = { render: (args) => { return ( ); }, @@ -158,40 +158,40 @@ export const NirvanaMaxGraphTest: Story = { render: (args) => { return ( ); }, }; export const CustomSchematicBlock: Story = { - render: (args) => , + render: (args) => , }; export const CustomLayer: Story = { - render: (args) => , + render: (args) => , }; export const VerticalGraph: Story = { - render: (args) => , + render: (args) => , }; export const SnappingGraph: Story = { render: (args) => ( ), }; export const EmptyGraph: Story = { - render: (args) => , + render: (args) => , parameters: { docs: { description: { diff --git a/apps/storybook/src/stories/main/GraphEditor.tsx b/apps/storybook/src/stories/main/GraphEditor.tsx index 01521cf9..c1229ff5 100644 --- a/apps/storybook/src/stories/main/GraphEditor.tsx +++ b/apps/storybook/src/stories/main/GraphEditor.tsx @@ -9,15 +9,16 @@ import { BlockStory } from "./Block"; export type TGraphComponentProps = { config: TGraphConfig; - graphRef?: React.MutableRefObject; + graphRef?: React.MutableRefObject; colors?: TGraphColorsPatch; constants?: NonNullable["constants"]; - renderBlock?: (graphObject: Graph, block: T) => React.JSX.Element; + /** Return `null` to fall back to the default `BlockStory`. */ + renderBlock?: (graphObject: Graph, block: T) => React.JSX.Element | null; }; const action = (name: string) => - (...args) => { + (...args: unknown[]) => { console.log(name, "[", ...args, "]"); }; diff --git a/apps/storybook/src/stories/main/ToolboxLayer.tsx b/apps/storybook/src/stories/main/ToolboxLayer.tsx index ebb776e7..17df1412 100644 --- a/apps/storybook/src/stories/main/ToolboxLayer.tsx +++ b/apps/storybook/src/stories/main/ToolboxLayer.tsx @@ -1,6 +1,6 @@ import React, { ChangeEvent, useCallback } from "react"; -import { Graph, Layer } from "@gravity-ui/graph"; +import { CoreComponent, Graph, Layer, LayerProps } from "@gravity-ui/graph"; import ReactDOM from "react-dom/client"; function Toolbox(props: { graph: Graph }) { @@ -18,18 +18,18 @@ function Toolbox(props: { graph: Graph }) { } export class ToolboxLayer extends Layer { - protected reactRoot: ReactDOM.Root; + protected reactRoot?: ReactDOM.Root; - constructor(props, context) { + constructor(props: LayerProps, parent?: CoreComponent) { super( { html: { zIndex: 300, - className: "custom-control", + classNames: ["custom-control"], }, ...props, }, - context + parent ); } diff --git a/apps/storybook/src/stories/plugins/elk/elk.stories.tsx b/apps/storybook/src/stories/plugins/elk/elk.stories.tsx index 880ac53f..2e1cefe3 100644 --- a/apps/storybook/src/stories/plugins/elk/elk.stories.tsx +++ b/apps/storybook/src/stories/plugins/elk/elk.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo } from "react"; import type { TMultipointConnection } from "@gravity-ui/graph"; -import { Graph, GraphState, MultipointConnection, TBlock, TConnection, TGraphConfig } from "@gravity-ui/graph"; +import { Graph, GraphState, MultipointConnection, TBlock, TConnection } from "@gravity-ui/graph"; import { GraphCanvas, useElk, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import { ThemeProvider } from "@gravity-ui/uikit"; import { Description, Meta as StorybookMeta, Title } from "@storybook/addon-docs/blocks"; @@ -9,6 +9,7 @@ import type { Meta, StoryFn } from "@storybook/react-webpack5"; import ELK, { ElkNode } from "elkjs"; import { useFn } from "../../../useFn"; +import { TGeneratedGraphConfig } from "../../configurations/generatePretty"; import { BlockStory } from "../../main/Block"; import { getExampleConfig } from "./getExampleConfig"; @@ -29,8 +30,8 @@ export enum Algorithm { import "@gravity-ui/uikit/styles/styles.css"; interface GraphAppProps { - elkConfig?: ElkNode; - graphConfig?: TGraphConfig; + elkConfig: ElkNode; + graphConfig: TGeneratedGraphConfig; } const GraphApp = ({ elkConfig, graphConfig }: GraphAppProps) => { @@ -50,7 +51,7 @@ const GraphApp = ({ elkConfig, graphConfig }: GraphAppProps) => { const connections = graphConfig.connections.reduce< (TConnection & Pick)[] >((acc, connection) => { - if (connection.id in result.edges) { + if (connection.id !== undefined && connection.id in result.edges) { acc.push({ ...connection, ...result.edges[connection.id], diff --git a/apps/storybook/src/stories/plugins/elk/generateExampleTree.ts b/apps/storybook/src/stories/plugins/elk/generateExampleTree.ts index 04b8659e..0a86d563 100644 --- a/apps/storybook/src/stories/plugins/elk/generateExampleTree.ts +++ b/apps/storybook/src/stories/plugins/elk/generateExampleTree.ts @@ -1,9 +1,7 @@ -import { TGraphConfig } from "@gravity-ui/graph"; +import { TGeneratedGraphConfig, createBlock } from "../../configurations/generatePretty"; -import { createBlock } from "../../configurations/generatePretty"; - -export function generateExampleTree(levels: number) { - const tree: Pick = { +export function generateExampleTree(levels: number): TGeneratedGraphConfig { + const tree: TGeneratedGraphConfig = { blocks: [], connections: [], }; diff --git a/apps/storybook/src/stories/plugins/elk/getExampleConfig.ts b/apps/storybook/src/stories/plugins/elk/getExampleConfig.ts index 5c4ec1e6..b811e61c 100644 --- a/apps/storybook/src/stories/plugins/elk/getExampleConfig.ts +++ b/apps/storybook/src/stories/plugins/elk/getExampleConfig.ts @@ -1,15 +1,15 @@ -import { TGraphConfig } from "@gravity-ui/graph"; +import { TBlock, TConnection } from "@gravity-ui/graph"; import { ElkExtendedEdge, ElkNode, LayoutOptions } from "elkjs"; import { measureText } from "../../../storyUtils"; -import { createBlock, generatePrettyBlocks } from "../../configurations/generatePretty"; +import { TGeneratedGraphConfig, createBlock, generatePrettyBlocks } from "../../configurations/generatePretty"; import { Algorithm } from "./elk.stories"; import { generateExampleTree } from "./generateExampleTree"; const FONT_SIZE = 14; -const prepareChildren = (blocks: TGraphConfig["blocks"]) => { +const prepareChildren = (blocks: TBlock[]) => { return blocks.map((b) => { return { id: b.id as string, @@ -19,7 +19,7 @@ const prepareChildren = (blocks: TGraphConfig["blocks"]) => { }); }; -const prepareEdges = (connections: TGraphConfig["connections"], skipLabels?: boolean) => { +const prepareEdges = (connections: TConnection[], skipLabels?: boolean) => { return connections.map((c, i) => { const labelText = `label ${i}`; @@ -87,7 +87,7 @@ const ElkOptionalMap: Record = { }, }; -export const getExampleConfig = (algorithm: Algorithm): { elkConfig: ElkNode; graphConfig: TGraphConfig } => { +export const getExampleConfig = (algorithm: Algorithm): { elkConfig: ElkNode; graphConfig: TGeneratedGraphConfig } => { let config = generatePrettyBlocks({ layersCount: 10, connectionsPerLayer: 30, dashedLine: true }); const elkConfig = { diff --git a/apps/storybook/tsconfig.json b/apps/storybook/tsconfig.json index 672d114b..68afa0ed 100644 --- a/apps/storybook/tsconfig.json +++ b/apps/storybook/tsconfig.json @@ -15,7 +15,7 @@ "types": [ "node" ], - "strict": false, + "strict": true, "noUncheckedSideEffectImports": true }, "include": [