mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-12 21:50:23 +08:00
impl embed schema editor
This commit is contained in:
parent
2b81c67f48
commit
69c3462d6c
68
packages/editor/src/components/CodeEditor/SchemaEditor.tsx
Normal file
68
packages/editor/src/components/CodeEditor/SchemaEditor.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import CodeMirror from 'codemirror';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { css } from '@emotion/react';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/addon/fold/brace-fold';
|
||||
import 'codemirror/addon/fold/foldgutter';
|
||||
import 'codemirror/addon/fold/foldgutter.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/ayu-mirage.css';
|
||||
|
||||
export const SchemaEditor: React.FC<{
|
||||
defaultCode: string;
|
||||
onChange: (v: string) => void;
|
||||
}> = ({ defaultCode, onChange }) => {
|
||||
const style = css`
|
||||
.CodeMirror {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const wrapperEl = useRef<HTMLDivElement>(null);
|
||||
const cm = useRef<CodeMirror.Editor | null>(null);
|
||||
useEffect(() => {
|
||||
if (!wrapperEl.current) {
|
||||
return;
|
||||
}
|
||||
if (!cm.current) {
|
||||
cm.current = CodeMirror(wrapperEl.current, {
|
||||
value: defaultCode,
|
||||
mode: {
|
||||
name: 'javascript',
|
||||
json: true,
|
||||
},
|
||||
foldGutter: true,
|
||||
lineWrapping: true,
|
||||
lineNumbers: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
foldOptions: {
|
||||
widget: () => {
|
||||
return '\u002E\u002E\u002E';
|
||||
},
|
||||
},
|
||||
theme: 'ayu-mirage',
|
||||
/**
|
||||
* Codemirror has a serach addon which can search all the content
|
||||
* without render all.
|
||||
* But it's search behavior is differnet with popular code editors
|
||||
* and the native UX of the browser:
|
||||
* https://github.com/codemirror/CodeMirror/issues/4491#issuecomment-284741358
|
||||
* So since our schema is not that large, currently we will render
|
||||
* all content to support native search.
|
||||
*/
|
||||
viewportMargin: Infinity,
|
||||
});
|
||||
}
|
||||
const handler = (instance: CodeMirror.Editor) => {
|
||||
onChange(instance.getValue());
|
||||
};
|
||||
cm.current.on('change', handler);
|
||||
return () => {
|
||||
cm.current?.off('change', handler);
|
||||
};
|
||||
}, [defaultCode]);
|
||||
|
||||
return <Box css={style} ref={wrapperEl} height="100%" width="100%"></Box>;
|
||||
};
|
@ -7,7 +7,6 @@ import 'codemirror/addon/fold/brace-fold';
|
||||
import 'codemirror/addon/fold/foldgutter';
|
||||
import 'codemirror/addon/fold/foldgutter.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/duotone-dark.css';
|
||||
|
||||
export const StateEditor: React.FC<{ code: string }> = ({ code }) => {
|
||||
const style = css`
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './StateEditor';
|
||||
export * from './SchemaEditor';
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { GridCallbacks, DIALOG_CONTAINER_ID, initMetaUI } from '@meta-ui/runtime';
|
||||
import produce from 'immer';
|
||||
import { Box, Tabs, TabList, Tab, TabPanels, TabPanel } from '@chakra-ui/react';
|
||||
import { Box, Tabs, TabList, Tab, TabPanels, TabPanel, Flex } from '@chakra-ui/react';
|
||||
import { StructureTree } from './StructureTree';
|
||||
import {
|
||||
CreateComponentOperation,
|
||||
ModifyComponentPropertyOperation,
|
||||
ReplaceAppOperation,
|
||||
} from '../operations/Operations';
|
||||
import { eventBus, SelectComponentEvent } from '../eventBus';
|
||||
import { ComponentForm } from './ComponentForm';
|
||||
@ -15,7 +16,7 @@ import { EditorHeader } from './EditorHeader';
|
||||
import { PreviewModal } from './PreviewModal';
|
||||
import { KeyboardEventWrapper } from './KeyboardEventWrapper';
|
||||
import { ComponentWrapper } from './ComponentWrapper';
|
||||
import { StateEditor } from './CodeEditor';
|
||||
import { StateEditor, SchemaEditor } from './CodeEditor';
|
||||
import { AppModelManager } from '../operations/AppModelManager';
|
||||
|
||||
type ReturnOfInit = ReturnType<typeof initMetaUI>;
|
||||
@ -40,6 +41,8 @@ export const Editor: React.FC<Props> = ({
|
||||
);
|
||||
const [scale, setScale] = useState(100);
|
||||
const [preview, setPreview] = useState(false);
|
||||
const [codeMode, setCodeMode] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
eventBus.on(SelectComponentEvent, id => {
|
||||
@ -91,6 +94,92 @@ export const Editor: React.FC<Props> = ({
|
||||
);
|
||||
}, [app, gridCallbacks]);
|
||||
|
||||
const renderMain = () => {
|
||||
const appBox = (
|
||||
<Box flex="1" background="gray.50" p={4}>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
background="white"
|
||||
transform={`scale(${scale / 100})`}
|
||||
>
|
||||
<Box id={DIALOG_CONTAINER_ID} width="full" height="full" position="absolute" />
|
||||
{appComponent}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (codeMode) {
|
||||
return (
|
||||
<Flex width="100%" height="100%">
|
||||
<Box flex="1">
|
||||
<SchemaEditor defaultCode={JSON.stringify(app, null, 2)} onChange={setCode} />
|
||||
</Box>
|
||||
{appBox}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box width="280px" borderRightWidth="1px" borderColor="gray.200">
|
||||
<Tabs
|
||||
align="center"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
textAlign="left"
|
||||
isLazy
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>UI Tree</Tab>
|
||||
<Tab>State</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto">
|
||||
<TabPanel p={0}>
|
||||
<StructureTree
|
||||
app={app}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={id => setSelectedComponentId(id)}
|
||||
registry={registry}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0} height="100%">
|
||||
<StateEditor code={JSON.stringify(stateStore, null, 2)} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
{appBox}
|
||||
<Box width="320px" borderLeftWidth="1px" borderColor="gray.200" overflow="auto">
|
||||
<Tabs
|
||||
align="center"
|
||||
textAlign="left"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>Inspect</Tab>
|
||||
<Tab>Insert</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto">
|
||||
<TabPanel p={0}>
|
||||
<ComponentForm
|
||||
app={app}
|
||||
selectedId={selectedComponentId}
|
||||
registry={registry}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<ComponentList registry={registry} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardEventWrapper selectedComponentId={selectedComponentId}>
|
||||
<Box display="flex" height="100%" width="100%" flexDirection="column">
|
||||
@ -98,78 +187,16 @@ export const Editor: React.FC<Props> = ({
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
onPreview={() => setPreview(true)}
|
||||
codeMode={codeMode}
|
||||
onCodeMode={v => {
|
||||
setCodeMode(v);
|
||||
if (!v && code) {
|
||||
eventBus.send('operation', new ReplaceAppOperation(JSON.parse(code)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box display="flex" flex="1" overflow="auto">
|
||||
<Box width="280px" borderRightWidth="1px" borderColor="gray.200">
|
||||
<Tabs
|
||||
align="center"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
textAlign="left"
|
||||
isLazy
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>UI Tree</Tab>
|
||||
<Tab>State</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto">
|
||||
<TabPanel p={0}>
|
||||
<StructureTree
|
||||
app={app}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={id => setSelectedComponentId(id)}
|
||||
registry={registry}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0} height="100%">
|
||||
<StateEditor code={JSON.stringify(stateStore, null, 2)} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box flex="1" background="gray.50" p={4}>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
background="white"
|
||||
transform={`scale(${scale / 100})`}
|
||||
>
|
||||
<Box
|
||||
id={DIALOG_CONTAINER_ID}
|
||||
width="full"
|
||||
height="full"
|
||||
position="absolute"
|
||||
/>
|
||||
{appComponent}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box width="320px" borderLeftWidth="1px" borderColor="gray.200" overflow="auto">
|
||||
<Tabs
|
||||
align="center"
|
||||
textAlign="left"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<TabList background="gray.50">
|
||||
<Tab>Inspect</Tab>
|
||||
<Tab>Insert</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex="1" overflow="auto">
|
||||
<TabPanel p={0}>
|
||||
<ComponentForm
|
||||
app={app}
|
||||
selectedId={selectedComponentId}
|
||||
registry={registry}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<ComponentList registry={registry} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
{renderMain()}
|
||||
</Box>
|
||||
</Box>
|
||||
{preview && (
|
||||
|
@ -5,10 +5,20 @@ export const EditorHeader: React.FC<{
|
||||
scale: number;
|
||||
setScale: (v: number) => void;
|
||||
onPreview: () => void;
|
||||
}> = ({ scale, setScale, onPreview }) => {
|
||||
codeMode: boolean;
|
||||
onCodeMode: (v: boolean) => void;
|
||||
}> = ({ scale, setScale, onPreview, onCodeMode, codeMode }) => {
|
||||
return (
|
||||
<Flex p={2} borderBottomWidth="2px" borderColor="gray.200" align="center">
|
||||
<Flex flex="1" />
|
||||
<Flex flex="1">
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
variant={codeMode ? 'solid' : 'outline'}
|
||||
onClick={() => onCodeMode(!codeMode)}
|
||||
>
|
||||
{codeMode ? 'save' : 'code mode'}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex flex="1" align="center" justify="center">
|
||||
<Button size="sm" disabled={scale <= 50} onClick={() => setScale(scale - 10)}>
|
||||
-
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
RemoveTraitOperation,
|
||||
ModifyTraitPropertiesOperation,
|
||||
ReplaceComponentPropertyOperation,
|
||||
ReplaceAppOperation,
|
||||
} from './Operations';
|
||||
import { produce } from 'immer';
|
||||
import { eventBus } from '../eventBus';
|
||||
@ -287,6 +288,13 @@ export class AppModelManager {
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'replaceApp': {
|
||||
const rao = o as ReplaceAppOperation;
|
||||
newApp = produce(this.app, () => {
|
||||
return rao.app;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.updateApp(newApp);
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Application } from '@meta-ui/core';
|
||||
|
||||
export type Operations =
|
||||
| CreateComponentOperation
|
||||
| RemoveComponentOperation
|
||||
| ModifyComponentPropertyOperation;
|
||||
| ModifyComponentPropertyOperation
|
||||
| ReplaceAppOperation;
|
||||
export class CreateComponentOperation {
|
||||
kind = 'createComponent';
|
||||
|
||||
@ -73,3 +76,8 @@ export class SortComponentOperation {
|
||||
kind = 'sortComponent';
|
||||
constructor(public componentId: string, public direction: 'up' | 'down') {}
|
||||
}
|
||||
|
||||
export class ReplaceAppOperation {
|
||||
kind = 'replaceApp';
|
||||
constructor(public app: Application) {}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user