mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-02-17 17:40:31 +08:00
Merge pull request #165 from webzard-io/feat/drag-to-reorder
drag component into view & move components across level
This commit is contained in:
commit
736ef98bd0
@ -1,4 +1,4 @@
|
||||
import { observable, makeObservable, action } from 'mobx';
|
||||
import { observable, makeObservable, action, toJS } from 'mobx';
|
||||
import { Application, ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { ImplementedRuntimeModule } from '@sunmao-ui/runtime';
|
||||
import { produce } from 'immer';
|
||||
@ -65,7 +65,7 @@ export class AppStorage {
|
||||
) {
|
||||
switch (type) {
|
||||
case 'app':
|
||||
const newApp = produce(this.app, draft => {
|
||||
const newApp = produce(toJS(this.app), draft => {
|
||||
draft.spec.components = components;
|
||||
});
|
||||
this.setApp(newApp);
|
||||
@ -75,7 +75,7 @@ export class AppStorage {
|
||||
const i = this.modules.findIndex(
|
||||
m => m.version === version && m.metadata.name === name
|
||||
);
|
||||
const newModules = produce(this.modules, draft => {
|
||||
const newModules = produce(toJS(this.modules), draft => {
|
||||
draft[i].impl = components;
|
||||
});
|
||||
this.setModules(newModules);
|
||||
@ -85,7 +85,7 @@ export class AppStorage {
|
||||
}
|
||||
|
||||
saveAppMetaDataInLS({ version, name }: { version: string; name: string }) {
|
||||
const newApp = produce(this.app, draft => {
|
||||
const newApp = produce(toJS(this.app), draft => {
|
||||
draft.metadata.name = name;
|
||||
draft.version = version;
|
||||
});
|
||||
@ -108,7 +108,7 @@ export class AppStorage {
|
||||
const i = this.modules.findIndex(
|
||||
m => m.version === originVersion && m.metadata.name === originName
|
||||
);
|
||||
const newModules = produce(this.modules, draft => {
|
||||
const newModules = produce(toJS(this.modules), draft => {
|
||||
draft[i].metadata.name = name;
|
||||
draft[i].spec.stateMap = stateMap;
|
||||
draft[i].version = version;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeAutoObservable, observable, reaction, action } from 'mobx';
|
||||
import { action, makeAutoObservable, observable, reaction } from 'mobx';
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { eventBus } from './eventBus';
|
||||
import { AppStorage } from './AppStorage';
|
||||
@ -15,6 +15,7 @@ class EditorStore {
|
||||
// currentEditingComponents, it could be app's or module's components
|
||||
selectedComponentId = '';
|
||||
hoverComponentId = '';
|
||||
dragIdStack: string[] = [];
|
||||
// current editor editing target(app or module)
|
||||
currentEditingTarget: EditingTarget = {
|
||||
kind: 'app',
|
||||
@ -36,10 +37,16 @@ class EditorStore {
|
||||
return this.components.find(c => c.id === this.selectedComponentId);
|
||||
}
|
||||
|
||||
get dragOverComponentId() {
|
||||
return this.dragIdStack[this.dragIdStack.length - 1];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
components: observable.shallow,
|
||||
dragIdStack: observable.shallow,
|
||||
setComponents: action,
|
||||
setDragIdStack: action,
|
||||
});
|
||||
|
||||
eventBus.on('selectComponent', id => {
|
||||
@ -56,13 +63,16 @@ class EditorStore {
|
||||
);
|
||||
});
|
||||
|
||||
reaction(() => this.currentEditingTarget, (target) => {
|
||||
if (target.name) {
|
||||
this.clearSunmaoGlobalState();
|
||||
eventBus.send('componentsRefresh', this.originComponents);
|
||||
this.setComponents(this.originComponents);
|
||||
reaction(
|
||||
() => this.currentEditingTarget,
|
||||
target => {
|
||||
if (target.name) {
|
||||
this.clearSunmaoGlobalState();
|
||||
eventBus.send('componentsRefresh', this.originComponents);
|
||||
this.setComponents(this.originComponents);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.updateCurrentEditingTarget('app', this.app.version, this.app.metadata.name);
|
||||
}
|
||||
@ -109,6 +119,18 @@ class EditorStore {
|
||||
setComponents = (val: ApplicationComponent[]) => {
|
||||
this.components = val;
|
||||
};
|
||||
pushDragIdStack = (val: string) => {
|
||||
this.setDragIdStack([...this.dragIdStack, val]);
|
||||
};
|
||||
popDragIdStack = () => {
|
||||
this.setDragIdStack(this.dragIdStack.slice(0, this.dragIdStack.length - 1));
|
||||
};
|
||||
clearIdStack = () => {
|
||||
this.setDragIdStack([]);
|
||||
};
|
||||
setDragIdStack = (ids: string[]) => {
|
||||
this.dragIdStack = ids;
|
||||
};
|
||||
}
|
||||
|
||||
export const editorStore = new EditorStore();
|
||||
|
@ -7,6 +7,7 @@ import 'codemirror/addon/fold/brace-fold';
|
||||
import 'codemirror/addon/fold/foldgutter';
|
||||
import 'codemirror/addon/fold/foldgutter.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
|
||||
export const StateEditor: React.FC<{ code: string }> = ({ code }) => {
|
||||
const style = css`
|
||||
@ -44,5 +45,9 @@ export const StateEditor: React.FC<{ code: string }> = ({ code }) => {
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
return <Box css={style} ref={wrapperEl} height="100%"></Box>;
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Box css={style} ref={wrapperEl} height="100%"></Box>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ import SchemaField from './JsonSchemaForm/SchemaField';
|
||||
import { genOperation } from '../../operations';
|
||||
import { editorStore } from '../../EditorStore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { StyleTraitForm } from './StyleTraitForm';
|
||||
|
||||
type Props = {
|
||||
@ -98,50 +99,57 @@ export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
);
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
// prevent form keyboard events to accidentally trigger operation shortcut
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack p="2" spacing="2" background="gray.50">
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
<strong>Component ID</strong>
|
||||
</FormLabel>
|
||||
<Input
|
||||
key={selectedComponent.id}
|
||||
defaultValue={selectedComponent.id}
|
||||
background="white"
|
||||
onBlur={e => changeComponentId(selectedComponent?.id, e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<VStack width="full" alignItems="start">
|
||||
<strong>Properties</strong>
|
||||
<VStack
|
||||
width="full"
|
||||
padding="2"
|
||||
background="white"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
borderRadius="4"
|
||||
>
|
||||
<SchemaField
|
||||
schema={cImpl.spec.properties}
|
||||
label=""
|
||||
formData={properties}
|
||||
onChange={newFormData => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyComponentProperty', {
|
||||
componentId: selectedComponentId,
|
||||
properties: newFormData,
|
||||
})
|
||||
);
|
||||
}}
|
||||
registry={registry}
|
||||
<ErrorBoundary>
|
||||
<VStack p="2" spacing="2" background="gray.50" onKeyDown={onKeyDown}>
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
<strong>Component ID</strong>
|
||||
</FormLabel>
|
||||
<Input
|
||||
key={selectedComponent.id}
|
||||
defaultValue={selectedComponent.id}
|
||||
background="white"
|
||||
onBlur={e => changeComponentId(selectedComponent?.id, e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<VStack width="full" alignItems="start">
|
||||
<strong>Properties</strong>
|
||||
<VStack
|
||||
width="full"
|
||||
padding="2"
|
||||
background="white"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
borderRadius="4"
|
||||
>
|
||||
<SchemaField
|
||||
schema={cImpl.spec.properties}
|
||||
label=""
|
||||
formData={properties}
|
||||
onChange={newFormData => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('modifyComponentProperty', {
|
||||
componentId: selectedComponentId,
|
||||
properties: newFormData,
|
||||
})
|
||||
);
|
||||
}}
|
||||
registry={registry}
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
<EventTraitForm component={selectedComponent} registry={registry} />
|
||||
<FetchTraitForm component={selectedComponent} registry={registry} />
|
||||
<StyleTraitForm component={selectedComponent} registry={registry} />
|
||||
<GeneralTraitFormList component={selectedComponent} registry={registry} />
|
||||
</VStack>
|
||||
<EventTraitForm component={selectedComponent} registry={registry} />
|
||||
<FetchTraitForm component={selectedComponent} registry={registry} />
|
||||
<StyleTraitForm component={selectedComponent} registry={registry} />
|
||||
<GeneralTraitFormList component={selectedComponent} registry={registry} />
|
||||
</VStack>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
@ -24,7 +24,6 @@ export const StyleTraitForm: React.FC<Props> = props => {
|
||||
if (!styleSlots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack width="full">
|
||||
<Box fontWeight="bold" textAlign="left" width="100%">
|
||||
|
@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { ComponentWrapperType } from '@sunmao-ui/runtime';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { editorStore } from '../EditorStore';
|
||||
import { registry } from '../setup';
|
||||
import { eventBus } from '../eventBus';
|
||||
import { genOperation } from '../operations';
|
||||
import { Text } from '@chakra-ui/react';
|
||||
|
||||
// children of components in this list should render height as 100%
|
||||
const fullHeightList = ['core/v1/grid_layout'];
|
||||
@ -15,32 +19,38 @@ export const ComponentWrapper: ComponentWrapperType = observer(props => {
|
||||
setSelectedComponentId,
|
||||
hoverComponentId,
|
||||
setHoverComponentId,
|
||||
dragOverComponentId,
|
||||
pushDragIdStack,
|
||||
popDragIdStack,
|
||||
clearIdStack,
|
||||
} = editorStore;
|
||||
|
||||
const isHover = hoverComponentId === component.id;
|
||||
const isSelected = selectedComponentId === component.id;
|
||||
const slots = useMemo(() => {
|
||||
return registry.getComponentByType(component.type).spec.slots;
|
||||
}, [component.type]);
|
||||
|
||||
let borderColor = 'transparent';
|
||||
if (isSelected) {
|
||||
borderColor = 'red';
|
||||
} else if (isHover) {
|
||||
borderColor = 'black';
|
||||
}
|
||||
const style = css`
|
||||
display: ${inlineList.includes(component.type) ? 'inline-block' : 'block'};
|
||||
height: ${fullHeightList.includes(parentType) ? '100%' : 'auto'};
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
border: 1px solid ${borderColor};
|
||||
pointer-events: none;
|
||||
const isDroppable = slots.length > 0;
|
||||
|
||||
const borderColor = useMemo(() => {
|
||||
if (dragOverComponentId === component.id) {
|
||||
return 'orange';
|
||||
} else if (selectedComponentId === component.id) {
|
||||
return 'red';
|
||||
} else if (hoverComponentId === component.id) {
|
||||
return 'black';
|
||||
} else {
|
||||
return 'transparent';
|
||||
}
|
||||
`;
|
||||
}, [dragOverComponentId, selectedComponentId, hoverComponentId, component.id]);
|
||||
|
||||
const style = useMemo(() => {
|
||||
return css`
|
||||
display: ${inlineList.includes(component.type) ? 'inline-block' : 'block'};
|
||||
height: ${fullHeightList.includes(parentType) ? '100%' : 'auto'};
|
||||
position: relative;
|
||||
`;
|
||||
}, [component.type, parentType]);
|
||||
|
||||
const onClickWrapper = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedComponentId(component.id);
|
||||
@ -53,14 +63,79 @@ export const ComponentWrapper: ComponentWrapperType = observer(props => {
|
||||
e.stopPropagation();
|
||||
setHoverComponentId('');
|
||||
};
|
||||
|
||||
const onDragEnter = () => {
|
||||
if (isDroppable) {
|
||||
pushDragIdStack(component.id);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (e: React.DragEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isDroppable) return;
|
||||
clearIdStack();
|
||||
const creatingComponent = e.dataTransfer?.getData('component') || '';
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
componentType: creatingComponent,
|
||||
parentId: component.id,
|
||||
slot: slots[0],
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
if (isDroppable) {
|
||||
popDragIdStack();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onClick={onClickWrapper}
|
||||
onMouseEnter={onMouseEnterWrapper}
|
||||
onMouseLeave={onMouseLeaveWrapper}
|
||||
css={style}
|
||||
>
|
||||
{props.children}
|
||||
{borderColor === 'transparent' ? undefined : (
|
||||
<OutlineMask color={borderColor} text={component.id} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const outlineMaskStyle = css`
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
border: 1px solid;
|
||||
pointer-events: none;
|
||||
`;
|
||||
const outlineMaskTextStyle = css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translateY(-100%);
|
||||
padding: 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: black;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
function OutlineMask({ color, text }: { color: string; text: string }) {
|
||||
return (
|
||||
<div css={outlineMaskStyle} style={{ borderColor: color }}>
|
||||
<Text css={outlineMaskTextStyle} style={{ background: color }}>
|
||||
{text}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { Explorer } from './Explorer';
|
||||
import { editorStore } from '../EditorStore';
|
||||
import { genOperation } from '../operations';
|
||||
import { ComponentForm } from './ComponentForm';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
type ReturnOfInit = ReturnType<typeof initSunmaoUI>;
|
||||
|
||||
@ -75,13 +76,15 @@ export const Editor: React.FC<Props> = observer(({ App, registry, stateStore })
|
||||
|
||||
const appComponent = useMemo(() => {
|
||||
return (
|
||||
<App
|
||||
options={app}
|
||||
debugEvent={false}
|
||||
debugStore={false}
|
||||
gridCallbacks={gridCallbacks}
|
||||
componentWrapper={ComponentWrapper}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<App
|
||||
options={app}
|
||||
debugEvent={false}
|
||||
debugStore={false}
|
||||
gridCallbacks={gridCallbacks}
|
||||
componentWrapper={ComponentWrapper}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}, [app, gridCallbacks]);
|
||||
|
||||
@ -179,7 +182,10 @@ export const Editor: React.FC<Props> = observer(({ App, registry, stateStore })
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardEventWrapper components={components} selectedComponentId={selectedComponentId}>
|
||||
<KeyboardEventWrapper
|
||||
components={components}
|
||||
selectedComponentId={selectedComponentId}
|
||||
>
|
||||
<Box display="flex" height="100%" width="100%" flexDirection="column">
|
||||
<EditorHeader
|
||||
scale={scale}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { ExplorerForm } from './ExplorerForm/ExplorerForm';
|
||||
import { ExplorerTree } from './ExplorerTree';
|
||||
|
||||
@ -18,13 +19,15 @@ export const Explorer: React.FC = () => {
|
||||
};
|
||||
if (isEditingMode) {
|
||||
return (
|
||||
<ExplorerForm
|
||||
formType={formType}
|
||||
version={currentVersion}
|
||||
name={currentName}
|
||||
onBack={onBack}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<ExplorerForm
|
||||
formType={formType}
|
||||
version={currentVersion}
|
||||
name={currentName}
|
||||
onBack={onBack}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
return <ExplorerTree onEdit={onEdit} />;
|
||||
return <ErrorBoundary><ExplorerTree onEdit={onEdit} /></ErrorBoundary>;
|
||||
};
|
||||
|
@ -31,9 +31,9 @@ export const ExplorerForm: React.FC<Props> = observer(
|
||||
const moduleMetaData = {
|
||||
name,
|
||||
version,
|
||||
stateMap: moduleSpec.spec.stateMap,
|
||||
stateMap: moduleSpec?.spec.stateMap || {},
|
||||
};
|
||||
form = <ModuleMetaDataForm data={moduleMetaData} />;
|
||||
form = <ModuleMetaDataForm initData={moduleMetaData} />;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
|
@ -12,19 +12,19 @@ type ModuleMetaDataFormData = {
|
||||
};
|
||||
|
||||
type ModuleMetaDataFormProps = {
|
||||
data: ModuleMetaDataFormData;
|
||||
initData: ModuleMetaDataFormData;
|
||||
};
|
||||
|
||||
export const ModuleMetaDataForm: React.FC<ModuleMetaDataFormProps> = observer(
|
||||
({ data }) => {
|
||||
({ initData }) => {
|
||||
const onSubmit = (value: ModuleMetaDataFormData) => {
|
||||
editorStore.appStorage.saveModuleMetaDataInLS(
|
||||
{ originName: data.name, originVersion: data.version },
|
||||
{ originName: initData.name, originVersion: initData.version },
|
||||
value
|
||||
);
|
||||
};
|
||||
const formik = useFormik({
|
||||
initialValues: data,
|
||||
initialValues: initData,
|
||||
onSubmit,
|
||||
});
|
||||
return (
|
||||
|
@ -24,8 +24,6 @@ export const KeyboardEventWrapper: React.FC<Props> = ({
|
||||
`;
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.target instanceof Element && e.target.id !== 'keyboard-event-wrapper')
|
||||
return false;
|
||||
switch (e.key) {
|
||||
case 'Delete':
|
||||
case 'Backspace':
|
||||
|
@ -9,6 +9,7 @@ import { Box, HStack, IconButton, Text } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
title: string;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
@ -24,6 +25,7 @@ type Props = {
|
||||
|
||||
export const ComponentItemView: React.FC<Props> = props => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
isSelected,
|
||||
noChevron,
|
||||
@ -59,13 +61,21 @@ export const ComponentItemView: React.FC<Props> = props => {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = (e: React.DragEvent) => {
|
||||
e.dataTransfer.setData('moveComponent', id);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
width="full"
|
||||
onDragStart={onDragStart}
|
||||
// onDragEnd={onDragEnd}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={() => setIsDragOver(false)}
|
||||
background={isDragOver ? 'gray.100' : undefined}
|
||||
draggable
|
||||
>
|
||||
{noChevron ? null : expandIcon}
|
||||
<HStack width="full" justify="space-between">
|
||||
|
@ -50,7 +50,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
const onDrop = (creatingComponent: string) => {
|
||||
const onCreateComponent = (creatingComponent: string) => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
@ -60,8 +60,21 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onMoveComponent = (movingComponent: string) => {
|
||||
if (movingComponent === component.id) return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('moveComponent', {
|
||||
fromId: movingComponent,
|
||||
toId: component.id,
|
||||
slot,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const slotName = (
|
||||
<DropComponentWrapper onDrop={onDrop}>
|
||||
<DropComponentWrapper onCreateComponent={onCreateComponent} onMoveComponent={onMoveComponent}>
|
||||
<Text color="gray.500" fontWeight="medium">
|
||||
Slot: {slot}
|
||||
</Text>
|
||||
@ -90,7 +103,7 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const onDrop = (creatingComponent: string) => {
|
||||
const onCreateComponent = (creatingComponent: string) => {
|
||||
if (slots.length === 0) return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
@ -122,6 +135,17 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const onMoveComponent = (movingComponent: string) => {
|
||||
if (movingComponent === component.id) return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('moveComponent', {
|
||||
fromId: movingComponent,
|
||||
toId: component.id,
|
||||
slot: slots[0],
|
||||
})
|
||||
);
|
||||
};
|
||||
return (
|
||||
<VStack
|
||||
key={component.id}
|
||||
@ -130,8 +154,9 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
width="full"
|
||||
alignItems="start"
|
||||
>
|
||||
<DropComponentWrapper onDrop={onDrop}>
|
||||
<DropComponentWrapper onCreateComponent={onCreateComponent} onMoveComponent={onMoveComponent}>
|
||||
<ComponentItemView
|
||||
id={component.id}
|
||||
title={component.id}
|
||||
isSelected={component.id === selectedComponentId}
|
||||
onClick={() => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
type Props = {
|
||||
onDrop: (componentType: string) => void;
|
||||
onCreateComponent: (componentType: string) => void;
|
||||
onMoveComponent: (from: string) => void;
|
||||
};
|
||||
|
||||
export const DropComponentWrapper: React.FC<Props> = props => {
|
||||
@ -14,8 +15,15 @@ export const DropComponentWrapper: React.FC<Props> = props => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const creatingComponent = e.dataTransfer?.getData('component') || '';
|
||||
const movingComponent = e.dataTransfer?.getData('moveComponent') || '';
|
||||
|
||||
props.onDrop(creatingComponent);
|
||||
if (movingComponent) {
|
||||
props.onMoveComponent(movingComponent)
|
||||
}
|
||||
|
||||
if (creatingComponent) {
|
||||
props.onCreateComponent(creatingComponent);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box width="full" onDrop={onDrop} onDragOver={onDragOver}>
|
||||
|
@ -8,6 +8,7 @@ import { DropComponentWrapper } from './DropComponentWrapper';
|
||||
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
|
||||
import { genOperation as genOperation } from '../../operations';
|
||||
import { resolveApplicationComponents } from '../../utils/resolveApplicationComponents';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
|
||||
export type ChildrenMap = Map<string, SlotsMap>;
|
||||
type SlotsMap = Map<string, ApplicationComponent[]>;
|
||||
@ -22,8 +23,21 @@ type Props = {
|
||||
export const StructureTree: React.FC<Props> = props => {
|
||||
const { components, selectedComponentId, onSelectComponent, registry } = props;
|
||||
|
||||
const [realComponents, dataSources] = useMemo(() => {
|
||||
const _realComponent: ApplicationComponent[] = [];
|
||||
const _datasources: ApplicationComponent[] = [];
|
||||
components.forEach(c => {
|
||||
if (c.type === 'core/v1/dummy') {
|
||||
_datasources.push(c);
|
||||
} else {
|
||||
_realComponent.push(c);
|
||||
}
|
||||
});
|
||||
return [_realComponent, _datasources];
|
||||
}, [components]);
|
||||
|
||||
const componentEles = useMemo(() => {
|
||||
const { topLevelComponents, childrenMap } = resolveApplicationComponents(components)
|
||||
const { topLevelComponents, childrenMap } = resolveApplicationComponents(realComponents);
|
||||
|
||||
return topLevelComponents.map(c => (
|
||||
<ComponentTree
|
||||
@ -35,10 +49,9 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
registry={registry}
|
||||
/>
|
||||
));
|
||||
}, [components, selectedComponentId, onSelectComponent, registry]);
|
||||
}, [realComponents, selectedComponentId, onSelectComponent, registry]);
|
||||
|
||||
const dataSourceEles = useMemo(() => {
|
||||
const dataSources = components.filter(c => c.type === 'core/v1/dummy');
|
||||
return dataSources.map(dummy => {
|
||||
const onClickRemove = () => {
|
||||
eventBus.send(
|
||||
@ -50,6 +63,7 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
};
|
||||
return (
|
||||
<ComponentItemView
|
||||
id={dummy.id}
|
||||
key={dummy.id}
|
||||
title={dummy.id}
|
||||
isSelected={dummy.id === selectedComponentId}
|
||||
@ -61,7 +75,7 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [components, selectedComponentId, onSelectComponent, registry]);
|
||||
}, [dataSources, selectedComponentId, onSelectComponent, registry]);
|
||||
|
||||
return (
|
||||
<VStack spacing="2" padding="5" alignItems="start">
|
||||
@ -79,7 +93,7 @@ export const StructureTree: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
function RootItem() {
|
||||
const onDrop = (creatingComponent: string) => {
|
||||
const onCreateComponent = (creatingComponent: string) => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('createComponent', {
|
||||
@ -87,17 +101,35 @@ function RootItem() {
|
||||
})
|
||||
);
|
||||
};
|
||||
const onMoveComponent = (movingComponent: string) => {
|
||||
if (movingComponent === 'root') return;
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('moveComponent', {
|
||||
fromId: movingComponent,
|
||||
toId: '__root__',
|
||||
slot: '__root__',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width="full">
|
||||
<DropComponentWrapper onDrop={onDrop}>
|
||||
<ComponentItemView
|
||||
title="Root"
|
||||
isSelected={false}
|
||||
onClick={() => undefined}
|
||||
isDroppable={true}
|
||||
noChevron={true}
|
||||
/>
|
||||
</DropComponentWrapper>
|
||||
</Box>
|
||||
<ErrorBoundary>
|
||||
<Box width="full">
|
||||
<DropComponentWrapper
|
||||
onCreateComponent={onCreateComponent}
|
||||
onMoveComponent={onMoveComponent}
|
||||
>
|
||||
<ComponentItemView
|
||||
id={'root'}
|
||||
title="Root"
|
||||
isSelected={false}
|
||||
onClick={() => undefined}
|
||||
isDroppable={true}
|
||||
noChevron={true}
|
||||
/>
|
||||
</DropComponentWrapper>
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './createComponentBranchOperation';
|
||||
export * from './modifyComponentIdBranchOperation';
|
||||
export * from './removeComponentBranchOperation';
|
||||
export * from './moveComponentBranchOperation';
|
||||
|
@ -0,0 +1,53 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { BaseBranchOperation } from '../type';
|
||||
import { CreateTraitLeafOperation, ModifyTraitPropertiesLeafOperation, RemoveTraitLeafOperation } from '../leaf';
|
||||
|
||||
export type MoveComponentBranchOperationContext = {
|
||||
fromId: string;
|
||||
toId: string;
|
||||
slot: string;
|
||||
};
|
||||
|
||||
export class MoveComponentBranchOperation extends BaseBranchOperation<MoveComponentBranchOperationContext> {
|
||||
do(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
const from = prev.find(c => c.id === this.context.fromId);
|
||||
if (!from) return prev;
|
||||
|
||||
const traitIndex = from.traits.findIndex(t => t.type === 'core/v1/slot');
|
||||
|
||||
if (this.context.toId === '__root__') {
|
||||
this.operationStack.insert(
|
||||
new RemoveTraitLeafOperation({
|
||||
componentId: this.context.fromId,
|
||||
index: traitIndex,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const newSlotProperties = {
|
||||
container: { id: this.context.toId, slot: this.context.slot },
|
||||
};
|
||||
if (traitIndex > -1) {
|
||||
this.operationStack.insert(
|
||||
new ModifyTraitPropertiesLeafOperation({
|
||||
componentId: this.context.fromId,
|
||||
traitIndex,
|
||||
properties: newSlotProperties,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.operationStack.insert(
|
||||
new CreateTraitLeafOperation({
|
||||
componentId: this.context.fromId,
|
||||
traitType: 'core/v1/slot',
|
||||
properties: newSlotProperties,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.operationStack.reduce((prev, node) => {
|
||||
prev = node.do(prev);
|
||||
return prev;
|
||||
}, prev);
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ import {
|
||||
ModifyComponentIdBranchOperationContext,
|
||||
RemoveComponentBranchOperation,
|
||||
RemoveComponentBranchOperationContext,
|
||||
MoveComponentBranchOperation,
|
||||
MoveComponentBranchOperationContext,
|
||||
} from './branch';
|
||||
import {
|
||||
AdjustComponentOrderLeafOperation,
|
||||
@ -38,6 +40,7 @@ const OperationConstructors: Record<
|
||||
modifyTraitProperty: ModifyTraitPropertiesLeafOperation,
|
||||
replaceApp: ReplaceAppLeafOperation,
|
||||
pasteComponent: PasteComponentLeafOperation,
|
||||
moveComponent: MoveComponentBranchOperation,
|
||||
};
|
||||
|
||||
type OperationTypes = keyof OperationConfigMaps;
|
||||
@ -86,6 +89,10 @@ type OperationConfigMaps = {
|
||||
PasteComponentLeafOperation,
|
||||
PasteComponentLeafOperationContext
|
||||
>;
|
||||
moveComponent: OperationConfigMap<
|
||||
MoveComponentBranchOperation,
|
||||
MoveComponentBranchOperationContext
|
||||
>;
|
||||
};
|
||||
|
||||
export const genOperation = <T extends OperationTypes>(
|
||||
|
@ -29,8 +29,10 @@ const GridLayout: React.FC<RGL.ReactGridLayoutProps> = props => {
|
||||
const key = (e as React.DragEvent).dataTransfer.types
|
||||
.map(decodeDragDataTransfer)
|
||||
.find(t => t.startsWith(DROP_EXAMPLE_SIZE_PREFIX));
|
||||
const componentSize = JSON.parse(key?.replace(DROP_EXAMPLE_SIZE_PREFIX, '') || '');
|
||||
return { w: componentSize[0], h: componentSize[1] };
|
||||
if (key) {
|
||||
const componentSize = JSON.parse(key?.replace(DROP_EXAMPLE_SIZE_PREFIX, '') || '');
|
||||
return { w: componentSize[0], h: componentSize[1] };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import { SlotsMap } from '../../types/RuntimeSchema';
|
||||
|
||||
export function getSlots<T>(slotsMap: SlotsMap | undefined, slot: string, rest: T) {
|
||||
return (slotsMap?.get(slot) || []).map(({ component: ImplWrapper, id }) => (
|
||||
export function getSlots<T>(slotsMap: SlotsMap | undefined, slot: string, rest: T): React.ReactElement[] {
|
||||
const components = slotsMap?.get(slot);
|
||||
if (!components) {
|
||||
const placeholder = <div key='slot-placeholder' style={{color: 'gray'}}>Slot {slot} is empty.Please drag component to this slot.</div>;
|
||||
return [placeholder]
|
||||
}
|
||||
return components.map(({ component: ImplWrapper, id }) => (
|
||||
<ImplWrapper key={id} {...rest} />
|
||||
));
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ export default {
|
||||
properties: PropsSchema,
|
||||
state: StateSchema,
|
||||
methods: [],
|
||||
slots: ['content'],
|
||||
slots: [],
|
||||
styleSlots: ['content'],
|
||||
events: ['onLoad', 'onError'],
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { createComponent } from '@sunmao-ui/core';
|
||||
import { Tabs as BaseTabs, TabList, Tab, TabPanels, TabPanel } from '@chakra-ui/react';
|
||||
import { Tabs as BaseTabs, TabList, Tab, TabPanels, TabPanel, Text } from '@chakra-ui/react';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { ComponentImplementation } from '../../services/registry';
|
||||
import { getSlots } from '../_internal/Slot';
|
||||
@ -18,6 +18,9 @@ const Tabs: ComponentImplementation<Static<typeof PropsSchema>> = ({
|
||||
useEffect(() => {
|
||||
mergeState({ selectedTabIndex });
|
||||
}, [selectedTabIndex]);
|
||||
|
||||
const slotComponents = getSlots(slotsMap, 'content', {})
|
||||
const placeholder = <Text color='gray'>Slot content is empty.Please drag component to this slot.</Text>
|
||||
return (
|
||||
<BaseTabs
|
||||
defaultIndex={initialSelectedTabIndex}
|
||||
@ -36,7 +39,7 @@ const Tabs: ComponentImplementation<Static<typeof PropsSchema>> = ({
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{getSlots(slotsMap, 'content', {}).map((content, idx) => {
|
||||
{tabNames.map((_, idx) => {
|
||||
return (
|
||||
<TabPanel
|
||||
key={idx}
|
||||
@ -44,7 +47,7 @@ const Tabs: ComponentImplementation<Static<typeof PropsSchema>> = ({
|
||||
${customStyle?.tabContent}
|
||||
`}
|
||||
>
|
||||
{content}
|
||||
{slotComponents[idx] || placeholder}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
|
Loading…
Reference in New Issue
Block a user