Merge pull request #401 from webzard-io/feat/windlike-patch

feat: improve the editor performance
This commit is contained in:
tanbowensg 2022-05-18 16:45:00 +08:00 committed by GitHub
commit 7fe0cb8343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 304 additions and 206 deletions

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo, useCallback } from 'react';
import { css } from '@emotion/css';
import { IconButton, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
@ -27,13 +27,74 @@ const TableRowStyle = css`
}
`;
type Props = WidgetProps & {
type ArrayTableProps = WidgetProps & {
itemSpec: JSONSchema7;
};
type RowProps = ArrayTableProps & {
itemValue: any;
itemIndex: number;
};
export const ArrayTable: React.FC<Props> = props => {
const { value, itemSpec, spec, level, path, children, onChange } = props;
const DEFAULT_KEYS = ['index'];
const TableRow: React.FC<RowProps> = props => {
const { value, itemSpec, spec, level, path, children, itemValue, itemIndex, onChange } =
props;
const { expressionOptions, displayedKeys = [] } = spec.widgetOptions || {};
const keys = displayedKeys.length ? displayedKeys : DEFAULT_KEYS;
const mergedSpec = useMemo(
() =>
mergeWidgetOptionsIntoSpec(
{
...itemSpec,
title: '',
},
{
expressionOptions,
}
),
[itemSpec, expressionOptions]
);
const nextPath = useMemo(() => path.concat(String(itemIndex)), [path, itemIndex]);
const onPopoverWidgetChange = useCallback(
(newItemValue: any) => {
const newValue = [...value];
newValue[itemIndex] = newItemValue;
onChange(newValue);
},
[itemIndex, onChange, value]
);
return (
<Tr className={TableRowStyle}>
<Td key="setting">
<PopoverWidget
{...props}
value={itemValue}
spec={mergedSpec}
path={nextPath}
level={level + 1}
onChange={onPopoverWidgetChange}
>
{typeof children === 'function' ? children(props, itemValue, itemIndex) : null}
</PopoverWidget>
</Td>
{keys.map((key: string) => {
const propertyValue =
key === 'index' ? itemValue[key] ?? itemIndex : itemValue[key];
return <Td key={key}>{propertyValue}</Td>;
})}
<Td key="button">
<ArrayButtonGroup index={itemIndex} value={value} onChange={onChange} />
</Td>
</Tr>
);
};
export const ArrayTable: React.FC<ArrayTableProps> = props => {
const { value, itemSpec, spec, onChange } = props;
const { displayedKeys = [] } = spec.widgetOptions || {};
const keys = displayedKeys.length ? displayedKeys : ['index'];
return (
@ -63,43 +124,12 @@ export const ArrayTable: React.FC<Props> = props => {
</Thead>
<Tbody>
{value.map((itemValue: any, itemIndex: number) => (
<Tr key={itemIndex} className={TableRowStyle}>
<Td key="setting">
<PopoverWidget
{...props}
value={itemValue}
spec={mergeWidgetOptionsIntoSpec(
{
...itemSpec,
title: '',
},
{
expressionOptions,
}
)}
path={path.concat(String(itemIndex))}
level={level + 1}
onChange={(newItemValue: any) => {
const newValue = [...value];
newValue[itemIndex] = newItemValue;
onChange(newValue);
}}
>
{typeof children === 'function'
? children(props, itemValue, itemIndex)
: null}
</PopoverWidget>
</Td>
{keys.map((key: string) => {
const propertyValue =
key === 'index' ? itemValue[key] ?? itemIndex : itemValue[key];
return <Td key={key}>{propertyValue}</Td>;
})}
<Td key="button">
<ArrayButtonGroup index={itemIndex} value={value} onChange={onChange} />
</Td>
</Tr>
<TableRow
{...props}
key={itemIndex}
itemValue={itemValue}
itemIndex={itemIndex}
/>
))}
</Tbody>
</Table>

View File

@ -93,6 +93,10 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
return params;
}, [formik.values.method.name]);
const parametersPath = useMemo(()=> path.concat('method', 'parameters'), [path]);
const parametersSpec = useMemo(()=> mergeWidgetOptionsIntoSpec(paramsSpec, { onlySetValue: true }), [paramsSpec]);
const disabledPath = useMemo(()=> path.concat('disabled'), [path]);
const disabledSpec = useMemo(()=> Type.Boolean({ widgetOptions: { isShowAsideExpressionButton: true } }), []);
const updateMethods = useCallback(
(componentId: string) => {
@ -127,18 +131,29 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
}
}, [value, updateMethods]);
const onTargetComponentChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const onTargetComponentChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
updateMethods(e.target.value);
formik.handleChange(e);
formik.setFieldValue('method', { name: '', parameters: {} });
};
}, [updateMethods, formik]);
const onSubmit = useCallback(() => {
formik.submitForm();
}, [formik]);
const onParametersChange = useCallback(json => {
formik.setFieldValue('method.parameters', json);
formik.submitForm();
}, [formik]);
const onDisabledChange = useCallback(value => {
formik.setFieldValue('disabled', value);
formik.submitForm();
}, [formik]);
const typeField = (
<FormControl>
<FormLabel>Event Type</FormLabel>
<Select
name="type"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
placeholder="Select Event Type"
value={formik.values.type}
@ -156,7 +171,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Target Component</FormLabel>
<Select
name="componentId"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={onTargetComponentChange}
placeholder="Select Target Component"
value={formik.values.componentId}
@ -174,7 +189,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Method</FormLabel>
<Select
name="method.name"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
placeholder="Select Method"
value={formik.values.method.name}
@ -193,15 +208,12 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Parameters</FormLabel>
<RecordWidget
component={component}
path={path.concat('method', 'parameters')}
path={parametersPath}
level={level + 1}
spec={mergeWidgetOptionsIntoSpec(paramsSpec, { onlySetValue: true })}
spec={parametersSpec}
services={services}
value={formik.values.method.parameters}
onChange={json => {
formik.setFieldValue('method.parameters', json);
formik.submitForm();
}}
onChange={onParametersChange}
/>
</FormControl>
);
@ -211,7 +223,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Wait Type</FormLabel>
<Select
name="wait.type"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
value={formik.values.wait?.type}
>
@ -227,7 +239,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Wait Time</FormLabel>
<Input
name="wait.time"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
value={formik.values.wait?.time}
/>
@ -239,14 +251,11 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Disabled</FormLabel>
<SpecWidget
{...props}
spec={Type.Boolean({ widgetOptions: { isShowAsideExpressionButton: true } })}
spec={disabledSpec}
level={level + 1}
path={['disabled']}
path={disabledPath}
value={formik.values.disabled}
onChange={value => {
formik.setFieldValue('disabled', value);
formik.submitForm();
}}
onChange={onDisabledChange}
/>
</FormControl>
);

View File

@ -1,4 +1,10 @@
import React, { useState, useEffect, useCallback, useImperativeHandle } from 'react';
import React, {
useState,
useEffect,
useMemo,
useCallback,
useImperativeHandle,
} from 'react';
import {
Popover,
PopoverTrigger,
@ -38,10 +44,20 @@ export const PopoverWidget = React.forwardRef<
>((props, ref) => {
const { spec, path, children } = props;
const isObjectChildren = children && typeof children === 'object';
const [isInit, setIsInit] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const mergedSpec = useMemo(
() => ({
...spec,
widget:
spec.widget === `${CORE_VERSION}/${CoreWidgetName.Popover}` ? '' : spec.widget,
}),
[spec]
);
const handleOpen = useCallback(() => {
setIsOpen(true);
setIsInit(true);
emitter.emit('other-popover-close', path);
}, [path]);
const handleClickTrigger = useCallback(event => {
@ -132,17 +148,13 @@ export const PopoverWidget = React.forwardRef<
<PopoverContent onClick={handleClickContent}>
<PopoverArrow />
<PopoverBody maxHeight="400px" overflow="auto">
{isObjectChildren && 'body' in children ? (
(children as Children).body
) : (
<SpecWidget
{...props}
spec={{
...spec,
widget: spec.widget === `${CORE_VERSION}/${CoreWidgetName.Popover}` ? '' : spec.widget,
}}
/>
)}
{isInit ? (
isObjectChildren && 'body' in children ? (
(children as Children).body
) : (
<SpecWidget {...props} spec={mergedSpec} />
)
) : null}
</PopoverBody>
</PopoverContent>
</Portal>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { EventWidget } from '@sunmao-ui/editor-sdk';
import {
Box,
@ -44,9 +44,13 @@ export const EventHandlerForm: React.FC<Props> = props => {
onUp,
onDown,
} = props;
const [hasOpened, setHasOpened] = React.useState<boolean>(false);
const onAccordionChange = useCallback(() => {
setHasOpened(true);
}, []);
return (
<Accordion width="100%" allowMultiple>
<Accordion width="100%" allowMultiple onChange={onAccordionChange}>
<AccordionItem>
<h2>
<AccordionButton>
@ -57,45 +61,47 @@ export const EventHandlerForm: React.FC<Props> = props => {
</AccordionButton>
</h2>
<AccordionPanel pb={4} pt={2} padding={0}>
<Box position="relative" width="100%">
<VStack className={formWrapperCSS}>
<EventWidget
component={component}
spec={spec}
value={handler}
path={[]}
level={1}
services={services}
onChange={onChange}
/>
</VStack>
<Box position="absolute" right="4" top="4">
<IconButton
aria-label="up event handler"
icon={<ArrowUpIcon />}
size="xs"
variant="ghost"
disabled={index === 0}
onClick={onUp}
/>
<IconButton
aria-label="down event handler"
icon={<ArrowDownIcon />}
size="xs"
variant="ghost"
disabled={index === size - 1}
onClick={onDown}
/>
<IconButton
aria-label="remove event handler"
colorScheme="red"
icon={<CloseIcon />}
onClick={onRemove}
size="xs"
variant="ghost"
/>
{hasOpened ? (
<Box position="relative" width="100%">
<VStack className={formWrapperCSS}>
<EventWidget
component={component}
spec={spec}
value={handler}
path={[]}
level={1}
services={services}
onChange={onChange}
/>
</VStack>
<Box position="absolute" right="4" top="4">
<IconButton
aria-label="up event handler"
icon={<ArrowUpIcon />}
size="xs"
variant="ghost"
disabled={index === 0}
onClick={onUp}
/>
<IconButton
aria-label="down event handler"
icon={<ArrowDownIcon />}
size="xs"
variant="ghost"
disabled={index === size - 1}
onClick={onDown}
/>
<IconButton
aria-label="remove event handler"
colorScheme="red"
icon={<CloseIcon />}
onClick={onRemove}
size="xs"
variant="ghost"
/>
</Box>
</Box>
</Box>
) : null}
</AccordionPanel>
</AccordionItem>
</Accordion>

View File

@ -144,6 +144,12 @@ export const Editor: React.FC<Props> = observer(
}
}, [isDisplayApp]);
const onPreview = useCallback(() => setPreview(true), []);
const onSelectComponent = useCallback(id => {
editorStore.setSelectedComponentId(id);
}, [editorStore]);
const onRightTabChange = useCallback(activatedTab => {
setToolMenuTab(activatedTab);
}, []);
const renderMain = () => {
const appBox = (
@ -215,9 +221,7 @@ export const Editor: React.FC<Props> = observer(
<StructureTree
components={components}
selectedComponentId={selectedComponentId}
onSelectComponent={id => {
editorStore.setSelectedComponentId(id);
}}
onSelectComponent={onSelectComponent}
services={services}
/>
</TabPanel>
@ -256,9 +260,8 @@ export const Editor: React.FC<Props> = observer(
display="flex"
flexDirection="column"
index={toolMenuTab}
onChange={activatedTab => {
setToolMenuTab(activatedTab);
}}
onChange={onRightTabChange}
isLazy
>
<TabList background="gray.50">
<Tab>Inspect</Tab>

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useCallback } from 'react';
import { Box, Text, VStack } from '@chakra-ui/react';
import { ComponentSchema } from '@sunmao-ui/core';
import { ComponentItemView } from './ComponentItemView';
@ -6,26 +6,43 @@ import { DropComponentWrapper } from './DropComponentWrapper';
import { ChildrenMap } from './StructureTree';
import { genOperation } from '../../operations';
import { EditorServices } from '../../types';
import { observer } from 'mobx-react-lite';
type Props = {
component: ComponentSchema;
parentId: string | undefined;
slot: string | undefined;
childrenMap: ChildrenMap;
selectedComponentId: string;
onSelectComponent: (id: string) => void;
services: EditorServices;
isAncestorDragging: boolean;
depth: number;
};
type ComponentTreeProps = Props & {
isSelected: boolean;
};
export const ComponentTree: React.FC<Props> = props => {
const observeSelected = (Component: React.FC<ComponentTreeProps>) => {
const ObserveActive: React.FC<Props> = props => {
const { services } = props;
const { editorStore } = services;
const { selectedComponentId } = editorStore;
return (
<Component {...props} isSelected={selectedComponentId === props.component.id} />
);
};
return observer(ObserveActive);
};
const ComponentTree = (props: ComponentTreeProps) => {
const {
component,
childrenMap,
parentId,
slot,
selectedComponentId,
isSelected,
onSelectComponent,
services,
isAncestorDragging,
@ -47,13 +64,12 @@ export const ComponentTree: React.FC<Props> = props => {
if (slotChildren && slotChildren.length > 0) {
slotContent = slotChildren.map(c => {
return (
<ComponentTree
<ComponentTreeWrapper
key={c.id}
component={c}
parentId={component.id}
slot={_slot}
childrenMap={childrenMap}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
services={services}
isAncestorDragging={isAncestorDragging || isDragging}
@ -89,7 +105,7 @@ export const ComponentTree: React.FC<Props> = props => {
return (
<Box key={_slot} paddingLeft="3" width="full">
{/* although component can have multiple slots, but for now, most components have only one slot
so we hide slot name to save more view area */}
so we hide slot name to save more view area */}
{slots.length > 1 ? slotName : undefined}
<VStack spacing="0" width="full" alignItems="start">
{slotContent}
@ -101,7 +117,6 @@ export const ComponentTree: React.FC<Props> = props => {
slots,
childrenMap,
component.id,
selectedComponentId,
onSelectComponent,
services,
isAncestorDragging,
@ -110,14 +125,20 @@ export const ComponentTree: React.FC<Props> = props => {
isExpanded,
]);
const onClickRemove = () => {
const onClickRemove = useCallback(() => {
eventBus.send(
'operation',
genOperation(registry, 'removeComponent', {
componentId: component.id,
})
);
};
}, [component.id, eventBus, registry]);
const onClickItem = useCallback(() => {
onSelectComponent(component.id);
}, [component.id, onSelectComponent]);
const onToggleExpanded = useCallback(() => setIsExpanded(prev => !prev), []);
const onDragStart = useCallback(() => setIsDragging(true), []);
const onDragEnd = useCallback(() => setIsDragging(false), []);
return (
<VStack
@ -140,16 +161,14 @@ export const ComponentTree: React.FC<Props> = props => {
<ComponentItemView
id={component.id}
title={component.id}
isSelected={component.id === selectedComponentId}
onClick={() => {
onSelectComponent(component.id);
}}
isSelected={isSelected}
onClick={onClickItem}
onClickRemove={onClickRemove}
noChevron={slots.length === 0}
isExpanded={isExpanded}
onToggleExpanded={() => setIsExpanded(prev => !prev)}
onDragStart={() => setIsDragging(true)}
onDragEnd={() => setIsDragging(false)}
onToggleExpanded={onToggleExpanded}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
depth={depth}
/>
</DropComponentWrapper>
@ -157,3 +176,7 @@ export const ComponentTree: React.FC<Props> = props => {
</VStack>
);
};
export const ComponentTreeWrapper: React.FC<Props> = observeSelected(
React.memo(ComponentTree)
);

View File

@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState, useCallback } from 'react';
import { genOperation } from '../../operations';
import { EditorServices } from '../../types';
@ -31,83 +31,99 @@ export const DropComponentWrapper: React.FC<Props> = props => {
const [dragDirection, setDragDirection] = useState<'prev' | 'next' | undefined>();
const [isDragOver, setIsDragOver] = useState<boolean>(false);
const onDragOver = (e: React.DragEvent) => {
if (!droppable) {
return;
}
setIsDragOver(true);
e.preventDefault();
e.stopPropagation();
const onDragOver = useCallback(
(e: React.DragEvent) => {
if (!droppable) {
return;
}
setIsDragOver(true);
e.preventDefault();
e.stopPropagation();
if (isDropInOnly) return;
if (isDropInOnly) return;
const rect = ref.current?.getBoundingClientRect();
const rect = ref.current?.getBoundingClientRect();
if (!rect) return;
if (!rect) return;
if (e.clientY < rect.top + rect.height / 2) {
setDragDirection('prev');
} else if (e.clientY >= rect.top + rect.height / 2) {
setDragDirection('next');
}
};
if (e.clientY < rect.top + rect.height / 2) {
setDragDirection('prev');
} else if (e.clientY >= rect.top + rect.height / 2) {
setDragDirection('next');
}
},
[droppable, isDropInOnly]
);
const onDragLeave = () => {
const onDragLeave = useCallback(() => {
if (!droppable) {
return;
}
setDragDirection(undefined);
setIsDragOver(false);
};
}, [droppable]);
const onDrop = (e: React.DragEvent) => {
if (!droppable) {
return;
}
e.stopPropagation();
e.preventDefault();
const creatingComponent = e.dataTransfer?.getData('component') || '';
const movingComponent = e.dataTransfer?.getData('moveComponent') || '';
const onDrop = useCallback(
(e: React.DragEvent) => {
if (!droppable) {
return;
}
e.stopPropagation();
e.preventDefault();
const creatingComponent = e.dataTransfer?.getData('component') || '';
const movingComponent = e.dataTransfer?.getData('moveComponent') || '';
let targetParentId = parentId;
let targetParentSlot = parentSlot;
let targetId = componentId;
if (dragDirection === 'next' && isExpanded && hasSlot) {
targetParentId = componentId;
targetParentSlot = 'content';
targetId = undefined;
}
let targetParentId = parentId;
let targetParentSlot = parentSlot;
let targetId = componentId;
if (dragDirection === 'next' && isExpanded && hasSlot) {
targetParentId = componentId;
targetParentSlot = 'content';
targetId = undefined;
}
// move component before or after currentComponent
if (movingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'moveComponent', {
fromId: movingComponent,
toId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
// move component before or after currentComponent
if (movingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'moveComponent', {
fromId: movingComponent,
toId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
// create component as children
if (creatingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'createComponent', {
componentType: creatingComponent,
parentId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
setDragDirection(undefined);
setIsDragOver(false);
};
// create component as children
if (creatingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'createComponent', {
componentType: creatingComponent,
parentId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
setDragDirection(undefined);
setIsDragOver(false);
},
[
droppable,
dragDirection,
isExpanded,
hasSlot,
componentId,
parentId,
parentSlot,
eventBus,
registry,
]
);
const boxShadow = useMemo(() => {
if (isDropInOnly) return '';
@ -124,7 +140,7 @@ export const DropComponentWrapper: React.FC<Props> = props => {
ref={ref}
width="full"
boxShadow={boxShadow}
background={ isDragOver ? '#ffc6c6' : undefined}
background={isDragOver ? '#ffc6c6' : undefined}
onDrop={onDrop}
onDragOver={onDragOver}
onDragLeave={onDragLeave}

View File

@ -1,7 +1,7 @@
import React, { useMemo, useRef, useEffect } from 'react';
import { ComponentSchema } from '@sunmao-ui/core';
import { Box, Text, VStack } from '@chakra-ui/react';
import { ComponentTree } from './ComponentTree';
import { ComponentTreeWrapper } from './ComponentTree';
import { DropComponentWrapper } from './DropComponentWrapper';
import { resolveApplicationComponents } from '../../utils/resolveApplicationComponents';
import ErrorBoundary from '../ErrorBoundary';
@ -33,27 +33,26 @@ export const StructureTree: React.FC<Props> = props => {
resolveApplicationComponents(realComponents);
return topLevelComponents.map(c => (
<ComponentTree
<ComponentTreeWrapper
key={c.id}
component={c}
parentId={undefined}
slot={undefined}
childrenMap={childrenMap}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
services={services}
isAncestorDragging={false}
depth={0}
/>
));
}, [realComponents, selectedComponentId, onSelectComponent, services]);
}, [realComponents, onSelectComponent, services]);
useEffect(() => {
wrapperRef.current
?.querySelector(`#tree-item-${selectedComponentId}`)
?.scrollIntoView({
behavior:'smooth',
block:'center'
block: 'nearest',
inline: 'nearest'
});
}, [selectedComponentId]);