This commit is contained in:
Bowen Tan 2021-11-30 13:59:54 +08:00
parent 51c33c24fd
commit c85008204a
7 changed files with 234 additions and 209 deletions

View File

@ -42,6 +42,8 @@
"framer-motion": "^4",
"immer": "^9.0.6",
"lodash-es": "^4.17.21",
"mobx": "^6.3.8",
"mobx-react-lite": "^3.2.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},

View File

@ -0,0 +1,19 @@
import { makeAutoObservable } from 'mobx';
import { eventBus } from './eventBus';
class EditorStore {
selectedComponentId = '';
constructor() {
eventBus.on('selectComponent', id => {
this.setSelectedComponentId(id);
});
makeAutoObservable(this);
}
setSelectedComponentId(val: string) {
this.selectedComponentId = val;
}
}
export const editorStore = new EditorStore()

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { css } from '@emotion/react';
import { ComponentWrapperType } from '@sunmao-ui/runtime';
import { eventBus, HoverComponentEvent, SelectComponentEvent } from '../eventBus';
import { eventBus } from '../eventBus';
// children of components in this list should render height as 100%
const fullHeightList = ['core/v1/grid_layout'];
@ -15,10 +15,10 @@ export const ComponentWrapper: ComponentWrapperType = props => {
useEffect(() => {
const handler = (event: string, payload: any) => {
switch (event) {
case SelectComponentEvent:
case 'selectComponent':
setSelectedComponentId(payload);
break;
case HoverComponentEvent:
case 'hoverComponentEvent':
setHoverComponentId(payload);
break;
}
@ -52,15 +52,15 @@ export const ComponentWrapper: ComponentWrapperType = props => {
`;
const onClickWrapper = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
eventBus.send(SelectComponentEvent as any, component.id);
eventBus.send('selectComponent' as any, component.id);
};
const onMouseEnterWrapper = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
eventBus.send(HoverComponentEvent as any, component.id);
eventBus.send('hoverComponentEvent' as any, component.id);
};
const onMouseLeaveWrapper = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
eventBus.send(HoverComponentEvent as any, '');
eventBus.send('hoverComponentEvent' as any, '');
};
return (
<div

View File

@ -1,9 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { Application } from '@sunmao-ui/core';
import { GridCallbacks, DIALOG_CONTAINER_ID, initSunmaoUI } from '@sunmao-ui/runtime';
import { Box, Tabs, TabList, Tab, TabPanels, TabPanel, Flex } from '@chakra-ui/react';
import { observer } from 'mobx-react-lite';
import { StructureTree } from './StructureTree';
import { eventBus, SelectComponentEvent } from '../eventBus';
import { eventBus } from '../eventBus';
import { ComponentForm } from './ComponentForm';
import { ComponentList } from './ComponentsList';
import { useAppModel } from '../operations/useAppModel';
@ -20,6 +21,7 @@ import {
ReplaceAppLeafOperation,
} from '../operations/leaf';
import { CreateComponentBranchOperation } from '../operations/branch';
import { editorStore } from '../EditorStore';
type ReturnOfInit = ReturnType<typeof initSunmaoUI>;
@ -32,203 +34,198 @@ type Props = {
appStorage: AppStorage;
};
export const Editor: React.FC<Props> = ({
App,
registry,
stateStore,
appModelManager,
appStorage,
}) => {
const { components } = useAppModel(appModelManager);
export const Editor: React.FC<Props> = observer(
({ App, registry, stateStore, appModelManager, appStorage }) => {
const { components } = useAppModel(appModelManager);
const [selectedComponentId, setSelectedComponentId] = useState(
components?.[0]?.id || ''
);
const [scale, setScale] = useState(100);
const [preview, setPreview] = useState(false);
const [codeMode, setCodeMode] = useState(false);
const [code, setCode] = useState('');
const [scale, setScale] = useState(100);
const [preview, setPreview] = useState(false);
const [codeMode, setCodeMode] = useState(false);
const [code, setCode] = useState('');
useEffect(() => {
eventBus.on(SelectComponentEvent, id => {
setSelectedComponentId(id);
});
}, [setSelectedComponentId]);
const gridCallbacks: GridCallbacks = useMemo(() => {
return {
// drag an existing component
onDragStop(id, layout) {
eventBus.send(
'operation',
new ModifyComponentPropertiesLeafOperation({
componentId: id,
properties: { layout },
})
);
},
// drag a new component from tool box
onDrop(id, layout, _, e) {
const component = e.dataTransfer?.getData('component') || '';
eventBus.send(
'operation',
new CreateComponentBranchOperation({
componentType: component,
parentId: id,
slot: 'content',
layout,
})
);
},
};
}, []);
const gridCallbacks: GridCallbacks = useMemo(() => {
return {
// drag an existing component
onDragStop(id, layout) {
eventBus.send(
'operation',
new ModifyComponentPropertiesLeafOperation({
componentId: id,
properties: { layout },
})
);
},
// drag a new component from tool box
onDrop(id, layout, _, e) {
const component = e.dataTransfer?.getData('component') || '';
eventBus.send(
'operation',
new CreateComponentBranchOperation({
componentType: component,
parentId: id,
slot: 'content',
layout,
})
);
},
};
}, []);
const app = useMemo<Application>(() => {
return {
version: 'sunmao/v1',
kind: 'Application',
metadata: {
name: 'some App',
},
spec: {
components,
},
};
}, [components]);
const app = useMemo<Application>(() => {
return {
version: 'sunmao/v1',
kind: 'Application',
metadata: {
name: 'some App',
},
spec: {
components,
},
};
}, [components]);
const appComponent = useMemo(() => {
return (
<App
options={app}
debugEvent={false}
debugStore={false}
gridCallbacks={gridCallbacks}
componentWrapper={ComponentWrapper}
/>
);
}, [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) {
const appComponent = useMemo(() => {
return (
<Flex width="100%" height="100%">
<Box flex="1">
<SchemaEditor defaultCode={JSON.stringify(app, null, 2)} onChange={setCode} />
<App
options={app}
debugEvent={false}
debugStore={false}
gridCallbacks={gridCallbacks}
componentWrapper={ComponentWrapper}
/>
);
}, [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>Explorer</Tab>
<Tab>UI Tree</Tab>
<Tab>State</Tab>
</TabList>
<TabPanels flex="1" overflow="auto">
<TabPanel>
<Explorer appStorage={appStorage} />
</TabPanel>
<TabPanel p={0}>
<StructureTree
components={components}
selectedComponentId={editorStore.selectedComponentId}
onSelectComponent={id => editorStore.setSelectedComponentId(id)}
registry={registry}
/>
</TabPanel>
<TabPanel p={0} height="100%">
<StateEditor code={JSON.stringify(stateStore, null, 2)} />
</TabPanel>
</TabPanels>
</Tabs>
</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>Explorer</Tab>
<Tab>UI Tree</Tab>
<Tab>State</Tab>
</TabList>
<TabPanels flex="1" overflow="auto">
<TabPanel>
<Explorer appStorage={appStorage} />
</TabPanel>
<TabPanel p={0}>
<StructureTree
components={components}
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}
appModelManager={appModelManager}
/>
</TabPanel>
<TabPanel p={0}>
<ComponentList registry={registry} />
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</>
);
};
return (
<KeyboardEventWrapper selectedComponentId={selectedComponentId}>
<Box display="flex" height="100%" width="100%" flexDirection="column">
<EditorHeader
scale={scale}
setScale={setScale}
onPreview={() => setPreview(true)}
codeMode={codeMode}
onCodeMode={v => {
setCodeMode(v);
if (!v && code) {
eventBus.send('operation', new ReplaceAppLeafOperation(JSON.parse(code)));
}
}}
/>
<Box display="flex" flex="1" overflow="auto">
{renderMain()}
</Box>
</Box>
{preview && (
<PreviewModal onClose={() => setPreview(false)}>
<Box width="100%" height="100%">
<App
options={JSON.parse(JSON.stringify(app))}
debugEvent={false}
debugStore={false}
/>
<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={editorStore.selectedComponentId}
registry={registry}
appModelManager={appModelManager}
/>
</TabPanel>
<TabPanel p={0}>
<ComponentList registry={registry} />
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</PreviewModal>
)}
</KeyboardEventWrapper>
);
};
</>
);
};
return (
<KeyboardEventWrapper selectedComponentId={editorStore.selectedComponentId}>
<Box display="flex" height="100%" width="100%" flexDirection="column">
<EditorHeader
scale={scale}
setScale={setScale}
onPreview={() => setPreview(true)}
codeMode={codeMode}
onCodeMode={v => {
setCodeMode(v);
if (!v && code) {
eventBus.send('operation', new ReplaceAppLeafOperation(JSON.parse(code)));
}
}}
/>
<Box display="flex" flex="1" overflow="auto">
{renderMain()}
</Box>
</Box>
{preview && (
<PreviewModal onClose={() => setPreview(false)}>
<Box width="100%" height="100%">
<App
options={JSON.parse(JSON.stringify(app))}
debugEvent={false}
debugStore={false}
/>
</Box>
</PreviewModal>
)}
</KeyboardEventWrapper>
);
}
);

View File

@ -3,17 +3,14 @@ import { Application, ApplicationComponent } from '@sunmao-ui/core';
import { IOperation } from './operations/type';
import { ImplementedRuntimeModule } from '../../runtime/lib';
export const SelectComponentEvent = 'selectComponent';
export const HoverComponentEvent = 'hoverComponent';
export type EventNames = {
operation: IOperation;
redo: undefined;
undo: undefined;
componentsReload: ApplicationComponent[];
componentsChange: ApplicationComponent[];
[SelectComponentEvent]: string;
[HoverComponentEvent]: string;
selectComponent: string;
hoverComponent: string;
// for state decorators
appChange: Application;

View File

@ -1,5 +1,5 @@
import { ApplicationComponent } from '@sunmao-ui/core';
import { eventBus, SelectComponentEvent } from '../../eventBus';
import { eventBus } from '../../eventBus';
import { BaseLeafOperation } from '../type';
export type UpdateSelectComponentLeafOperationContext = {
@ -12,21 +12,21 @@ export class UpdateSelectComponentLeafOperation extends BaseLeafOperation<Update
do(prev: ApplicationComponent[]): ApplicationComponent[] {
this.prevId = this.context.componentId || prev[0].id;
setTimeout(() => {
eventBus.send(SelectComponentEvent, this.context.newId);
eventBus.send('selectComponent', this.context.newId);
});
return prev;
}
redo(prev: ApplicationComponent[]): ApplicationComponent[] {
setTimeout(() => {
eventBus.send(SelectComponentEvent, this.context.newId);
eventBus.send('selectComponent', this.context.newId);
});
return prev;
}
undo(prev: ApplicationComponent[]): ApplicationComponent[] {
setTimeout(() => {
eventBus.send(SelectComponentEvent, this.prevId);
eventBus.send('selectComponent', this.prevId);
});
return prev;
}

View File

@ -7264,6 +7264,16 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mobx-react-lite@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.2.tgz#cee5f09e6b0e4d2705d87276baf1de74d997fa33"
integrity sha512-FxJJMqmHcnQYOVVs2DdjNHioGlFsXF5/9VHztS9NAfIT3DYrxNZzVi119Zr/OmlWKkWNkAsssSNzPkqautfL4A==
mobx@^6.3.8:
version "6.3.8"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.8.tgz#a4654b1b69c683237f2444cf0af39a621d4611af"
integrity sha512-RfG2y766P1o9u9xrQht/HBXEoUPIr4B3Gjri3reLW/TuHm3I/3TfBBS0OaeMbw19RIF0AymqjDNlJgakN4ZK7g==
modify-values@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"