Merge pull request #165 from webzard-io/feat/drag-to-reorder

drag component into view & move components across level
This commit is contained in:
yz-yu 2021-12-20 11:31:05 +08:00 committed by GitHub
commit 736ef98bd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 393 additions and 131 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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%">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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':

View File

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

View File

@ -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={() => {

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export * from './createComponentBranchOperation';
export * from './modifyComponentIdBranchOperation';
export * from './removeComponentBranchOperation';
export * from './moveComponentBranchOperation';

View File

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

View File

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

View File

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

View File

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

View File

@ -150,7 +150,7 @@ export default {
properties: PropsSchema,
state: StateSchema,
methods: [],
slots: ['content'],
slots: [],
styleSlots: ['content'],
events: ['onLoad', 'onError'],
},

View File

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