feat(demo): sidebar support openning by node id

This commit is contained in:
xiamidaxia 2025-06-04 21:31:55 +08:00
parent d96f97dbc5
commit 22915805ad
11 changed files with 103 additions and 59 deletions

View File

@ -44,7 +44,7 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
if (nodeRender.dragging) { if (nodeRender.dragging) {
return; return;
} }
sidebar.setNodeRender(nodeRender); sidebar.setNodeId(nodeRender.node.id);
}} }}
style={{ style={{
/** /**

View File

@ -0,0 +1,14 @@
import { useNodeRender, FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
import { NodeRenderContext } from '../../context';
export function SidebarNodeRenderer(props: { node: FlowNodeEntity }) {
const { node } = props;
const nodeRender = useNodeRender(node);
return (
<NodeRenderContext.Provider value={nodeRender}>
{nodeRender.form?.render()}
</NodeRenderContext.Provider>
);
}

View File

@ -1,13 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor';
import { SidebarContext } from '../../context'; import { SidebarContext } from '../../context';
export function SidebarProvider({ children }: { children: React.ReactNode }) { export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [nodeRender, setNodeRender] = useState<NodeRenderReturnType | undefined>(); const [nodeId, setNodeId] = useState<string | undefined>();
return ( return (
<SidebarContext.Provider value={{ visible: !!nodeRender, nodeRender, setNodeRender }}> <SidebarContext.Provider value={{ visible: !!nodeId, nodeId, setNodeId }}>
{children} {children}
</SidebarContext.Provider> </SidebarContext.Provider>
); );

View File

@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect } from 'react'; import { useCallback, useContext, useEffect, useMemo } from 'react';
import { import {
PlaygroundEntityContext, PlaygroundEntityContext,
@ -7,20 +7,26 @@ import {
} from '@flowgram.ai/fixed-layout-editor'; } from '@flowgram.ai/fixed-layout-editor';
import { SideSheet } from '@douyinfe/semi-ui'; import { SideSheet } from '@douyinfe/semi-ui';
import { SidebarContext, IsSidebarContext, NodeRenderContext } from '../../context'; import { FlowNodeMeta } from '../../typings';
import { SidebarContext, IsSidebarContext } from '../../context';
import { SidebarNodeRenderer } from './sidebar-node-renderer';
export const SidebarRenderer = () => { export const SidebarRenderer = () => {
const { nodeRender, setNodeRender } = useContext(SidebarContext); const { nodeId, setNodeId } = useContext(SidebarContext);
const { selection, playground } = useClientContext(); const { selection, playground, document } = useClientContext();
const refresh = useRefresh(); const refresh = useRefresh();
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setNodeRender(undefined); setNodeId(undefined);
}, []); }, []);
const node = nodeId ? document.getNode(nodeId) : undefined;
/** /**
* Listen readonly * Listen readonly
*/ */
useEffect(() => { useEffect(() => {
const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh()); const disposable = playground.config.onReadonlyOrDisabledChange(() => {
handleClose();
refresh();
});
return () => disposable.dispose(); return () => disposable.dispose();
}, [playground]); }, [playground]);
/** /**
@ -34,41 +40,47 @@ export const SidebarRenderer = () => {
*/ */
if (selection.selection.length === 0) { if (selection.selection.length === 0) {
handleClose(); handleClose();
} else if (selection.selection.length === 1 && selection.selection[0] !== nodeRender?.node) { } else if (selection.selection.length === 1 && selection.selection[0] !== node) {
handleClose(); handleClose();
} }
}); });
return () => toDispose.dispose(); return () => toDispose.dispose();
}, [selection, handleClose]); }, [selection, handleClose, node]);
/** /**
* Close when node disposed * Close when node disposed
*/ */
useEffect(() => { useEffect(() => {
if (nodeRender) { if (node) {
const toDispose = nodeRender.node.onDispose(() => { const toDispose = node.onDispose(() => {
setNodeRender(undefined); setNodeId(undefined);
}); });
return () => toDispose.dispose(); return () => toDispose.dispose();
} }
return () => {}; return () => {};
}, [nodeRender]); }, [node]);
const visible = useMemo(() => {
if (!node) {
return false;
}
const { disableSideBar = false } = node.getNodeMeta<FlowNodeMeta>();
return !disableSideBar;
}, [node]);
if (playground.config.readonly) { if (playground.config.readonly) {
return null; return null;
} }
/** /**
* Add key to rerender the sidebar when the node changes * Add "key" to rerender the sidebar when the node changes
*/ */
const content = nodeRender ? ( const content = node ? (
<PlaygroundEntityContext.Provider key={nodeRender.node.id} value={nodeRender.node}> <PlaygroundEntityContext.Provider key={node.id} value={node}>
<NodeRenderContext.Provider value={nodeRender}> <SidebarNodeRenderer node={node} />
{nodeRender.form?.render()}
</NodeRenderContext.Provider>
</PlaygroundEntityContext.Provider> </PlaygroundEntityContext.Provider>
) : null; ) : null;
return ( return (
<SideSheet mask={false} visible={!!nodeRender} onCancel={handleClose}> <SideSheet mask={false} visible={visible} onCancel={handleClose}>
<IsSidebarContext.Provider value={true}>{content}</IsSidebarContext.Provider> <IsSidebarContext.Provider value={true}>{content}</IsSidebarContext.Provider>
</SideSheet> </SideSheet>
); );

View File

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor';
export const SidebarContext = React.createContext<{ export const SidebarContext = React.createContext<{
visible: boolean; visible: boolean;
nodeRender?: NodeRenderReturnType; nodeId?: string;
setNodeRender: (node: NodeRenderReturnType | undefined) => void; setNodeId: (node: string | undefined) => void;
}>({ visible: false, setNodeRender: () => {} }); }>({ visible: false, setNodeId: () => {} });
export const IsSidebarContext = React.createContext<boolean>(false); export const IsSidebarContext = React.createContext<boolean>(false);

View File

@ -4,6 +4,7 @@ import {
FlowNodeRegistry as FlowNodeRegistryDefault, FlowNodeRegistry as FlowNodeRegistryDefault,
FixedLayoutPluginContext, FixedLayoutPluginContext,
FlowNodeEntity, FlowNodeEntity,
FlowNodeMeta as FlowNodeMetaDefault,
} from '@flowgram.ai/fixed-layout-editor'; } from '@flowgram.ai/fixed-layout-editor';
import { type JsonSchema } from './json-schema'; import { type JsonSchema } from './json-schema';
@ -37,11 +38,19 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
}; };
} }
/**
* You can customize your own node meta
* meta
*/
export interface FlowNodeMeta extends FlowNodeMetaDefault {
disableSideBar?: boolean;
}
/** /**
* You can customize your own node registry * You can customize your own node registry
* *
*/ */
export interface FlowNodeRegistry extends FlowNodeRegistryDefault { export interface FlowNodeRegistry extends FlowNodeRegistryDefault {
meta: FlowNodeMeta;
info: { info: {
icon: string; icon: string;
description: string; description: string;

View File

@ -41,7 +41,7 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
onClick={(e) => { onClick={(e) => {
selectNode(e); selectNode(e);
if (!isDragging) { if (!isDragging) {
sidebar.setNodeRender(nodeRender); sidebar.setNodeId(nodeRender.node.id);
// 可选:将 isScrollToView 设为 true可以让节点选中后滚动到画布中间 // 可选:将 isScrollToView 设为 true可以让节点选中后滚动到画布中间
// Optional: Set isScrollToView to true to scroll the node to the center of the canvas after it is selected. // Optional: Set isScrollToView to true to scroll the node to the center of the canvas after it is selected.
if (isScrollToView) { if (isScrollToView) {

View File

@ -0,0 +1,14 @@
import { useNodeRender, FlowNodeEntity } from '@flowgram.ai/free-layout-editor';
import { NodeRenderContext } from '../../context';
export function SidebarNodeRenderer(props: { node: FlowNodeEntity }) {
const { node } = props;
const nodeRender = useNodeRender(node);
return (
<NodeRenderContext.Provider value={nodeRender}>
{nodeRender.form?.render()}
</NodeRenderContext.Provider>
);
}

View File

@ -1,13 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { NodeRenderReturnType } from '@flowgram.ai/free-layout-editor';
import { SidebarContext } from '../../context'; import { SidebarContext } from '../../context';
export function SidebarProvider({ children }: { children: React.ReactNode }) { export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [nodeRender, setNodeRender] = useState<NodeRenderReturnType | undefined>(); const [nodeId, setNodeId] = useState<string | undefined>();
return ( return (
<SidebarContext.Provider value={{ visible: !!nodeRender, nodeRender, setNodeRender }}> <SidebarContext.Provider value={{ visible: !!nodeId, nodeId, setNodeId }}>
{children} {children}
</SidebarContext.Provider> </SidebarContext.Provider>
); );

View File

@ -8,20 +8,25 @@ import {
import { SideSheet } from '@douyinfe/semi-ui'; import { SideSheet } from '@douyinfe/semi-ui';
import { FlowNodeMeta } from '../../typings'; import { FlowNodeMeta } from '../../typings';
import { SidebarContext, IsSidebarContext, NodeRenderContext } from '../../context'; import { SidebarContext, IsSidebarContext } from '../../context';
import { SidebarNodeRenderer } from './sidebar-node-renderer';
export const SidebarRenderer = () => { export const SidebarRenderer = () => {
const { nodeRender, setNodeRender } = useContext(SidebarContext); const { nodeId, setNodeId } = useContext(SidebarContext);
const { selection, playground } = useClientContext(); const { selection, playground, document } = useClientContext();
const refresh = useRefresh(); const refresh = useRefresh();
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setNodeRender(undefined); setNodeId(undefined);
}, []); }, []);
const node = nodeId ? document.getNode(nodeId) : undefined;
/** /**
* Listen readonly * Listen readonly
*/ */
useEffect(() => { useEffect(() => {
const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh()); const disposable = playground.config.onReadonlyOrDisabledChange(() => {
handleClose();
refresh();
});
return () => disposable.dispose(); return () => disposable.dispose();
}, [playground]); }, [playground]);
/** /**
@ -35,44 +40,42 @@ export const SidebarRenderer = () => {
*/ */
if (selection.selection.length === 0) { if (selection.selection.length === 0) {
handleClose(); handleClose();
} else if (selection.selection.length === 1 && selection.selection[0] !== nodeRender?.node) { } else if (selection.selection.length === 1 && selection.selection[0] !== node) {
handleClose(); handleClose();
} }
}); });
return () => toDispose.dispose(); return () => toDispose.dispose();
}, [selection, handleClose]); }, [selection, handleClose, node]);
/** /**
* Close when node disposed * Close when node disposed
*/ */
useEffect(() => { useEffect(() => {
if (nodeRender) { if (node) {
const toDispose = nodeRender.node.onDispose(() => { const toDispose = node.onDispose(() => {
setNodeRender(undefined); setNodeId(undefined);
}); });
return () => toDispose.dispose(); return () => toDispose.dispose();
} }
return () => {}; return () => {};
}, [nodeRender]); }, [node]);
const visible = useMemo(() => { const visible = useMemo(() => {
if (!nodeRender) { if (!node) {
return false; return false;
} }
const { disableSideBar = false } = nodeRender.node.getNodeMeta<FlowNodeMeta>(); const { disableSideBar = false } = node.getNodeMeta<FlowNodeMeta>();
return !disableSideBar; return !disableSideBar;
}, [nodeRender]); }, [node]);
if (playground.config.readonly) { if (playground.config.readonly) {
return null; return null;
} }
/** /**
* Add key to rerender the sidebar when the node changes * Add "key" to rerender the sidebar when the node changes
*/ */
const content = nodeRender ? ( const content = node ? (
<PlaygroundEntityContext.Provider key={nodeRender.node.id} value={nodeRender.node}> <PlaygroundEntityContext.Provider key={node.id} value={node}>
<NodeRenderContext.Provider value={nodeRender}> <SidebarNodeRenderer node={node} />
{nodeRender.form?.render()}
</NodeRenderContext.Provider>
</PlaygroundEntityContext.Provider> </PlaygroundEntityContext.Provider>
) : null; ) : null;

View File

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { NodeRenderReturnType } from '@flowgram.ai/free-layout-editor';
export const SidebarContext = React.createContext<{ export const SidebarContext = React.createContext<{
visible: boolean; visible: boolean;
nodeRender?: NodeRenderReturnType; nodeId?: string;
setNodeRender: (node: NodeRenderReturnType | undefined) => void; setNodeId: (node: string | undefined) => void;
}>({ visible: false, setNodeRender: () => {} }); }>({ visible: false, setNodeId: () => {} });
export const IsSidebarContext = React.createContext<boolean>(false); export const IsSidebarContext = React.createContext<boolean>(false);