impl embed schema editor

This commit is contained in:
Yanzhen Yu 2021-11-16 18:50:22 +08:00
parent 2b81c67f48
commit 69c3462d6c
7 changed files with 197 additions and 76 deletions

View 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>;
};

View File

@ -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`

View File

@ -1 +1,2 @@
export * from './StateEditor';
export * from './SchemaEditor';

View File

@ -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 && (

View File

@ -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)}>
-

View File

@ -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);
}

View File

@ -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) {}
}