doc: complete node form basic example

This commit is contained in:
YuanHeDx 2025-03-17 15:57:58 +08:00 committed by YuanHeDx
parent 493d3084f5
commit 8027436e06
14 changed files with 248 additions and 53 deletions

View File

@ -49,7 +49,7 @@
"@types/node": "^18", "@types/node": "^18",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/styled-components": "^5", "styled-components": "^5",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.54.0", "eslint": "^8.54.0",
"less": "^4.1.2", "less": "^4.1.2",

View File

@ -34,7 +34,8 @@
"@flowgram.ai/free-snap-plugin": "workspace:*", "@flowgram.ai/free-snap-plugin": "workspace:*",
"@flowgram.ai/minimap-plugin": "workspace:*", "@flowgram.ai/minimap-plugin": "workspace:*",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18",
"styled-components": "^5"
}, },
"devDependencies": { "devDependencies": {
"@flowgram.ai/ts-config": "workspace:*", "@flowgram.ai/ts-config": "workspace:*",

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
export const FieldTitle = styled.div`
padding-bottom: 4px;
`;

View File

@ -0,0 +1,18 @@
.error-message {
color: #f5222d !important;
}
.required {
color: #f5222d !important;
padding-left: 4px
}
.field-wrapper {
width: 100%;
margin-bottom: 12px;
}
.field-title {
margin-bottom: 6px;
}

View File

@ -0,0 +1,21 @@
import React from 'react';
import './field-wrapper.css';
interface FieldWrapperProps {
required?: boolean;
title: string;
children?: React.ReactNode;
error?: string;
}
export const FieldWrapper = ({ required, title, children, error }: FieldWrapperProps) => (
<div className="field-wrapper">
<div className="field-title">
{title}
{required ? <span className="required">*</span> : null}
</div>
{children}
<p className="error-message">{error}</p>
</div>
);

View File

@ -0,0 +1,2 @@
export { FieldTitle } from './field-title';
export { FieldWrapper } from './field-wrapper';

View File

@ -0,0 +1,41 @@
export const fieldWrapperTs = `import React from 'react';
import './index.css';
interface FieldWrapperProps {
required?: boolean;
title: string;
children?: React.ReactNode;
error?: string;
}
export const FieldWrapper = ({ required, title, children, error }: FieldWrapperProps) => (
<div className="field-wrapper">
<div className="field-title">
{title}
{required ? <span className="required">*</span> : null}
</div>
{children}
<p className="error-message">{error}</p>
</div>
);
`;
export const fieldWrapperCss = `.error-message {
color: #f5222d !important;
}
.required {
color: #f5222d !important;
padding-left: 4px
}
.field-wrapper {
width: 100%;
margin-bottom: 12px;
}
.field-title {
margin-bottom: 6px;
}
`;

View File

@ -0,0 +1,51 @@
import {
Field,
FieldRenderProps,
FormMeta,
ValidateTrigger,
} from '@flowgram.ai/free-layout-editor';
import { Input } from '@douyinfe/semi-ui';
// FieldWrapper is not provided by sdk, and can be customized
import { FieldWrapper } from './components';
const render = () => (
<div className="demo-node-content">
<div className="demo-node-title">Basic Node</div>
<Field name="name">
{({ field, fieldState }: FieldRenderProps<string>) => (
<FieldWrapper required title="Name" error={fieldState.errors?.[0]?.message}>
<Input size={'small'} {...field} />
</FieldWrapper>
)}
</Field>
<Field name="city">
{({ field, fieldState }: FieldRenderProps<string>) => (
<FieldWrapper required title="City" error={fieldState.errors?.[0]?.message}>
<Input size={'small'} {...field} />
</FieldWrapper>
)}
</Field>
</div>
);
const formMeta: FormMeta = {
render,
defaultValues: { name: 'Tina', city: 'Hangzhou' },
validateTrigger: ValidateTrigger.onChange,
validate: {
name: ({ value }) => {
if (!value) {
return 'Name is required';
}
},
city: ({ value }) => {
if (!value) {
return 'City is required';
}
},
},
};
export const DEFAULT_FORM_META = formMeta;

View File

@ -11,11 +11,17 @@
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1); box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
} }
.demo-free-node-title { .demo-node-content {
background-color: #93bfe2; padding: 8px 12px;
flex-grow: 1;
width: 100%; width: 100%;
border-radius: 8px 8px 0 0; }
padding: 4px 12px;
.demo-node-title {
font-weight: 500;
font-size: 14px;
width: 100%;
margin: 4px 0px 12px 0px;
} }
.demo-free-node-content { .demo-free-node-content {
padding: 4px 12px; padding: 4px 12px;

View File

@ -1 +1,6 @@
export { Editor } from './editor'; export { Editor } from './editor';
export { FieldTitle, FieldWrapper } from './components';
export { DEFAULT_FORM_META } from './form-meta';
export { DEFAULT_DEMO_REGISTRY } from './node-registries';
export { DEFAULT_INITIAL_DATA } from './initial-data';
export { fieldWrapperTs, fieldWrapperCss } from './constant';

View File

@ -8,10 +8,6 @@ export const DEFAULT_INITIAL_DATA: WorkflowJSON = {
meta: { meta: {
position: { x: 400, y: 0 }, position: { x: 400, y: 0 },
}, },
data: {
title: 'Custom',
content: 'Custom node content',
},
}, },
], ],
edges: [], edges: [],

View File

@ -1,23 +1,10 @@
import { WorkflowNodeRegistry, Field } from '@flowgram.ai/free-layout-editor'; import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
import { Input, TextArea } from '@douyinfe/semi-ui';
import { DEFAULT_FORM_META } from './form-meta';
export const DEFAULT_DEMO_REGISTRY: WorkflowNodeRegistry = { export const DEFAULT_DEMO_REGISTRY: WorkflowNodeRegistry = {
type: 'custom', type: 'custom',
meta: {}, meta: {},
defaultPorts: [{ type: 'output' }, { type: 'input' }], defaultPorts: [{ type: 'output' }, { type: 'input' }],
formMeta: { formMeta: DEFAULT_FORM_META,
render: () => (
<div>
<div>Basic Node</div>
<p>name</p>
<Field name="name">
<Input />
</Field>
<p>city</p>
<Field name="city">
<Input />
</Field>
</div>
),
},
}; };

View File

@ -1,42 +1,104 @@
import {
DEFAULT_DEMO_REGISTRY,
DEFAULT_INITIAL_DATA,
fieldWrapperCss,
fieldWrapperTs,
} from '@flowgram.ai/demo-node-form';
import { PreviewEditor } from '../preview-editor'; import { PreviewEditor } from '../preview-editor';
import { Editor } from '.'; import { Editor } from '.';
const indexCode = { const registryCode = {
code: `import { code: `import {
EditorRenderer, Field,
FreeLayoutEditorProvider, FieldRenderProps,
FormMeta,
ValidateTrigger,
} from '@flowgram.ai/free-layout-editor'; } from '@flowgram.ai/free-layout-editor';
import { Input } from '@douyinfe/semi-ui';
import { useEditorProps } from './hooks/use-editor-props' // FieldWrapper is not provided by sdk, it can be customized
import '@flowgram.ai/free-layout-editor/index.css'; import { FieldWrapper } from './components';
import './index.css';
export const App = () => { const render = () => (
const editorProps = useEditorProps() <div className="demo-node-content">
return ( <div className="demo-node-title">Basic Node</div>
<FreeLayoutEditorProvider {...editorProps}> <Field name="name">
<div className="demo-free-container"> {({ field, fieldState }: FieldRenderProps<string>) => (
<div className="demo-free-layout"> <FieldWrapper required title="Name" error={fieldState.errors?.[0]?.message}>
<NodeAddPanel /> <Input size={'small'} {...field} />
<EditorRenderer className="demo-free-editor" /> </FieldWrapper>
)}
</Field>
<Field name="city">
{({ field, fieldState }: FieldRenderProps<string>) => (
<FieldWrapper required title="City" error={fieldState.errors?.[0]?.message}>
<Input size={'small'} {...field} />
</FieldWrapper>
)}
</Field>
</div> </div>
<Tools /> );
<Minimap />
</div> const formMeta: FormMeta = {
</FreeLayoutEditorProvider> render,
) defaultValues: { name: 'Tina', city: 'Hangzhou' },
validateTrigger: ValidateTrigger.onChange,
validate: {
name: ({ value }) => {
if (!value) {
return 'Name is required';
}
},
city: ({ value }) => {
if (!value) {
return 'City is required';
}
}
}
};
export const nodeRegistry: WorkflowNodeRegistry = {
type: 'custom',
meta: {},
defaultPorts: [{ type: 'output' }, { type: 'input' }],
formMeta
}; };
`, `,
active: true, active: true,
}; };
const initialDataCode = {
code: `import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
export const DEFAULT_INITIAL_DATA: WorkflowJSON = {
nodes: [
{
id: 'node_0',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
},
],
edges: [],
};`,
active: true,
};
export const NodeFormBasicPreview = () => { export const NodeFormBasicPreview = () => {
const files = { const files = {
'index.tsx': indexCode, 'node-registry.tsx': registryCode,
'initial-data.ts': initialDataCode,
'field-wrapper.tsx': { code: fieldWrapperTs, active: true },
'field-wrapper.css': { code: fieldWrapperCss, active: true },
}; };
return ( return (
<PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}> <PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>
<Editor /> <Editor registry={DEFAULT_DEMO_REGISTRY} initialData={DEFAULT_INITIAL_DATA} />
</PreviewEditor> </PreviewEditor>
); );
}; };

View File

@ -245,9 +245,6 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^18 specifier: ^18
version: 18.3.5(@types/react@18.3.16) version: 18.3.5(@types/react@18.3.16)
'@types/styled-components':
specifier: ^5
version: 5.1.34
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^6.10.0 specifier: ^6.10.0
version: 6.21.0(eslint@8.57.1)(typescript@5.0.4) version: 6.21.0(eslint@8.57.1)(typescript@5.0.4)
@ -333,6 +330,9 @@ importers:
react-dom: react-dom:
specifier: ^18 specifier: ^18
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
styled-components:
specifier: ^5
version: 5.3.11(@babel/core@7.26.0)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)
devDependencies: devDependencies:
'@flowgram.ai/eslint-config': '@flowgram.ai/eslint-config':
specifier: workspace:* specifier: workspace:*