wip init grid onLayoutChange

This commit is contained in:
Bowen Tan 2021-09-28 17:37:53 +08:00
parent 4e9e565bd7
commit c1b1c65029
14 changed files with 166 additions and 517 deletions

View File

@ -1,4 +1,4 @@
import { ChakraProvider, FormControl, FormLabel, Input } from '@chakra-ui/react';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { Application } from '@meta-ui/core';
import React from 'react';
import { eventBus } from '../../eventBus';
@ -31,11 +31,9 @@ export const ComponentForm: React.FC<Props> = props => {
});
return (
<ChakraProvider>
<div>
<div>{selectedComponent?.id}</div>
<form>{fields}</form>
</div>
</ChakraProvider>
<div>
<div>{selectedComponent?.id}</div>
<form>{fields}</form>
</div>
);
};

View File

@ -1,15 +1,19 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Application } from '@meta-ui/core';
import { Box } from '@chakra-ui/react';
import { DialogFormSchema } from '../constants';
import { DefaultAppSchema } from '../constants';
import { App } from '../metaUI';
import { StructureTree } from './StructureTree';
import { OperationManager } from '../operations/OperationManager';
import { CreateComponentOperation } from '../operations/Operations';
import {
CreateComponentOperation,
ModifyComponentPropertyOperation,
} from '../operations/Operations';
import { eventBus } from '../eventBus';
import { ComponentForm } from './ComponentForm';
import { ComponentList } from './ComponentsList';
const operationManager = new OperationManager(DialogFormSchema);
const operationManager = new OperationManager(DefaultAppSchema);
export const Editor = () => {
const [selectedComponentId, setSelectedComponentId] = useState('');
@ -45,7 +49,7 @@ export const Editor = () => {
const onClickAdd = useCallback(() => {
eventBus.send(
'operation',
new CreateComponentOperation('root', 'root', 'chakra_ui/v1/input')
new CreateComponentOperation('root', 'container', 'chakra_ui/v1/input')
);
}, [app, setApp]);
@ -54,14 +58,31 @@ export const Editor = () => {
}, [app, setApp]);
return (
<Box display="flex" height="100vh">
<Box display="flex" height="100vh" width="100vw">
<Box flex="1">
<div className="droppable-element" draggable={true} unselectable="on">
hhhhh
</div>
<button onClick={onClickAdd}></button>
<button onClick={onClickUndo}></button>
<StructureTree app={app} onSelectComponent={id => setSelectedComponentId(id)} />
</Box>
<Box flex="1">
<ComponentList />{' '}
</Box>
<Box flex="3" borderRight="2px solid black">
<App options={app} componentWrapper={Wrapper} />
<App
options={app}
debugEvent={false}
debugStore={false}
onLayoutChange={(id, layout) => {
eventBus.send(
'operation',
new ModifyComponentPropertyOperation(id, 'layout', layout)
);
console.log('layout变啦', id, layout);
}}
/>
</Box>
<Box flex="1" borderRight="2px solid black">
<ComponentForm app={app} selectedId={selectedComponentId} />

View File

@ -1,5 +1,7 @@
import React from 'react';
import { Application } from '@meta-ui/core';
import { eventBus } from '../../eventBus';
import { RemoveComponentOperation } from '../../operations/Operations';
type ChildrenMap = Map<string, SlotsMap>;
type SlotsMap = Map<string, Array<Application['spec']['components'][0]>>;
@ -46,6 +48,11 @@ export const StructureTree: React.FC<Props> = props => {
);
});
}
const onClickRemove = () => {
eventBus.send('operation', new RemoveComponentOperation(component.id));
};
return (
<div key={component.id} style={{ paddingLeft: '16px' }}>
<strong
@ -55,6 +62,7 @@ export const StructureTree: React.FC<Props> = props => {
>
{component.id}
</strong>
<span onClick={onClickRemove}></span>
{slotsEle}
</div>
);

View File

@ -1,538 +1,110 @@
import { Application } from '@meta-ui/core';
export const DialogFormSchema: Application = {
version: 'example/v1',
export const DefaultAppSchema: Application = {
kind: 'Application',
version: 'example/v1',
metadata: {
name: 'dialog form',
description: 'dialog form example',
name: 'basic_grid_layout',
description: 'basic grid layout example',
},
spec: {
components: [
{
id: 'fetchVolumes',
type: 'core/v1/dummy',
properties: {},
traits: [
{
type: 'core/v1/fetch',
properties: {
name: 'query',
url: 'https://61373521eac1410017c18209.mockapi.io/Volume',
method: 'get',
lazy: false,
},
},
],
},
{
id: 'createVolume',
type: 'core/v1/dummy',
properties: {},
traits: [
{
type: 'core/v1/fetch',
properties: {
url: 'https://61373521eac1410017c18209.mockapi.io/Volume',
method: 'post',
lazy: true,
headers: [{ key: 'Content-Type', value: 'application/json' }],
body: '{{ form.data }}',
onComplete: [
{
componentId: '$utils',
method: {
name: 'toast.open',
parameters: {
id: 'createSuccessToast',
title: '恭喜',
description: '创建虚拟卷成功',
position: 'bottom-right',
duration: null,
isClosable: true,
},
},
},
{
componentId: 'editDialog',
method: {
name: 'cancelDialog',
},
},
{
componentId: 'form',
method: {
name: 'resetForm',
},
},
{
componentId: 'fetchVolumes',
method: {
name: 'triggerFetch',
parameters: 'query',
},
wait: {},
disabled: 'false',
},
],
},
},
],
},
{
id: 'deleteVolume',
type: 'core/v1/dummy',
properties: {},
traits: [
{
type: 'core/v1/fetch',
properties: {
url: 'https://61373521eac1410017c18209.mockapi.io/Volume/{{ table.selectedItem ? table.selectedItem.id : "" }}',
method: 'delete',
lazy: true,
onComplete: [
{
componentId: 'fetchVolumes',
method: {
name: 'triggerFetch',
parameters: 'query',
},
wait: {},
disabled: 'false',
},
],
},
},
],
},
{
id: 'root',
type: 'chakra_ui/v1/root',
properties: {},
type: 'core/v1/grid_layout',
properties: {
layout: [
{
x: 0,
y: 0,
w: 5,
h: 2,
i: 'input',
isResizable: false,
},
{
x: 4,
y: 0,
w: 4,
h: 9,
i: 'box1',
},
{
x: 8,
y: 0,
w: 2,
h: 12,
i: 'box2',
},
],
},
traits: [],
},
{
id: 'editDialog',
type: 'chakra_ui/v1/dialog',
properties: {
title: 'This is a dialog',
confirmButton: {
text: '保存',
colorScheme: 'purple',
},
cancelButton: {
text: '取消',
},
disableConfirm: '{{ form.isFormInvalid }}',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'root',
slot: 'root',
},
},
},
{
type: 'core/v1/event',
properties: {
events: [
{
event: 'confirmDialog',
componentId: 'createVolume',
method: {
name: 'triggerFetch',
},
wait: {},
disabled: false,
},
],
},
},
],
},
{
id: 'table',
type: 'chakra_ui/v1/table',
properties: {
data: '{{ fetchVolumes.fetch.data }}',
majorKey: 'id',
rowsPerPage: 5,
columns: [
{
key: 'id',
title: 'ID',
type: 'text',
},
{
key: 'name',
title: '名称',
type: 'text',
},
{
key: 'type',
title: '类别',
type: 'text',
displayValue:
'{{$listItem.type === "sharing" ? "共享虚拟卷" : "虚拟卷"}}',
},
{
key: 'size',
title: '容量',
type: 'text',
displayValue: '{{$listItem.size}} GiB',
},
{
key: 'policy',
title: '存储策略',
type: 'text',
},
{
key: 'isActive',
title: '是否激活',
type: 'text',
displayValue: '{{$listItem.isActive ? "是" : "否"}}',
},
{
key: 'operation',
title: '操作',
type: 'button',
buttonConfig: {
text: '删除',
events: [
{
componentId: 'deleteVolume',
method: {
name: 'triggerFetch',
},
},
],
},
},
{
key: 'edit',
title: '创建',
type: 'button',
buttonConfig: {
text: '创建',
events: [
{
componentId: 'editDialog',
method: {
name: 'openDialog',
parameters: {
title: '创建虚拟卷',
},
},
},
],
},
},
],
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'root',
slot: 'root',
},
},
},
],
},
{
id: 'form',
type: 'chakra_ui/v1/form',
properties: {
hideSubmit: true,
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'editDialog',
slot: 'content',
},
},
},
],
},
{
id: 'nameFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '名称',
fieldName: 'name',
isRequired: true,
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'nameInput',
id: 'input',
type: 'chakra_ui/v1/input',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.name : "" }}',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'nameFormControl',
slot: 'content',
},
},
},
{
type: 'core/v1/validation',
properties: {
value: '{{ nameInput.value || "" }}',
maxLength: 10,
minLength: 2,
},
},
],
},
{
id: 'typeFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '类型',
fieldName: 'type',
helperText:
'共享虚拟卷支持被多台虚拟机同时挂载。类型创建后不可修改。',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'typeRadioGroup',
type: 'chakra_ui/v1/radio_group',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.type : "notSharing" }}',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'typeFormControl',
slot: 'content',
},
},
},
],
},
{
id: 'radio1',
type: 'chakra_ui/v1/radio',
properties: {
text: {
raw: '虚拟卷',
format: 'plain',
},
value: 'notSharing',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'typeRadioGroup',
slot: 'content',
},
},
},
],
},
{
id: 'radio2',
type: 'chakra_ui/v1/radio',
properties: {
text: {
raw: '共享虚拟卷',
format: 'plain',
},
value: 'sharing',
size: 'md',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'typeRadioGroup',
slot: 'content',
},
},
},
],
},
{
id: 'sizeFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '容量',
fieldName: 'size',
variant: 'filled',
placeholder: 'This a example',
size: 'lg',
colorScheme: 'pink',
focusBorderColor: 'pink.500',
isDisabled: false,
isRequired: true,
left: {
type: 'addon',
children: 'https://',
},
right: {
type: 'element',
children: '.com',
color: 'red',
fontSize: '16px',
},
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
id: 'root',
slot: 'container',
},
},
},
],
},
{
id: 'sizeInput',
type: 'chakra_ui/v1/number_input',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.size : 0 }}',
min: 0,
max: 100,
step: 5,
precision: 2,
clampValueOnBlur: false,
allowMouseWheel: true,
},
id: 'box1',
type: 'chakra_ui/v1/box',
properties: {},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'sizeFormControl',
slot: 'content',
id: 'root',
slot: 'container',
},
},
},
],
},
{
id: 'policyFormControl',
type: 'chakra_ui/v1/formControl',
id: 'box2',
type: 'chakra_ui/v1/box',
properties: {
label: '存储策略',
fieldName: 'policy',
bgColor: 'pink',
w: '100%',
h: '100%',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'policySelect',
type: 'chakra_ui/v1/select',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.policy : "2thin" }}',
options: [
{
value: '2thin',
label: '2 副本,精简置备',
},
{
value: '3thin',
label: '3 副本,精简置备',
},
{
value: '2thick',
label: '2 副本,厚置备',
},
{
value: '3thick',
label: '3 副本,厚置备',
},
],
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'policyFormControl',
slot: 'content',
},
},
},
],
},
{
id: 'isActiveFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '激活',
fieldName: 'isActive',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'checkbox',
type: 'chakra_ui/v1/checkbox',
properties: {
value: 'isActive',
defaultIsChecked:
'{{table.selectedItem ? !!table.selectedItem.isActive : false}}',
text: {
raw: '激活',
format: 'plain',
},
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'isActiveFormControl',
slot: 'content',
id: 'root',
slot: 'container',
},
},
},

View File

@ -1,3 +1,4 @@
import { ChakraProvider } from '@chakra-ui/react';
import { css } from '@emotion/react';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom';
@ -5,13 +6,15 @@ import { Editor } from './components/Editor';
export default function renderApp() {
ReactDOM.render(
<StrictMode>
<div
css={css`
display: flex;
`}
>
<Editor />
</div>
<ChakraProvider>
<div
css={css`
display: flex;
`}
>
<Editor />
</div>
</ChakraProvider>
</StrictMode>,
document.getElementById('root')
);

View File

@ -41,8 +41,16 @@ function genComponent(
export class OperationManager {
private undoStack: Operations[] = [];
private app: Application;
constructor(app: Application) {
const appFromLS = localStorage.getItem('schema');
if (appFromLS) {
this.app = JSON.parse(appFromLS);
} else {
this.app = app;
}
constructor(private app: Application) {
eventBus.on('undo', () => this.undo());
eventBus.on('operation', o => this.apply(o));
}
@ -53,6 +61,7 @@ export class OperationManager {
updateApp(app: Application) {
eventBus.send('appChange', app);
localStorage.setItem('schema', JSON.stringify(app));
this.app = app;
}

View File

@ -18,6 +18,7 @@ export const App: React.FC<AppProps> = props => {
options,
mModules,
componentWrapper,
onLayoutChange,
debugStore = true,
debugEvent = true,
} = props;
@ -26,7 +27,14 @@ export const App: React.FC<AppProps> = props => {
initStateAndMethod(mModules.registry, mModules.stateManager, app.spec.components);
const { topLevelComponents, slotComponentsMap } = useMemo(
() => resolveAppComponents(mModules, app.spec.components, componentWrapper, app),
() =>
resolveAppComponents(
mModules,
app.spec.components,
app,
componentWrapper,
onLayoutChange
),
[app]
);
@ -42,6 +50,7 @@ export const App: React.FC<AppProps> = props => {
targetSlot={null}
app={app}
componentWrapper={componentWrapper}
onLayoutChange={onLayoutChange}
/>
);
})}

View File

@ -12,14 +12,29 @@ export const LayoutPropertySchema = Type.Array(
w: Type.Number(),
h: Type.Number(),
i: Type.String(),
isResizable: Type.Optional(Type.Boolean()),
})
);
const GridLayout: React.FC<{
layout: Static<typeof LayoutPropertySchema>;
}> = ({ children, layout }) => {
onLayoutChange?: (layout: RGL.Layout[]) => void;
}> = ({ children, layout, onLayoutChange }) => {
return (
<ReactGridLayout rowHeight={30} layout={layout}>
<ReactGridLayout
isDraggable={!!onLayoutChange}
isResizable={!!onLayoutChange}
compactType={null}
preventCollision={true}
rowHeight={30}
layout={layout}
onLayoutChange={onLayoutChange}
onDragStart={() => {
console.log('dragstart');
}}
// onDrop={onLayoutChange}
isDroppable={true}
>
{children}
</ReactGridLayout>
);

View File

@ -60,7 +60,6 @@ const List: ComponentImplementation<Static<typeof PropsSchema>> = ({
const { topLevelComponents, slotComponentsMap } = resolveAppComponents(
mModules,
evaledTemplate,
undefined,
app
);

View File

@ -1,4 +1,5 @@
import React, { Suspense } from 'react';
import RGL from 'react-grid-layout';
import { ComponentImplementation } from '../../modules/registry';
import { createComponent } from '@meta-ui/core';
import { getSlots } from '../_internal/Slot';
@ -10,10 +11,17 @@ const BaseGridLayout = React.lazy(() => import('../../components/_internal/GridL
const GridLayout: ComponentImplementation<Static<typeof PropsSchema>> = ({
slotsMap,
layout = [],
onLayoutChange,
component,
}) => {
const _onLayoutChange = (layout: RGL.Layout[]) => {
onLayoutChange && onLayoutChange(component.id, layout);
};
return (
<Suspense fallback={null}>
<BaseGridLayout layout={layout}>{getSlots(slotsMap, 'container')}</BaseGridLayout>
<BaseGridLayout onLayoutChange={_onLayoutChange} layout={layout}>
{getSlots(slotsMap, 'container')}
</BaseGridLayout>
</Suspense>
);
};

View File

@ -22,6 +22,7 @@ type ImplWrapperProps = {
mModules: MetaUIModules;
app?: RuntimeApplication;
componentWrapper?: ComponentWrapperType;
onLayoutChange?: (id: string, layout: any) => void;
};
export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
@ -34,6 +35,7 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
children,
componentWrapper: ComponentWrapper,
mModules,
onLayoutChange,
} = props;
const { registry, stateManager, globalHandlerMap, apiService } = props.mModules;
@ -174,6 +176,7 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
mModules={mModules}
mergeState={mergeState}
subscribeMethods={subscribeMethods}
onLayoutChange={onLayoutChange}
slotsMap={slotsMap}
app={app}
/>

View File

@ -66,6 +66,7 @@ export type ComponentMergedProps = {
effects?: Array<() => void>;
app?: RuntimeApplication;
mModules: MetaUIModules;
onLayoutChange?: (id: string, layout: any) => void;
};
export type ComponentImplementation<T = any> = React.FC<T & ComponentMergedProps>;

View File

@ -12,8 +12,9 @@ import { ImplWrapper } from './ImplWrapper';
export function resolveAppComponents(
mModules: MetaUIModules,
components: RuntimeApplication['spec']['components'],
app?: RuntimeApplication,
componentWrapper?: ComponentWrapperType,
app?: RuntimeApplication
onLayoutChange?: (id: string, layout: any) => void
): {
topLevelComponents: RuntimeApplication['spec']['components'];
slotComponentsMap: SlotComponentMap;
@ -44,6 +45,7 @@ export function resolveAppComponents(
mModules={mModules}
app={app}
componentWrapper={componentWrapper}
onLayoutChange={onLayoutChange}
{...props}
ref={ref}
/>

View File

@ -16,6 +16,7 @@ export type MetaUIModules = {
export type AppProps = {
options: Application;
mModules: MetaUIModules;
onLayoutChange?: (id: string, layout: any) => void;
componentWrapper?: ComponentWrapperType;
debugStore?: boolean;
debugEvent?: boolean;