diff --git a/apps/demo-fixed-layout/src/components/base-node/index.tsx b/apps/demo-fixed-layout/src/components/base-node/index.tsx index ee518d8b..a604512e 100644 --- a/apps/demo-fixed-layout/src/components/base-node/index.tsx +++ b/apps/demo-fixed-layout/src/components/base-node/index.tsx @@ -1,9 +1,9 @@ -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor'; import { ConfigProvider } from '@douyinfe/semi-ui'; -import { NodeRenderContext } from '../../context'; +import { NodeRenderContext, SidebarContext } from '../../context'; import { BaseNodeStyle, ErrorIcon } from './styles'; export const BaseNode = ({ node }: { node: FlowNodeEntity }) => { @@ -24,6 +24,11 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => { */ const getPopupContainer = useCallback(() => node.renderData.node || document.body, []); + /** + * Sidebar control + */ + const sidebar = useContext(SidebarContext); + return ( {form?.state.invalid && } @@ -34,6 +39,13 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => { **/ onMouseEnter={nodeRender.onMouseEnter} onMouseLeave={nodeRender.onMouseLeave} + className={nodeRender.activated ? 'activated' : ''} + onClick={() => { + if (nodeRender.dragging) { + return; + } + sidebar.setNodeRender(nodeRender); + }} style={{ /** * Lets you precisely control the style of branch nodes diff --git a/apps/demo-fixed-layout/src/components/base-node/styles.tsx b/apps/demo-fixed-layout/src/components/base-node/styles.tsx index 6717c77b..22d899b3 100644 --- a/apps/demo-fixed-layout/src/components/base-node/styles.tsx +++ b/apps/demo-fixed-layout/src/components/base-node/styles.tsx @@ -12,8 +12,9 @@ export const BaseNodeStyle = styled.div` justify-content: center; position: relative; width: 360px; - - transition: all 0.3s ease; + &.activated { + border: 1px solid #4e40e5; + } `; export const ErrorIcon = () => ( diff --git a/apps/demo-fixed-layout/src/components/sidebar/index.tsx b/apps/demo-fixed-layout/src/components/sidebar/index.tsx new file mode 100644 index 00000000..2afd32dc --- /dev/null +++ b/apps/demo-fixed-layout/src/components/sidebar/index.tsx @@ -0,0 +1,2 @@ +export { SidebarProvider } from './sidebar-provider'; +export { SidebarRenderer } from './sidebar-renderer'; diff --git a/apps/demo-fixed-layout/src/components/sidebar/sidebar-provider.tsx b/apps/demo-fixed-layout/src/components/sidebar/sidebar-provider.tsx new file mode 100644 index 00000000..9996f008 --- /dev/null +++ b/apps/demo-fixed-layout/src/components/sidebar/sidebar-provider.tsx @@ -0,0 +1,14 @@ +import { useState } from 'react'; + +import { NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor'; + +import { SidebarContext } from '../../context'; + +export function SidebarProvider({ children }: { children: React.ReactNode }) { + const [nodeRender, setNodeRender] = useState(); + return ( + + {children} + + ); +} diff --git a/apps/demo-fixed-layout/src/components/sidebar/sidebar-renderer.tsx b/apps/demo-fixed-layout/src/components/sidebar/sidebar-renderer.tsx new file mode 100644 index 00000000..ffc7ea10 --- /dev/null +++ b/apps/demo-fixed-layout/src/components/sidebar/sidebar-renderer.tsx @@ -0,0 +1,75 @@ +import { useCallback, useContext, useEffect } from 'react'; + +import { + PlaygroundEntityContext, + useRefresh, + useClientContext, +} from '@flowgram.ai/fixed-layout-editor'; +import { SideSheet } from '@douyinfe/semi-ui'; + +import { SidebarContext, IsSidebarContext, NodeRenderContext } from '../../context'; + +export const SidebarRenderer = () => { + const { nodeRender, setNodeRender } = useContext(SidebarContext); + const { selection, playground } = useClientContext(); + const refresh = useRefresh(); + const handleClose = useCallback(() => { + setNodeRender(undefined); + }, []); + /** + * Listen readonly + */ + useEffect(() => { + const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh()); + return () => disposable.dispose(); + }, [playground]); + /** + * Listen selection + */ + useEffect(() => { + const toDispose = selection.onSelectionChanged(() => { + /** + * 如果没有选中任何节点,则自动关闭侧边栏 + * If no node is selected, the sidebar is automatically closed + */ + if (selection.selection.length === 0) { + handleClose(); + } else if (selection.selection.length === 1 && selection.selection[0] !== nodeRender?.node) { + handleClose(); + } + }); + return () => toDispose.dispose(); + }, [selection, handleClose]); + /** + * Close when node disposed + */ + useEffect(() => { + if (nodeRender) { + const toDispose = nodeRender.node.onDispose(() => { + setNodeRender(undefined); + }); + return () => toDispose.dispose(); + } + return () => {}; + }, [nodeRender]); + + if (playground.config.readonly) { + return null; + } + /** + * Add key to rerender the sidebar when the node changes + */ + const content = nodeRender ? ( + + + {nodeRender.form?.render()} + + + ) : null; + + return ( + + {content} + + ); +}; diff --git a/apps/demo-fixed-layout/src/components/tools/index.tsx b/apps/demo-fixed-layout/src/components/tools/index.tsx index 7d387055..2f3f324d 100644 --- a/apps/demo-fixed-layout/src/components/tools/index.tsx +++ b/apps/demo-fixed-layout/src/components/tools/index.tsx @@ -37,7 +37,7 @@ export const DemoTools = () => { } - disabled={!tools.canUndo} + disabled={!tools.canUndo || playground.config.readonly} onClick={() => tools.undo()} /> @@ -45,7 +45,7 @@ export const DemoTools = () => { } - disabled={!tools.canRedo} + disabled={!tools.canRedo || playground.config.readonly} onClick={() => tools.redo()} /> diff --git a/apps/demo-fixed-layout/src/context/index.ts b/apps/demo-fixed-layout/src/context/index.ts index ba7ab355..ca6c0067 100644 --- a/apps/demo-fixed-layout/src/context/index.ts +++ b/apps/demo-fixed-layout/src/context/index.ts @@ -1 +1,2 @@ export { NodeRenderContext } from './node-render-context'; +export { SidebarContext, IsSidebarContext } from './sidebar-context'; diff --git a/apps/demo-fixed-layout/src/context/sidebar-context.ts b/apps/demo-fixed-layout/src/context/sidebar-context.ts new file mode 100644 index 00000000..2cb7b599 --- /dev/null +++ b/apps/demo-fixed-layout/src/context/sidebar-context.ts @@ -0,0 +1,11 @@ +import React from 'react'; + +import { NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor'; + +export const SidebarContext = React.createContext<{ + visible: boolean; + nodeRender?: NodeRenderReturnType; + setNodeRender: (node: NodeRenderReturnType | undefined) => void; +}>({ visible: false, setNodeRender: () => {} }); + +export const IsSidebarContext = React.createContext(false); diff --git a/apps/demo-fixed-layout/src/editor.tsx b/apps/demo-fixed-layout/src/editor.tsx index 5653596a..a314aa87 100644 --- a/apps/demo-fixed-layout/src/editor.tsx +++ b/apps/demo-fixed-layout/src/editor.tsx @@ -3,6 +3,7 @@ import { EditorRenderer, FixedLayoutEditorProvider } from '@flowgram.ai/fixed-la import { FlowNodeRegistries } from './nodes'; import { initialData } from './initial-data'; import { useEditorProps } from './hooks/use-editor-props'; +import { SidebarProvider, SidebarRenderer } from './components/sidebar'; import { DemoTools } from './components'; import '@flowgram.ai/fixed-layout-editor/index.css'; @@ -16,8 +17,11 @@ export const Editor = () => { return (
- - + + + + +
); diff --git a/apps/demo-fixed-layout/src/form-components/form-content/index.tsx b/apps/demo-fixed-layout/src/form-components/form-content/index.tsx index 77c17873..47fe07de 100644 --- a/apps/demo-fixed-layout/src/form-components/form-content/index.tsx +++ b/apps/demo-fixed-layout/src/form-components/form-content/index.tsx @@ -1,8 +1,8 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor'; -import { NodeRenderContext } from '../../context'; +import { useIsSidebar, useNodeRenderContext } from '../../hooks'; import { FormTitleDescription, FormWrapper } from './styles'; /** @@ -10,13 +10,14 @@ import { FormTitleDescription, FormWrapper } from './styles'; * @constructor */ export function FormContent(props: { children?: React.ReactNode }) { - const { expanded, node } = useContext(NodeRenderContext); + const { node, expanded } = useNodeRenderContext(); + const isSidebar = useIsSidebar(); const registry = node.getNodeRegistry(); return ( {expanded ? ( <> - {registry.info?.description} + {isSidebar && {registry.info?.description}} {props.children} ) : undefined} diff --git a/apps/demo-fixed-layout/src/form-components/form-header/index.tsx b/apps/demo-fixed-layout/src/form-components/form-header/index.tsx index 8787f2d7..35d50038 100644 --- a/apps/demo-fixed-layout/src/form-components/form-header/index.tsx +++ b/apps/demo-fixed-layout/src/form-components/form-header/index.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useContext, useCallback } from 'react'; import { Field, FieldRenderProps, useClientContext } from '@flowgram.ai/fixed-layout-editor'; import { IconButton, Dropdown, Typography, Button } from '@douyinfe/semi-ui'; @@ -8,6 +8,7 @@ import { IconMore } from '@douyinfe/semi-icons'; import { Feedback } from '../feedback'; import { FlowNodeRegistry } from '../../typings'; import { FlowCommandId } from '../../shortcuts/constants'; +import { useIsSidebar } from '../../hooks'; import { NodeRenderContext } from '../../context'; import { getIcon } from './utils'; import { Header, Operators, Title } from './styles'; @@ -18,16 +19,29 @@ function DropdownContent() { const { node, deleteNode } = useContext(NodeRenderContext); const clientContext = useClientContext(); const registry = node.getNodeRegistry(); - const handleCopy = () => { - clientContext.playground.commandService.executeCommand(FlowCommandId.COPY, node); - }; + const handleCopy = useCallback( + (e: React.MouseEvent) => { + clientContext.playground.commandService.executeCommand(FlowCommandId.COPY, node); + e.stopPropagation(); // Disable clicking prevents the sidebar from opening + }, + [clientContext, node] + ); + + const handleDelete = useCallback( + (e: React.MouseEvent) => { + deleteNode(); + e.stopPropagation(); // Disable clicking prevents the sidebar from opening + }, + [clientContext, node] + ); + return ( Copy Delete @@ -39,12 +53,18 @@ function DropdownContent() { export function FormHeader() { const { node, expanded, startDrag, toggleExpand, readonly } = useContext(NodeRenderContext); + const isSidebar = useIsSidebar(); + const handleExpand = (e: React.MouseEvent) => { + toggleExpand(); + e.stopPropagation(); // Disable clicking prevents the sidebar from opening + }; + return (
{ // trigger drag node startDrag(e); - e.stopPropagation(); + // e.stopPropagation(); }} > {getIcon(node)} @@ -58,13 +78,15 @@ export function FormHeader() { )} -
diff --git a/apps/demo-free-layout/src/form-components/form-inputs/index.tsx b/apps/demo-free-layout/src/form-components/form-inputs/index.tsx index 0b549d32..30475597 100644 --- a/apps/demo-free-layout/src/form-components/form-inputs/index.tsx +++ b/apps/demo-free-layout/src/form-components/form-inputs/index.tsx @@ -4,10 +4,10 @@ import { FxExpression } from '../fx-expression'; import { FormItem } from '../form-item'; import { Feedback } from '../feedback'; import { JsonSchema } from '../../typings'; -import { useNodeRenderContext } from '../../hooks'; +import { useIsSidebar } from '../../hooks'; export function FormInputs() { - const { readonly } = useNodeRenderContext(); + const readonly = !useIsSidebar(); return ( name="inputs"> {({ field: inputsField }) => { @@ -29,7 +29,7 @@ export function FormInputs() { 0} /> diff --git a/apps/demo-free-layout/src/form-components/form-outputs/index.tsx b/apps/demo-free-layout/src/form-components/form-outputs/index.tsx index bd8c7eaa..905a9a39 100644 --- a/apps/demo-free-layout/src/form-components/form-outputs/index.tsx +++ b/apps/demo-free-layout/src/form-components/form-outputs/index.tsx @@ -2,9 +2,14 @@ import { Field } from '@flowgram.ai/free-layout-editor'; import { TypeTag } from '../type-tag'; import { JsonSchema } from '../../typings'; +import { useIsSidebar } from '../../hooks'; import { FormOutputsContainer } from './styles'; export function FormOutputs() { + const isSidebar = useIsSidebar(); + if (isSidebar) { + return null; + } return ( name={'outputs'}> {({ field }) => { diff --git a/apps/demo-free-layout/src/form-components/fx-expression/index.tsx b/apps/demo-free-layout/src/form-components/fx-expression/index.tsx index acabdc71..765a2cf3 100644 --- a/apps/demo-free-layout/src/form-components/fx-expression/index.tsx +++ b/apps/demo-free-layout/src/form-components/fx-expression/index.tsx @@ -2,6 +2,7 @@ import React, { type SVGProps } from 'react'; import { Input, Button } from '@douyinfe/semi-ui'; +import { ValueDisplay } from '../value-display'; import { FlowRefValueSchema, FlowLiteralValueSchema } from '../../typings'; import { VariableSelector } from '../../plugins/sync-variable-plugin/variable-selector'; @@ -18,18 +19,44 @@ export function FxIcon(props: SVGProps) { ); } +function InputWrap({ + value, + onChange, + readonly, + hasError, + style, +}: { + value: string; + onChange: (v: string) => void; + readonly?: boolean; + hasError?: boolean; + style?: React.CSSProperties; +}) { + if (readonly) { + return ; + } + return ( + + ); +} + export interface FxExpressionProps { value?: FlowLiteralValueSchema | FlowRefValueSchema; onChange: (value: FlowLiteralValueSchema | FlowRefValueSchema) => void; literal?: boolean; hasError?: boolean; - disabled?: boolean; + readonly?: boolean; icon?: React.ReactNode; } export function FxExpression(props: FxExpressionProps) { - const { value, onChange, disabled, literal, icon } = props; - if (literal) return ; + const { value, onChange, readonly, literal, icon } = props; + if (literal) return ; const isExpression = typeof value === 'object' && value.type === 'expression'; const toggleExpression = () => { if (isExpression) { @@ -46,18 +73,18 @@ export function FxExpression(props: FxExpressionProps) { hasError={props.hasError} style={{ flexGrow: 1 }} onChange={(v) => onChange({ type: 'expression', content: v })} - disabled={disabled} + readonly={readonly} /> ) : ( - )} - {!disabled && + {!readonly && (icon ||