preview.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import { PreviewEditor } from '../preview-editor';
  2. import { FixedLayoutSimple } from './index';
  3. const indexCode = {
  4. code: `import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
  5. import '@flowgram.ai/fixed-layout-editor/index.css';
  6. import './index.css'
  7. import { useEditorProps } from './use-editor-props';
  8. import { initialData } from './initial-data'
  9. import { nodeRegistries } from './node-registries'
  10. import { Tools } from './tools'
  11. import { Minimap } from './minimap'
  12. export const Editor = () => {
  13. const editorProps = useEditorProps(initialData, nodeRegistries);
  14. return (
  15. <FixedLayoutEditorProvider {...editorProps}>
  16. <div className="demo-fixed-container">
  17. <EditorRenderer>{/* add child panel here */}</EditorRenderer>
  18. </div>
  19. <Tools />
  20. <Minimap />
  21. </FixedLayoutEditorProvider>
  22. );
  23. }`,
  24. active: true,
  25. };
  26. const indexCssCode = `.demo-fixed-node {
  27. align-items: flex-start;
  28. background-color: #fff;
  29. border: 1px solid rgba(6, 7, 9, 0.15);
  30. border-radius: 8px;
  31. box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
  32. display: flex;
  33. flex-direction: column;
  34. justify-content: center;
  35. position: relative;
  36. width: 360px;
  37. transition: all 0.3s ease;
  38. }
  39. .demo-fixed-node-title {
  40. background-color: #93bfe2;
  41. width: 100%;
  42. border-radius: 8px 8px 0 0;
  43. padding: 4px 12px;
  44. }
  45. .demo-fixed-node-content {
  46. padding: 16px;
  47. flex-grow: 1;
  48. width: 100%;
  49. }
  50. .demo-fixed-adder {
  51. width: 28px;
  52. height: 18px;
  53. background: rgb(187, 191, 196);
  54. display: flex;
  55. border-radius: 9px;
  56. justify-content: space-evenly;
  57. align-items: center;
  58. color: #fff;
  59. font-size: 10px;
  60. font-weight: bold;
  61. div {
  62. display: flex;
  63. justify-content: center;
  64. align-items: center;
  65. svg {
  66. width: 12px;
  67. height: 12px;
  68. }
  69. }
  70. }
  71. .demo-fixed-adder.activated {
  72. background: #82A7FC
  73. }
  74. .demo-fixed-adder.isHorizontal {
  75. transform: rotate(90deg);
  76. }
  77. .gedit-playground * {
  78. box-sizing: border-box;
  79. }`;
  80. const initialDataCode = `import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
  81. /**
  82. * 配置流程数据,数据为 blocks 嵌套的格式
  83. */
  84. export const initialData: FlowDocumentJSON = {
  85. nodes: [
  86. // 开始节点
  87. {
  88. id: 'start_0',
  89. type: 'start',
  90. data: {
  91. title: 'Start',
  92. content: 'start content'
  93. },
  94. blocks: [],
  95. },
  96. // 分支节点
  97. {
  98. id: 'condition_0',
  99. type: 'condition',
  100. data: {
  101. title: 'Condition'
  102. },
  103. blocks: [
  104. {
  105. id: 'branch_0',
  106. type: 'block',
  107. data: {
  108. title: 'Branch 0',
  109. content: 'branch 1 content'
  110. },
  111. blocks: [
  112. {
  113. id: 'custom_0',
  114. type: 'custom',
  115. data: {
  116. title: 'Custom',
  117. content: 'custrom content'
  118. },
  119. },
  120. ],
  121. },
  122. {
  123. id: 'branch_1',
  124. type: 'block',
  125. data: {
  126. title: 'Branch 1',
  127. content: 'branch 1 content'
  128. },
  129. blocks: [],
  130. },
  131. ],
  132. },
  133. // 结束节点
  134. {
  135. id: 'end_0',
  136. type: 'end',
  137. data: {
  138. title: 'End',
  139. content: 'end content'
  140. },
  141. },
  142. ],
  143. };`;
  144. const nodeRegistriesCode = `import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
  145. import { nanoid } from 'nanoid';
  146. /**
  147. * 自定义节点注册
  148. */
  149. export const nodeRegistries: FlowNodeRegistry[] = [
  150. {
  151. /**
  152. * 自定义节点类型
  153. */
  154. type: 'condition',
  155. /**
  156. * 自定义节点扩展:
  157. * - loop: 扩展为循环节点
  158. * - start: 扩展为开始节点
  159. * - dynamicSplit: 扩展为分支节点
  160. * - end: 扩展为结束节点
  161. * - tryCatch: 扩展为 tryCatch 节点
  162. * - default: 扩展为普通节点 (默认)
  163. */
  164. extend: 'dynamicSplit',
  165. /**
  166. * 节点配置信息
  167. */
  168. meta: {
  169. // isStart: false, // 是否为开始节点
  170. // isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
  171. // draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
  172. // selectable: false, // 触发器等开始节点不能被框选
  173. // deleteDisable: true, // 禁止删除
  174. // copyDisable: true, // 禁止copy
  175. // addDisable: true, // 禁止添加
  176. },
  177. onAdd() {
  178. return {
  179. id: \`condition_\${nanoid(5)}\`,
  180. type: 'condition',
  181. data: {
  182. title: 'Condition',
  183. },
  184. blocks: [
  185. {
  186. id: nanoid(5),
  187. type: 'block',
  188. data: {
  189. title: 'If_0',
  190. },
  191. },
  192. {
  193. id: nanoid(5),
  194. type: 'block',
  195. data: {
  196. title: 'If_1',
  197. },
  198. },
  199. ],
  200. };
  201. },
  202. },
  203. {
  204. type: 'custom',
  205. meta: {},
  206. onAdd() {
  207. return {
  208. id: \`custom_\${nanoid(5)}\`,
  209. type: 'custom',
  210. data: {
  211. title: 'Custom',
  212. content: 'this is custom content'
  213. }
  214. }
  215. }
  216. }
  217. ];`;
  218. const useEditorPropsCode = `import { useMemo } from 'react';
  219. import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
  220. import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
  221. import {
  222. type FixedLayoutProps,
  223. FlowDocumentJSON,
  224. FlowNodeRegistry,
  225. FlowTextKey,
  226. Field,
  227. FlowRendererKey,
  228. } from '@flowgram.ai/fixed-layout-editor';
  229. import { BaseNode } from './base-node'
  230. import { BranchAdder } from './branch-adder'
  231. import { NodeAdder } from '../components/node-adder';
  232. /** semi materials */
  233. export function useEditorProps(
  234. initialData: FlowDocumentJSON, // 初始化数据
  235. nodeRegistries: FlowNodeRegistry[], // 节点定义
  236. ): FixedLayoutProps {
  237. return useMemo<FixedLayoutProps>(
  238. () => ({
  239. /**
  240. * Whether to enable the background
  241. */
  242. background: true,
  243. /**
  244. * Whether it is read-only or not, the node cannot be dragged in read-only mode
  245. */
  246. readonly: false,
  247. /**
  248. * Initial data
  249. * 初始化数据
  250. */
  251. initialData,
  252. /**
  253. * 画布节点定义
  254. */
  255. nodeRegistries,
  256. /**
  257. * Get the default node registry, which will be merged with the 'nodeRegistries'
  258. * 提供默认的节点注册,这个会和 nodeRegistries 做合并
  259. */
  260. getNodeDefaultRegistry(type) {
  261. return {
  262. type,
  263. meta: {
  264. defaultExpanded: true,
  265. },
  266. formMeta: {
  267. /**
  268. * Render form
  269. */
  270. render: () => <>
  271. <Field<string> name="title">
  272. {({ field }) => <div className="demo-fixed-node-title">{field.value}</div>}
  273. </Field>
  274. <div className="demo-fixed-node-content">
  275. <Field<string> name="content">
  276. <input />
  277. </Field>
  278. </div>
  279. </>
  280. }
  281. };
  282. },
  283. /**
  284. * Materials, components can be customized based on the key
  285. * 可以通过 key 自定义 UI 组件
  286. */
  287. materials: {
  288. renderNodes: {
  289. ...defaultFixedSemiMaterials,
  290. /**
  291. * Components can be customized based on key business-side requirements.
  292. * 这里可以根据 key 业务侧定制组件
  293. */
  294. [FlowRendererKey.ADDER]: NodeAdder,
  295. [FlowRendererKey.BRANCH_ADDER]: BranchAdder,
  296. // [FlowRendererKey.DRAG_NODE]: DragNode,
  297. },
  298. renderDefaultNode: BaseNode, // 节点渲染
  299. renderTexts: {
  300. [FlowTextKey.LOOP_END_TEXT]: 'loop end',
  301. [FlowTextKey.LOOP_TRAVERSE_TEXT]: 'looping',
  302. },
  303. },
  304. /**
  305. * Node engine enable, you can configure formMeta in the FlowNodeRegistry
  306. */
  307. nodeEngine: {
  308. enable: true,
  309. },
  310. history: {
  311. enable: true,
  312. enableChangeNode: true, // Listen Node engine data change
  313. onApply(ctx, opt) {
  314. // Listen change to trigger auto save
  315. // console.log('auto save: ', ctx.document.toJSON(), opt);
  316. },
  317. },
  318. /**
  319. * 画布初始化
  320. */
  321. onInit: ctx => {
  322. /**
  323. * Data can also be dynamically loaded via fromJSON
  324. * 也可以通过 fromJSON 动态加载数据
  325. */
  326. // ctx.document.fromJSON(initialData)
  327. console.log('---- Playground Init ----');
  328. },
  329. /**
  330. * 画布销毁
  331. */
  332. onDispose: () => {
  333. console.log('---- Playground Dispose ----');
  334. },
  335. plugins: () => [
  336. /**
  337. * Minimap plugin
  338. * 缩略图插件
  339. */
  340. createMinimapPlugin({
  341. disableLayer: true,
  342. enableDisplayAllNodes: true,
  343. canvasStyle: {
  344. canvasWidth: 182,
  345. canvasHeight: 102,
  346. canvasPadding: 50,
  347. canvasBackground: 'rgba(245, 245, 245, 1)',
  348. canvasBorderRadius: 10,
  349. viewportBackground: 'rgba(235, 235, 235, 1)',
  350. viewportBorderRadius: 4,
  351. viewportBorderColor: 'rgba(201, 201, 201, 1)',
  352. viewportBorderWidth: 1,
  353. viewportBorderDashLength: 2,
  354. nodeColor: 'rgba(255, 255, 255, 1)',
  355. nodeBorderRadius: 2,
  356. nodeBorderWidth: 0.145,
  357. nodeBorderColor: 'rgba(6, 7, 9, 0.10)',
  358. overlayColor: 'rgba(255, 255, 255, 0)',
  359. },
  360. inactiveDebounceTime: 1,
  361. }),
  362. ],
  363. }),
  364. [],
  365. );
  366. }
  367. `;
  368. const baseNodeCode = `import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
  369. export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
  370. /**
  371. * Provides methods related to node rendering
  372. * 提供节点渲染相关的方法
  373. */
  374. const nodeRender = useNodeRender();
  375. /**
  376. * It can only be used when nodeEngine is enabled
  377. * 只有在节点引擎开启时候才能使用表单
  378. */
  379. const form = nodeRender.form;
  380. return (
  381. <div
  382. className="demo-fixed-node"
  383. onMouseEnter={nodeRender.onMouseEnter}
  384. onMouseLeave={nodeRender.onMouseLeave}
  385. onMouseDown={e => {
  386. // trigger drag node
  387. nodeRender.startDrag(e);
  388. e.stopPropagation();
  389. }}
  390. style={{
  391. ...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
  392. }}
  393. >
  394. {form?.render()}
  395. </div>
  396. );
  397. };
  398. `;
  399. const branchAdderCode = `import { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';
  400. import { IconPlus } from '@douyinfe/semi-icons';
  401. import { nanoid } from 'nanoid';
  402. interface PropsType {
  403. activated?: boolean;
  404. node: FlowNodeEntity;
  405. }
  406. export function BranchAdder(props: PropsType) {
  407. const { activated, node } = props;
  408. const nodeData = node.firstChild!.renderData;
  409. const ctx = useClientContext();
  410. const { operation, playground } = ctx;
  411. const { isVertical } = node;
  412. function addBranch() {
  413. const block = operation.addBlock(node, {
  414. id: \`branch_\${nanoid(5)}\`,
  415. type: 'block',
  416. data: {
  417. title: 'New Branch',
  418. content: ''
  419. }
  420. });
  421. setTimeout(() => {
  422. playground.scrollToView({
  423. bounds: block.bounds,
  424. scrollToCenter: true,
  425. });
  426. }, 10);
  427. }
  428. if (playground.config.readonlyOrDisabled) return null;
  429. const className = [
  430. 'demo-fixed-adder',
  431. isVertical ? '' : 'isHorizontal',
  432. activated ? 'activated' : ''
  433. ].join(' ');
  434. return (
  435. <div
  436. className={className}
  437. onMouseEnter={() => nodeData?.toggleMouseEnter()}
  438. onMouseLeave={() => nodeData?.toggleMouseLeave()}
  439. >
  440. <div
  441. onClick={() => {
  442. addBranch();
  443. }}
  444. aria-hidden="true"
  445. style={{ flexGrow: 1, textAlign: 'center' }}
  446. >
  447. <IconPlus />
  448. </div>
  449. </div>
  450. );
  451. }
  452. `;
  453. const miniMapCode = `import { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin';
  454. import { useService } from '@flowgram.ai/fixed-layout-editor';
  455. export const Minimap = () => {
  456. const minimapService = useService(FlowMinimapService);
  457. return (
  458. <div
  459. style={{
  460. position: 'absolute',
  461. left: 16,
  462. bottom: 51,
  463. zIndex: 100,
  464. width: 182,
  465. }}
  466. >
  467. <MinimapRender
  468. service={minimapService}
  469. containerStyles={{
  470. pointerEvents: 'auto',
  471. position: 'relative',
  472. top: 'unset',
  473. right: 'unset',
  474. bottom: 'unset',
  475. left: 'unset',
  476. }}
  477. inactiveStyle={{
  478. opacity: 1,
  479. scale: 1,
  480. translateX: 0,
  481. translateY: 0,
  482. }}
  483. />
  484. </div>
  485. );
  486. };
  487. `;
  488. const nodeAdderCode = `import { FlowNodeEntity, FlowOperationService, useClientContext, usePlayground, useService } from "@flowgram.ai/fixed-layout-editor"
  489. import { Dropdown } from '@douyinfe/semi-ui'
  490. import { IconPlusCircle } from "@douyinfe/semi-icons";
  491. import { nodeRegistries } from '../node-registries';
  492. export const NodeAdder = (props: {
  493. from: FlowNodeEntity;
  494. to?: FlowNodeEntity;
  495. hoverActivated: boolean;
  496. }) => {
  497. const { from, hoverActivated } = props;
  498. const playground = usePlayground();
  499. const context = useClientContext();
  500. const flowOperationService = useService(FlowOperationService) as FlowOperationService;
  501. const add = (addProps: any) => {
  502. const blocks = addProps.blocks ? addProps.blocks : undefined;
  503. const block = flowOperationService.addFromNode(from, {
  504. ...addProps,
  505. blocks,
  506. });
  507. setTimeout(() => {
  508. playground.scrollToView({
  509. bounds: block.bounds,
  510. scrollToCenter: true,
  511. });
  512. }, 10);
  513. };
  514. if (playground.config.readonlyOrDisabled) return null;
  515. return (
  516. <Dropdown
  517. render={
  518. <Dropdown.Menu>
  519. {nodeRegistries.map(registry => <Dropdown.Item onClick={() => {
  520. const props = registry?.onAdd(context, from);
  521. add(props);
  522. }}>{registry.type}</Dropdown.Item>)}
  523. </Dropdown.Menu>
  524. }
  525. >
  526. <div
  527. style={{
  528. width: hoverActivated ? 15 : 6,
  529. height: hoverActivated ? 15 : 6,
  530. backgroundColor: 'rgb(143, 149, 158)',
  531. color: '#fff',
  532. borderRadius: '50%',
  533. cursor: 'pointer'
  534. }}
  535. >
  536. {hoverActivated ?
  537. <IconPlusCircle
  538. style={{
  539. color: '#3370ff',
  540. backgroundColor: '#fff',
  541. borderRadius: 15
  542. }}
  543. /> : null
  544. }
  545. </div>
  546. </Dropdown>
  547. );
  548. }
  549. `;
  550. const toolsCode = `import { useEffect, useState } from 'react'
  551. import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
  552. export function Tools() {
  553. const { history } = useClientContext();
  554. const tools = usePlaygroundTools();
  555. const [canUndo, setCanUndo] = useState(false);
  556. const [canRedo, setCanRedo] = useState(false);
  557. useEffect(() => {
  558. const disposable = history.undoRedoService.onChange(() => {
  559. setCanUndo(history.canUndo());
  560. setCanRedo(history.canRedo());
  561. });
  562. return () => disposable.dispose();
  563. }, [history]);
  564. return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}>
  565. <button onClick={() => tools.zoomin()}>ZoomIn</button>
  566. <button onClick={() => tools.zoomout()}>ZoomOut</button>
  567. <button onClick={() => tools.fitView()}>Fitview</button>
  568. <button onClick={() => tools.changeLayout()}>ChangeLayout</button>
  569. <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>
  570. <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>
  571. <span>{Math.floor(tools.zoom * 100)}%</span>
  572. </div>
  573. }
  574. `;
  575. export const FixedLayoutSimplePreview = () => (
  576. <PreviewEditor
  577. files={{
  578. 'App.js': `import React from 'react';
  579. import { Editor } from './index.tsx';
  580. const App = () => {
  581. return <Editor />
  582. }
  583. export default App;`,
  584. 'index.tsx': indexCode,
  585. 'index.css': indexCssCode,
  586. 'initial-data.ts': initialDataCode,
  587. 'node-registries.ts': nodeRegistriesCode,
  588. 'use-editor-props.tsx': useEditorPropsCode,
  589. 'base-node.tsx': baseNodeCode,
  590. 'branch-adder.tsx': branchAdderCode,
  591. 'minimap.tsx': miniMapCode,
  592. 'node-adder.tsx': nodeAdderCode,
  593. 'tools.tsx': toolsCode,
  594. }}
  595. previewStyle={{
  596. height: 500,
  597. }}
  598. editorStyle={{
  599. height: 500,
  600. }}
  601. >
  602. <FixedLayoutSimple />
  603. </PreviewEditor>
  604. );