From c928f983917929858be5fc9f99bf7475cda8de66 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 11 Nov 2026 16:59:48 +0800 Subject: [PATCH 1/5] fix: restore React Viewer render order for 0.1.1 Restore mixed node and group title ordering with regression coverage. Align release automation with the published version baseline. --- .github/workflows/release.yml | 41 ++- .gitignore | 3 + .../2026-11-11-viewer-render-order.json | 10 + common/config/rush/version-policies.json | 2 +- .../guide/en/react-node-spec/react-viewer.md | 22 ++ .../guide/zh/react-node-spec/react-viewer.md | 21 +- .../examples/viewer/render_order.tsx | 148 ++++++++++ packages/react-vgraph/src/viewer/index.tsx | 255 +++++++++--------- .../test/viewer_render_order.spec.tsx | 164 +++++++++++ packages/react-vgraph/tsconfig.json | 2 +- packages/react-vgraph/tsconfig.test.json | 6 +- 11 files changed, 537 insertions(+), 137 deletions(-) create mode 100644 common/changes/@visactor/react-vgraph/2026-11-11-viewer-render-order.json create mode 100644 packages/react-vgraph/examples/viewer/render_order.tsx create mode 100644 packages/react-vgraph/test/viewer_render_order.spec.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56e63197c..53e32b5f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,16 +137,21 @@ jobs: const path = require('path'); const { execSync } = require('child_process'); - const baseDir = path.join(process.cwd(), 'common', 'changes', '@visactor', 'vgraph'); + const baseDir = path.join(process.cwd(), 'common', 'changes'); let changefiles = []; try { - const files = fs.readdirSync(baseDir).filter(name => name.endsWith('.json')); - for (const file of files) { - const fullPath = path.join(baseDir, file); - const json = JSON.parse(fs.readFileSync(fullPath, 'utf8')); - changefiles.push(json); + function collectChangefiles(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + collectChangefiles(fullPath); + } else if (entry.name.endsWith('.json')) { + changefiles.push(JSON.parse(fs.readFileSync(fullPath, 'utf8'))); + } + } } + collectChangefiles(baseDir); console.log('Collected changefiles:', changefiles.length); } catch (e) { console.log('No changefiles found or unable to read, fallback to empty list.', e.message || e); @@ -408,6 +413,9 @@ jobs: echo "Build packages: ${package_names[*]}" node common/scripts/install-run-rush.js build "${build_args[@]}" + - name: Test packages + run: node common/scripts/install-run-rush.js test --only tag:package + - name: Check npm versions (release) if: startsWith(github.ref_name, 'release/') id: npm_version_release @@ -624,8 +632,11 @@ jobs: rm -rf .changelog find docs/assets/changelog -name '*.bak' -delete 2>/dev/null || true git add . - git commit -m "build: release version ${{ steps.package_version_release.outputs.current_version }} [skip ci]" -n - git push --no-verify origin ${{ github.ref_name }} + git commit -m "$(cat <<'EOF' + build: release version ${{ steps.package_version_release.outputs.current_version }} [skip ci] + EOF + )" + git push origin ${{ github.ref_name }} fi - name: Commit & Push changes (hotfix) @@ -636,8 +647,11 @@ jobs: echo 'No changes to commit for hotfix branch.' else git add . - git commit -m "build: hotfix version ${{ steps.package_version_hotfix.outputs.current_version }} [skip ci]" -n - git push --no-verify origin ${{ github.ref_name }} + git commit -m "$(cat <<'EOF' + build: hotfix version ${{ steps.package_version_hotfix.outputs.current_version }} [skip ci] + EOF + )" + git push origin ${{ github.ref_name }} fi - name: Commit & Push changes (pre-release) @@ -648,8 +662,11 @@ jobs: echo 'No changes to commit for pre-release branch.' else git add . - git commit -m "build: prerelease version ${{ steps.package_version_prerelease.outputs.current_version }} [skip ci]" -n - git push --no-verify origin ${{ github.ref_name }} + git commit -m "$(cat <<'EOF' + build: prerelease version ${{ steps.package_version_prerelease.outputs.current_version }} [skip ci] + EOF + )" + git push origin ${{ github.ref_name }} fi - name: Create Pull Request to main (release) diff --git a/.gitignore b/.gitignore index 2ea263cef..fe262032d 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,6 @@ config/secrets.yml logs/ __pycache__/ *.pyc + +# Local Run button configuration +.trae/environments/ diff --git a/common/changes/@visactor/react-vgraph/2026-11-11-viewer-render-order.json b/common/changes/@visactor/react-vgraph/2026-11-11-viewer-render-order.json new file mode 100644 index 000000000..5db8901ab --- /dev/null +++ b/common/changes/@visactor/react-vgraph/2026-11-11-viewer-render-order.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/react-vgraph", + "comment": "Restore the optional Viewer setRenderOrder callback for mixed node and group title rendering, with regression tests and a comparison example.", + "type": "patch" + } + ], + "packageName": "@visactor/react-vgraph" +} diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 8daef7396..598050376 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vgraphMain","version":"0.2.0","mainProject":"@visactor/vgraph","nextBump":"minor"}] +[{"definitionName":"lockStepVersion","policyName":"vgraphMain","version":"0.1.0","mainProject":"@visactor/vgraph","nextBump":"patch"}] diff --git a/docs/assets/guide/en/react-node-spec/react-viewer.md b/docs/assets/guide/en/react-node-spec/react-viewer.md index 551f48577..b28d860d8 100755 --- a/docs/assets/guide/en/react-node-spec/react-viewer.md +++ b/docs/assets/guide/en/react-node-spec/react-viewer.md @@ -45,6 +45,7 @@ export default () => { | setAnchor | (node: Node, anchor: AnchorConfigs) => ReactNode | The React anchor component. | | setAnchorClassName | (node: Node) => string \| string[] | Adds a `className` to the anchor container. | | setGroupTitle | (group: Group) => ReactNode | \[NEW] The React group title component. Please configure `titleSize` on the group to use it. | +| setRenderOrder | (items: (Node \| Group)[]) => (Node \| Group)[] | Customize the DOM order of visible nodes and group titles. Defaults to group titles followed by nodes. Receives a new mixed array and returns the desired order. | | localRendering | boolean | Whether to enable local rendering. When enabled, nodes only support stateless components. When disabled, they support stateful components. Default is enabled. | | hideDetails | object | When the zoom ratio is very small, there may be many nodes on the screen, and the content of these nodes is basically unreadable. You can configure this option to display summary information and improve fluency. | | hideDetails.ratio | number | Hides node details when the zoom ratio is less than `ratio`. | @@ -52,6 +53,27 @@ export default () => { | responsiveNode | boolean | Whether to enable responsive React nodes. If enabled, it will respond to changes in the width and height of React nodes and trigger the redrawing of connection positions. | | onResizeNode | (node: Node, bbox: BBox) => void | The callback function for when the width and height of a node change. | +### Custom Node and Group Title Order + +```tsx + items.reverse()} +/> +``` + +This reverses the default group-title-then-node order. The callback receives a +new array of visible elements, so in-place sorting does not mutate the Viewer's +node or group state arrays. It may run on every render, including an initial +empty array. Return the desired order without updating React state inside the +callback. Later elements usually cover earlier ones in the same stacking context; +explicit CSS stacking rules can change the final overlap. + +The local `render_order.html` example compares both orders and verifies callback +execution and DOM order. + ### Viewer ref Methods | Method | Type | Description | | --- | --- | --- | diff --git a/docs/assets/guide/zh/react-node-spec/react-viewer.md b/docs/assets/guide/zh/react-node-spec/react-viewer.md index 22898fcd8..047f972f5 100644 --- a/docs/assets/guide/zh/react-node-spec/react-viewer.md +++ b/docs/assets/guide/zh/react-node-spec/react-viewer.md @@ -46,6 +46,7 @@ export default () => { | setAnchor | (node: Node, anchor: AnchorConfigs) => ReactNode | React 锚点组件 | | setAnchorClassName | (node: Node) => string \| string\[\] | 给锚点容器添加 className | | setGroupTitle | (group: Group) => ReactNode | \[NEW\] React 分组标题组件。请在 group 上配置 `titleSize` 配合使用。 | +| setRenderOrder | (items: (Node \| Group)[]) => (Node \| Group)[] | 自定义可见节点与分组标题的 DOM 顺序。默认先分组标题、后节点;回调接收新的混合数组,返回期望的排列。 | | localRendering | boolean | 是否开启局部渲染,开启后节点仅支持无状态组件,关闭后支持有状态组件。默认开启。 | | hideDetails | object | 当缩放比很小时屏幕中可能有很多节点,而这些节点内容基本看不清,可以配置此选项显示概要信息并提升流畅度 | | hideDetails.ratio | number | 当缩放比小于 ratio 时隐藏节点细节 | @@ -53,8 +54,26 @@ export default () => { | responsiveNode | boolean | 是否开启响应式 React 节点,如果开启,则会响应 React 节点的宽高变化并触发连线位置的重新绘制。 | | onResizeNode | (node: Node, bbox: BBox) => void | 节点宽高发生变化时的回调函数。 | +### 自定义节点与分组标题的顺序 + +```tsx + items.reverse()} +/> +``` + +这里将默认的“分组标题 → 节点”反转。回调接收当前可见元素的新数组, +原地排序不会改变 Viewer 的节点或分组状态数组。回调可能在每次渲染时执行, +包括初次渲染的空数组,应只返回排列结果,避免在其中更新 React 状态。 +同一层叠上下文中,后面的元素通常覆盖前面的元素;显式 CSS 层级仍会影响最终遮挡。 + +本地 `render_order.html` 示例提供默认与反转顺序的对照,并校验回调与 DOM 顺序。 + ### Viewer ref 方法 | 方法 | 类型 | 描述 | | --------- | ------------------------------------------------------------ | -------------------------------------------------- | | refresh | (forced=false) => void; | 刷新 react 视图。Viewer 仅会在 graph 发生平移缩放时进行自动刷新,其余情况请按需手动刷新。当数据刷新时建议配置 force 为 true 强制刷新一次。 | -| updateSize | () => void; | 当图实例的大小发生变化时请调用 updateSize 保持组件的大小一致 | \ No newline at end of file +| updateSize | () => void; | 当图实例的大小发生变化时请调用 updateSize 保持组件的大小一致 | diff --git a/packages/react-vgraph/examples/viewer/render_order.tsx b/packages/react-vgraph/examples/viewer/render_order.tsx new file mode 100644 index 000000000..8f45e5167 --- /dev/null +++ b/packages/react-vgraph/examples/viewer/render_order.tsx @@ -0,0 +1,148 @@ +import React, { useEffect, useRef, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { Graph, Group, Node } from "@visactor/vgraph"; +import { Viewer } from "../../src/viewer"; + +function OrderPanel({ reverse }: { reverse: boolean }) { + const container = useRef(null); + const calls = useRef(0); + const [graph, setGraph] = useState(null); + const [result, setResult] = useState("等待渲染"); + const [clicked, setClicked] = useState(""); + + useEffect(() => { + const element = container.current!; + const { width, height } = element.getBoundingClientRect(); + const g = new Graph({ + container: element, + width, + height, + renderMode: "dom", + setDefaultNode: () => ({ width: 200, height: 56 }), + setDefaultGroup: () => ({ + titleSize: 56, + padding: [20, 20, 20, 20], + fillStyle: "#f1f5f9", + strokeStyle: "#cbd5e1", + }), + }); + g.data({ + nodes: [ + { id: "inside", x: width / 2, y: height / 2 + 30 }, + { id: "overlay", x: width / 2, y: height / 2 }, + ], + edges: [], + groups: [{ id: "group", children: ["inside"] }], + }); + g.refresh(); + const title = g.getGroupById("group")!.getTitleBBox(); + g.update(g.getNodeById("overlay")!, { + x: title.left + title.width / 2, + y: title.top + title.height / 2, + }); + g.refresh(); + setGraph(g); + return () => g.destroy(); + }, []); + + useEffect(() => { + if (!graph) { + return; + } + const frame = requestAnimationFrame(() => { + const viewport = container.current!.querySelector( + ".vgraph-viewer-viewport" + )!; + const order = Array.from(viewport.children).map((child) => + child.classList.contains("vgraph-viewer-group-title") + ? "group" + : child.querySelector("[data-id]")!.getAttribute("data-id") + ); + const expected = reverse + ? ["overlay", "inside", "group"] + : ["group", "inside", "overlay"]; + const passed = + JSON.stringify(order) === JSON.stringify(expected) && + (reverse ? calls.current > 0 : calls.current === 0); + console.assert(passed, "Viewer 渲染顺序校验失败", { order, expected }); + setResult( + `${passed ? "通过" : "失败"} · 回调 ${calls.current} 次 · ${order.join( + " → " + )}` + ); + }); + return () => cancelAnimationFrame(frame); + }, [graph, reverse]); + + function reverseOrder(items: (Node | Group)[]) { + calls.current += 1; + return items.reverse(); + } + + return ( +
+

{reverse ? "反转:节点 → 分组标题" : "默认:分组标题 → 节点"}

+

{reverse ? "重叠处由分组标题覆盖" : "重叠处由节点覆盖"}

+
+ {graph && ( + ( + + )} + setGroupTitle={() => ( + + )} + setRenderOrder={reverse ? reverseOrder : undefined} + /> + )} +
+

{result}

+

点击结果:{clicked || "请点击重叠区域"}

+
+ ); +} + +createRoot(document.getElementById("root")!).render( +
+

节点与分组渲染顺序对照

+

+ 两侧使用相同数据和位置。右侧将可见元素列表反转;下方检查实际 DOM + 顺序与回调执行,点击重叠区域可对比覆盖效果。 +

+
+ + +
+
+); diff --git a/packages/react-vgraph/src/viewer/index.tsx b/packages/react-vgraph/src/viewer/index.tsx index 64e809e78..cc51e3578 100644 --- a/packages/react-vgraph/src/viewer/index.tsx +++ b/packages/react-vgraph/src/viewer/index.tsx @@ -90,6 +90,11 @@ export interface ViewerProps { * 当批量节点大小发生变化的回调,与 `adjustNodeSize` 配合使用。 */ onNodeDefaultSizeChanged?: (nodes: Node[]) => void; + /** + * Customize the render order of visible nodes and group titles. + * 自定义可见节点与分组标题的渲染顺序,默认先分组标题、后节点。 + */ + setRenderOrder?: (items: (Node | Group)[]) => (Node | Group)[]; } type ReactAnchorConfigs = { @@ -530,142 +535,151 @@ export class Viewer extends React.Component { this.refresh(true); } - render() { + renderNode(node: Node) { const { graph, hideDetails, setNodeClassName, setAnchorClassName, adjustNodeSize, - setGroupTitle, } = this.props; - const { showNodes, showGroups, matrix, showDetails, showAnchors } = - this.state; - const container = graph.get("container"); - if (!graph) { - return null; - } - const nodes = showNodes.map((node: Node) => { - const { width, height, x, y, id } = node.configs; - let styles = { - width, - height, - left: x - width / 2, - top: y - height / 2, - cursor: node.configs.moving ? "grabbing" : "default", - }; - let content = null; - if (showDetails) { - content = this.props.setNode(node); - } else { - if (hideDetails?.getNodeStyles) { - styles = { - ...hideDetails.getNodeStyles(node), - ...styles, - }; - } + const { showDetails, showAnchors } = this.state; + const { width, height, x, y, id } = node.configs; + let styles = { + width, + height, + left: x - width / 2, + top: y - height / 2, + cursor: node.configs.moving ? "grabbing" : "default", + }; + let content = null; + if (showDetails) { + content = this.props.setNode(node); + } else { + if (hideDetails?.getNodeStyles) { + styles = { + ...hideDetails.getNodeStyles(node), + ...styles, + }; } - let classNames: any = []; - if (setNodeClassName) { - classNames = setNodeClassName(node); - if (!Array.isArray(classNames)) { - classNames = [classNames]; - } + } + let classNames: any = []; + if (setNodeClassName) { + classNames = setNodeClassName(node); + if (!Array.isArray(classNames)) { + classNames = [classNames]; } + } - classNames.unshift("vgraph-viewer-node"); - const layer = node.layer; - const showNodeAnchors = showAnchors[id]; - let anchorContents: any = []; - if (showNodeAnchors) { - anchorContents = showNodeAnchors.map( - (anchorConfigs: ReactAnchorConfigs) => { - const size = anchorConfigs.configs.size ?? 8; - const offsets = anchorConfigs.configs.offsets || [0, 0]; - const styles = { - width: size, - height: size, - left: - anchorConfigs.position[0] - size / 2 + width / 2 + offsets[0], - top: - anchorConfigs.position[1] - size / 2 + height / 2 + offsets[1], - }; - let anchorContent = null; - anchorContent = this.props.setAnchor?.(node, anchorConfigs.configs); - if (!anchorContent) { - return null; - } - let classNames: any = []; - if (setAnchorClassName) { - classNames = setAnchorClassName(node, anchorConfigs.configs); - if (!Array.isArray(classNames)) { - classNames = [classNames]; - } + classNames.unshift("vgraph-viewer-node"); + const layer = node.layer; + const showNodeAnchors = showAnchors[id]; + let anchorContents: any = []; + if (showNodeAnchors) { + anchorContents = showNodeAnchors.map( + (anchorConfigs: ReactAnchorConfigs) => { + const size = anchorConfigs.configs.size ?? 8; + const offsets = anchorConfigs.configs.offsets || [0, 0]; + const styles = { + width: size, + height: size, + left: anchorConfigs.position[0] - size / 2 + width / 2 + offsets[0], + top: anchorConfigs.position[1] - size / 2 + height / 2 + offsets[1], + }; + let anchorContent = null; + anchorContent = this.props.setAnchor?.(node, anchorConfigs.configs); + if (!anchorContent) { + return null; + } + let classNames: any = []; + if (setAnchorClassName) { + classNames = setAnchorClassName(node, anchorConfigs.configs); + if (!Array.isArray(classNames)) { + classNames = [classNames]; } - classNames.unshift("vgraph-viewer-anchor"); - return ( -
- {anchorContent} -
- ); } - ); - } - return ( -
-
{ - if (adjustNodeSize && this.nodes.current[id] !== false) { - this.nodes.current[id] = ref; - } - }} - > - {content} -
- {anchorContents} -
+ classNames.unshift("vgraph-viewer-anchor"); + return ( +
+ {anchorContent} +
+ ); + } ); - }); - - const groups = showGroups.map((group: Group) => { - const bbox = group.get("__titleBBox"); - const lineWidth = group.get("strokeStyle") - ? group.get("lineWidth") / 2 || 0.5 - : 0; - return ( + } + return ( +
{ + if (adjustNodeSize && this.nodes.current[id] !== false) { + this.nodes.current[id] = ref; + } }} - className="vgraph-viewer-group-title" > - {setGroupTitle!(group)} + {content}
- ); - }); + {anchorContents} +
+ ); + } + + renderGroup(group: Group) { + const { graph, setGroupTitle } = this.props; + const bbox = group.get("__titleBBox"); + const lineWidth = group.get("strokeStyle") + ? group.get("lineWidth") / 2 || 0.5 + : 0; + return ( +
+ {setGroupTitle!(group)} +
+ ); + } + + render() { + const { graph, setRenderOrder } = this.props; + if (!graph) { + return null; + } + const { showNodes, showGroups, matrix } = this.state; + const container = graph.get("container"); + let renderItems: (Node | Group)[] = [...showGroups, ...showNodes]; + if (setRenderOrder) { + renderItems = setRenderOrder(renderItems); + } + const items = renderItems.map((item) => + item.type === "node" + ? this.renderNode(item as Node) + : this.renderGroup(item as Group) + ); const styles = { width: this.state.width, height: this.state.height, @@ -677,8 +691,7 @@ export class Viewer extends React.Component { className="vgraph-viewer-viewport" style={{ transform: `matrix(${matrix.join(",")})`, ...styles }} > - {groups} - {nodes} + {items} , container diff --git a/packages/react-vgraph/test/viewer_render_order.spec.tsx b/packages/react-vgraph/test/viewer_render_order.spec.tsx new file mode 100644 index 000000000..35c0acf17 --- /dev/null +++ b/packages/react-vgraph/test/viewer_render_order.spec.tsx @@ -0,0 +1,164 @@ +import React from "react"; +import { cleanup, fireEvent, render } from "@testing-library/react"; +import { Graph, Group, Node } from "@visactor/vgraph"; +import { Viewer, ViewerProps } from "../src/viewer"; + +describe(" render order", () => { + let graph: Graph; + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + graph = new Graph({ + container, + width: 600, + height: 400, + renderMode: "dom", + setDefaultNode: () => ({ + width: 120, + height: 40, + anchors: [{ position: [1, 0.5], size: 8 }], + }), + setDefaultGroup: () => ({ + titleSize: 40, + padding: [12, 12, 12, 12], + strokeStyle: "#ccc", + lineWidth: 2, + }), + }); + graph.data({ + nodes: [ + { id: "node-1", x: 200, y: 200 }, + { id: "node-2", x: 200, y: 200 }, + ], + edges: [], + groups: [{ id: "group-1", children: ["node-1"] }], + }); + graph.refresh(); + }); + + afterEach(() => { + cleanup(); + graph.destroy(); + container.remove(); + }); + + function props(): ViewerProps { + return { + graph, + localRendering: false, + setNode: (node) => {node.get("id")}, + setGroupTitle: (group) => {group.get("id")}, + }; + } + + function order() { + const viewport = container.querySelector(".vgraph-viewer-viewport")!; + return Array.from(viewport.children).map((child) => child.textContent); + } + + it("renders group titles before nodes by default", () => { + render(); + expect(order()).toEqual(["group-1", "node-1", "node-2"]); + }); + + it("calls the reverse callback with a fresh mixed array and renders its result", () => { + const viewer = React.createRef(); + const inputs: string[][] = []; + const reverse = jest.fn((items: (Node | Group)[]) => { + inputs.push(items.map((item) => item.get("id"))); + return items.reverse(); + }); + render(); + + expect(reverse).toHaveBeenCalled(); + expect(inputs[inputs.length - 1]).toEqual(["group-1", "node-1", "node-2"]); + expect(order()).toEqual(["node-2", "node-1", "group-1"]); + const mixed = reverse.mock.calls[reverse.mock.calls.length - 1][0]; + expect(mixed).not.toBe(viewer.current!.state.showNodes); + expect(mixed).not.toBe(viewer.current!.state.showGroups); + expect(mixed).toEqual([ + graph.getNodeById("node-2"), + graph.getNodeById("node-1"), + graph.getGroupById("group-1"), + ]); + expect( + viewer.current!.state.showNodes.map((node) => node.get("id")) + ).toEqual(["node-1", "node-2"]); + expect( + viewer.current!.state.showGroups.map((group) => group.get("id")) + ).toEqual(["group-1"]); + }); + + it("uses the current callback on prop updates and restores defaults when removed", () => { + const reverse = jest.fn((items: (Node | Group)[]) => items.reverse()); + const view = render(); + reverse.mockClear(); + const interleave = jest.fn((items: (Node | Group)[]) => { + const byId = new Map(items.map((item) => [item.get("id"), item])); + return ["node-1", "group-1", "node-2"].map((id) => byId.get(id)!); + }); + + view.rerender(); + expect(interleave).toHaveBeenCalledTimes(1); + expect(reverse).not.toHaveBeenCalled(); + expect(order()).toEqual(["node-1", "group-1", "node-2"]); + + interleave.mockClear(); + view.rerender(); + expect(interleave).not.toHaveBeenCalled(); + expect(order()).toEqual(["group-1", "node-1", "node-2"]); + }); + + it("passes only rendered nodes when no group title renderer is provided", () => { + const reverse = jest.fn((items: (Node | Group)[]) => items.reverse()); + render( + + ); + expect(reverse).toHaveBeenLastCalledWith([ + graph.getNodeById("node-2"), + graph.getNodeById("node-1"), + ]); + expect(order()).toEqual(["node-2", "node-1"]); + }); + + it("calls the callback with an empty array when the graph is empty", () => { + graph.data({ nodes: [], edges: [] }); + const reverse = jest.fn((items: (Node | Group)[]) => items.reverse()); + render(); + expect(reverse).toHaveBeenCalledWith([]); + expect(order()).toEqual([]); + }); + + it("preserves node anchors, styles and event targets after reordering", () => { + const nodeClick = jest.fn(); + const groupClick = jest.fn(); + graph.on("node:click", nodeClick); + graph.on("group.title:click", groupClick); + const view = render( + items.reverse()} + setNodeClassName={() => "custom-node"} + setAnchor={() => anchor} + /> + ); + const nodeElement = container.querySelector(".custom-node")!; + expect(nodeElement.classList.contains("vgraph-viewer-node")).toBe(true); + expect(nodeElement.querySelector(".vgraph-viewer-anchor")).not.toBeNull(); + expect((nodeElement as HTMLElement).style.pointerEvents).toBe("all"); + const title = container.querySelector(".vgraph-viewer-group-title")!; + const bbox = graph.getGroupById("group-1")!.get("__titleBBox"); + expect((title as HTMLElement).style.width).toBe(`${bbox.width + 1}px`); + + fireEvent.click(view.getByText("node-2")); + expect(nodeClick).toHaveBeenCalledTimes(1); + expect(nodeClick.mock.calls[0][0].target).toBe(graph.getNodeById("node-2")); + fireEvent.click(view.getByText("group-1")); + expect(groupClick).toHaveBeenCalledTimes(1); + expect(groupClick.mock.calls[0][0].target).toBe( + graph.getGroupById("group-1") + ); + }); +}); diff --git a/packages/react-vgraph/tsconfig.json b/packages/react-vgraph/tsconfig.json index 682ead64c..f07a72a7f 100644 --- a/packages/react-vgraph/tsconfig.json +++ b/packages/react-vgraph/tsconfig.json @@ -101,5 +101,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ // "suppressImplicitAnyIndexErrors": true }, - "include": ["./src", "./typings", "./examples", "test/table_lineage.spec.tsx"] + "include": ["./src", "./typings", "./examples", "test/**/*.spec.tsx"] } diff --git a/packages/react-vgraph/tsconfig.test.json b/packages/react-vgraph/tsconfig.test.json index 811bfc1a2..3236cdb64 100644 --- a/packages/react-vgraph/tsconfig.test.json +++ b/packages/react-vgraph/tsconfig.test.json @@ -1,7 +1,11 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": ["jest", "@testing-library/jest-dom"] + "types": ["jest"], + "paths": { + "@visactor/vgraph": ["../vgraph/cjs/index.d.ts"], + "@visactor/vgraph/*": ["../vgraph/cjs/*"] + } }, "include": [ "./src", From 39ade6edfbd61967b78d9c47267b9f4b27e8010a Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 11 Nov 2026 17:01:46 +0800 Subject: [PATCH 2/5] ci: pin npm to a Node 20 compatible release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53e32b5f4..7d95af175 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: cache-dependency-path: './common/config/rush/pnpm-lock.yaml' - name: Update npm - run: npm install -g npm@latest + run: npm install -g npm@11.6.2 - name: Install pnpm run: npm install -g pnpm@10.7.0 From c155a25483b6d1bca6fbdfb731a20d1f7a56194f Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 11 Nov 2026 17:07:29 +0800 Subject: [PATCH 3/5] ci: verify npm publishing access before release builds --- .github/workflows/release.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d95af175..bf00618d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,27 @@ jobs: - name: Update npm run: npm install -g npm@11.6.2 + - name: Verify npm publishing access + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + node <<'NODE' + const { execFileSync } = require('child_process'); + const username = execFileSync('npm', ['whoami', '--registry=https://registry.npmjs.org/'], { + encoding: 'utf8' + }).trim(); + console.log(`npm publishing account: ${username}`); + const permissions = JSON.parse(execFileSync('npm', [ + 'access', 'list', 'packages', username, '--json', '--registry=https://registry.npmjs.org/' + ], { encoding: 'utf8' })); + for (const name of ['@visactor/vgraph', '@visactor/react-vgraph', '@visactor/react-vgraph-ui']) { + console.log(`${name}: ${permissions[name] || 'no access'}`); + if (permissions[name] !== 'read-write') { + throw new Error(`The npm publishing account needs read-write access to ${name}.`); + } + } + NODE + - name: Install pnpm run: npm install -g pnpm@10.7.0 From 3c1ab58bbfad036face965ad92c528752d0fd198 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 11 Nov 2026 17:26:10 +0800 Subject: [PATCH 4/5] ci: support package-scoped npm publishing credentials Verify authentication without requiring organization management access. --- .github/workflows/release.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf00618d2..0292ab203 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: - name: Update npm run: npm install -g npm@11.6.2 - - name: Verify npm publishing access + - name: Verify npm authentication env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | @@ -58,15 +58,6 @@ jobs: encoding: 'utf8' }).trim(); console.log(`npm publishing account: ${username}`); - const permissions = JSON.parse(execFileSync('npm', [ - 'access', 'list', 'packages', username, '--json', '--registry=https://registry.npmjs.org/' - ], { encoding: 'utf8' })); - for (const name of ['@visactor/vgraph', '@visactor/react-vgraph', '@visactor/react-vgraph-ui']) { - console.log(`${name}: ${permissions[name] || 'no access'}`); - if (permissions[name] !== 'read-write') { - throw new Error(`The npm publishing account needs read-write access to ${name}.`); - } - } NODE - name: Install pnpm From 2f8b9ee579f3677afc1e3729c3f6f09e08649138 Mon Sep 17 00:00:00 2001 From: fangsmile Date: Wed, 11 Nov 2026 09:34:58 +0000 Subject: [PATCH 5/5] build: release version 0.1.1 [skip ci] --- .../vgraph/2026-06-26-standard-benchmark-tests.json | 10 ---------- common/config/rush/pnpm-lock.yaml | 11 ++++++----- common/config/rush/version-policies.json | 2 +- docs/assets/changelog/en/release.md | 7 +++++++ docs/assets/changelog/zh/release.md | 7 +++++++ docs/package.json | 6 +++--- packages/react-vgraph-ui/package.json | 6 +++--- packages/react-vgraph/package.json | 6 +++--- packages/vgraph/CHANGELOG.json | 12 ++++++++++++ packages/vgraph/CHANGELOG.md | 9 ++++++++- packages/vgraph/package.json | 2 +- 11 files changed, 51 insertions(+), 27 deletions(-) delete mode 100644 common/changes/@visactor/vgraph/2026-06-26-standard-benchmark-tests.json diff --git a/common/changes/@visactor/vgraph/2026-06-26-standard-benchmark-tests.json b/common/changes/@visactor/vgraph/2026-06-26-standard-benchmark-tests.json deleted file mode 100644 index d57c0744c..000000000 --- a/common/changes/@visactor/vgraph/2026-06-26-standard-benchmark-tests.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vgraph", - "comment": "Add edge-case tests for graph algorithms and graph data utilities.", - "type": "patch" - } - ], - "packageName": "@visactor/vgraph" -} diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index f16c20eeb..5d9af1118 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -16,13 +16,13 @@ importers: specifier: 2.39.3 version: 2.39.3(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@visactor/react-vgraph': - specifier: workspace:0.1.0 + specifier: workspace:0.1.1 version: link:../packages/react-vgraph '@visactor/react-vgraph-ui': - specifier: workspace:0.1.0 + specifier: workspace:0.1.1 version: link:../packages/react-vgraph-ui '@visactor/vgraph': - specifier: workspace:0.1.0 + specifier: workspace:0.1.1 version: link:../packages/vgraph buble: specifier: ^0.20.0 @@ -119,7 +119,7 @@ importers: specifier: ^3.0.5 version: 3.5.43(ts-node@10.9.2(@types/node@25.9.3)(typescript@4.9.5)) '@visactor/vgraph': - specifier: workspace:0.1.0 + specifier: workspace:0.1.1 version: link:../vgraph babel-jest: specifier: ^27.3.1 @@ -248,7 +248,7 @@ importers: specifier: ^3.0.5 version: 3.5.43(ts-node@10.9.2(@types/node@25.9.3)(typescript@4.9.5)) '@visactor/vgraph': - specifier: workspace:0.1.0 + specifier: workspace:0.1.1 version: link:../vgraph babel-jest: specifier: ^27.3.1 @@ -4695,6 +4695,7 @@ packages: image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} + deprecated: Please migrate to image-size@2.x hasBin: true import-fresh@3.3.1: diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 598050376..12d783200 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vgraphMain","version":"0.1.0","mainProject":"@visactor/vgraph","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vgraphMain","version":"0.1.1","mainProject":"@visactor/vgraph","nextBump":"patch"}] diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md index 3ce9a363c..9886d8f56 100644 --- a/docs/assets/changelog/en/release.md +++ b/docs/assets/changelog/en/release.md @@ -1,3 +1,10 @@ +# v0.1.1 + +## Updates + +- Restore the optional Viewer setRenderOrder callback for mixed node and group title rendering, with regression tests and a comparison example. +- Add edge-case tests for graph algorithms and graph data utilities. + # v0.1.0 ## Updates diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 6cce39f2d..fd82cd925 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -1,3 +1,10 @@ +# v0.1.1 + +## 更新内容 + +- Restore the optional Viewer setRenderOrder callback for mixed node and group title rendering, with regression tests and a comparison example. +- Add edge-case tests for graph algorithms and graph data utilities. + # v0.1.0 ## 更新内容 diff --git a/docs/package.json b/docs/package.json index f996507d4..f9123bd9f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@arco-design/web-react": "2.39.3", - "@visactor/vgraph": "workspace:0.1.0", - "@visactor/react-vgraph": "workspace:0.1.0", - "@visactor/react-vgraph-ui": "workspace:0.1.0", + "@visactor/vgraph": "workspace:0.1.1", + "@visactor/react-vgraph": "workspace:0.1.1", + "@visactor/react-vgraph-ui": "workspace:0.1.1", "buble": "^0.20.0", "highlight.js": "^11.8.0", "markdown-it": "^13.0.0", diff --git a/packages/react-vgraph-ui/package.json b/packages/react-vgraph-ui/package.json index cf7b21b26..3158e5a17 100644 --- a/packages/react-vgraph-ui/package.json +++ b/packages/react-vgraph-ui/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vgraph-ui", - "version": "0.1.0", + "version": "0.1.1", "description": "React UI components for VGraph", "keywords": [ "react", @@ -58,7 +58,7 @@ "@babel/preset-typescript": "^7.26.0", "@internal/eslint-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vgraph": "workspace:0.1.0", + "@visactor/vgraph": "workspace:0.1.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "12", "@types/jest": "^27.0.2", @@ -93,7 +93,7 @@ "worker-loader": "^3.0.8" }, "peerDependencies": { - "@visactor/vgraph": "^0.1.0", + "@visactor/vgraph": "^0.1.1", "@arco-design/web-react": "^2.39.3", "react": ">=16", "react-dom": ">=16" diff --git a/packages/react-vgraph/package.json b/packages/react-vgraph/package.json index 9270b8f6e..f48c539af 100644 --- a/packages/react-vgraph/package.json +++ b/packages/react-vgraph/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vgraph", - "version": "0.1.0", + "version": "0.1.1", "description": "The react version of VGraph", "keywords": [ "react", @@ -56,7 +56,7 @@ "@babel/preset-typescript": "^7.26.0", "@internal/eslint-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vgraph": "workspace:0.1.0", + "@visactor/vgraph": "workspace:0.1.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "12", "@types/dom-to-image": "^2.6.7", @@ -94,7 +94,7 @@ "worker-loader": "^3.0.8" }, "peerDependencies": { - "@visactor/vgraph": "^0.1.0", + "@visactor/vgraph": "^0.1.1", "react": ">=16", "react-dom": ">=16" }, diff --git a/packages/vgraph/CHANGELOG.json b/packages/vgraph/CHANGELOG.json index 32ed90957..b0a541131 100644 --- a/packages/vgraph/CHANGELOG.json +++ b/packages/vgraph/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vgraph", "entries": [ + { + "version": "0.1.1", + "tag": "@visactor/vgraph_v0.1.1", + "date": "Wed, 11 Nov 2026 09:30:51 GMT", + "comments": { + "patch": [ + { + "comment": "Add edge-case tests for graph algorithms and graph data utilities." + } + ] + } + }, { "version": "0.2.0", "tag": "@visactor/vgraph_v0.2.0", diff --git a/packages/vgraph/CHANGELOG.md b/packages/vgraph/CHANGELOG.md index 9a55210cf..e590f203d 100644 --- a/packages/vgraph/CHANGELOG.md +++ b/packages/vgraph/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vgraph -This log was last generated on Fri, 26 Jun 2026 03:01:27 GMT and should not be manually modified. +This log was last generated on Wed, 11 Nov 2026 09:30:51 GMT and should not be manually modified. + +## 0.1.1 +Wed, 11 Nov 2026 09:30:51 GMT + +### Patches + +- Add edge-case tests for graph algorithms and graph data utilities. ## 0.2.0 Fri, 26 Jun 2026 03:01:27 GMT diff --git a/packages/vgraph/package.json b/packages/vgraph/package.json index e0c563ba6..2c950375a 100644 --- a/packages/vgraph/package.json +++ b/packages/vgraph/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vgraph", - "version": "0.1.0", + "version": "0.1.1", "description": "graph visualization engine", "main": "cjs/index.js", "module": "es/index.js",