diff --git a/packages/editor-sdk/src/components/Form/ArrayTable.tsx b/packages/editor-sdk/src/components/Form/ArrayTable.tsx index f33f5dc4..94a1c984 100644 --- a/packages/editor-sdk/src/components/Form/ArrayTable.tsx +++ b/packages/editor-sdk/src/components/Form/ArrayTable.tsx @@ -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 => { - const { value, itemSpec, spec, level, path, children, onChange } = props; +const DEFAULT_KEYS = ['index']; + +const TableRow: React.FC = 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 ( + + + + {typeof children === 'function' ? children(props, itemValue, itemIndex) : null} + + + {keys.map((key: string) => { + const propertyValue = + key === 'index' ? itemValue[key] ?? itemIndex : itemValue[key]; + + return {propertyValue}; + })} + + + + + ); +}; + +export const ArrayTable: React.FC = 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 => { {value.map((itemValue: any, itemIndex: number) => ( - - - { - const newValue = [...value]; - newValue[itemIndex] = newItemValue; - onChange(newValue); - }} - > - {typeof children === 'function' - ? children(props, itemValue, itemIndex) - : null} - - - {keys.map((key: string) => { - const propertyValue = - key === 'index' ? itemValue[key] ?? itemIndex : itemValue[key]; - - return {propertyValue}; - })} - - - - + ))} diff --git a/packages/editor-sdk/src/components/Widgets/EventWidget.tsx b/packages/editor-sdk/src/components/Widgets/EventWidget.tsx index 45c2528f..9e4b7344 100644 --- a/packages/editor-sdk/src/components/Widgets/EventWidget.tsx +++ b/packages/editor-sdk/src/components/Widgets/EventWidget.tsx @@ -93,6 +93,10 @@ export const EventWidget: React.FC> = 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> = observ } }, [value, updateMethods]); - const onTargetComponentChange = (e: React.ChangeEvent) => { + const onTargetComponentChange = useCallback((e: React.ChangeEvent) => { 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 = ( Event Type formik.submitForm()} + onBlur={onSubmit} onChange={onTargetComponentChange} placeholder="Select Target Component" value={formik.values.componentId} @@ -174,7 +189,7 @@ export const EventWidget: React.FC> = observ Method formik.submitForm()} + onBlur={onSubmit} onChange={formik.handleChange} value={formik.values.wait?.type} > @@ -227,7 +239,7 @@ export const EventWidget: React.FC> = observ Wait Time formik.submitForm()} + onBlur={onSubmit} onChange={formik.handleChange} value={formik.values.wait?.time} /> @@ -239,14 +251,11 @@ export const EventWidget: React.FC> = observ Disabled { - formik.setFieldValue('disabled', value); - formik.submitForm(); - }} + onChange={onDisabledChange} /> ); diff --git a/packages/editor-sdk/src/components/Widgets/PopoverWidget.tsx b/packages/editor-sdk/src/components/Widgets/PopoverWidget.tsx index b5ec7b5b..0f3e2b74 100644 --- a/packages/editor-sdk/src/components/Widgets/PopoverWidget.tsx +++ b/packages/editor-sdk/src/components/Widgets/PopoverWidget.tsx @@ -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< - {isObjectChildren && 'body' in children ? ( - (children as Children).body - ) : ( - - )} + {isInit ? ( + isObjectChildren && 'body' in children ? ( + (children as Children).body + ) : ( + + ) + ) : null} diff --git a/packages/editor/src/components/ComponentForm/EventTraitForm/EventHandlerForm.tsx b/packages/editor/src/components/ComponentForm/EventTraitForm/EventHandlerForm.tsx index a4d02ec8..56834f38 100644 --- a/packages/editor/src/components/ComponentForm/EventTraitForm/EventHandlerForm.tsx +++ b/packages/editor/src/components/ComponentForm/EventTraitForm/EventHandlerForm.tsx @@ -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 => { onUp, onDown, } = props; + const [hasOpened, setHasOpened] = React.useState(false); + const onAccordionChange = useCallback(() => { + setHasOpened(true); + }, []); return ( - +

@@ -57,45 +61,47 @@ export const EventHandlerForm: React.FC = props => {

- - - - - - } - size="xs" - variant="ghost" - disabled={index === 0} - onClick={onUp} - /> - } - size="xs" - variant="ghost" - disabled={index === size - 1} - onClick={onDown} - /> - } - onClick={onRemove} - size="xs" - variant="ghost" - /> + {hasOpened ? ( + + + + + + } + size="xs" + variant="ghost" + disabled={index === 0} + onClick={onUp} + /> + } + size="xs" + variant="ghost" + disabled={index === size - 1} + onClick={onDown} + /> + } + onClick={onRemove} + size="xs" + variant="ghost" + /> + - + ) : null}
diff --git a/packages/editor/src/components/Editor.tsx b/packages/editor/src/components/Editor.tsx index e784a355..a6f83f9c 100644 --- a/packages/editor/src/components/Editor.tsx +++ b/packages/editor/src/components/Editor.tsx @@ -144,6 +144,12 @@ export const Editor: React.FC = 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 = observer( { - editorStore.setSelectedComponentId(id); - }} + onSelectComponent={onSelectComponent} services={services} /> @@ -256,9 +260,8 @@ export const Editor: React.FC = observer( display="flex" flexDirection="column" index={toolMenuTab} - onChange={activatedTab => { - setToolMenuTab(activatedTab); - }} + onChange={onRightTabChange} + isLazy > Inspect diff --git a/packages/editor/src/components/StructureTree/ComponentTree.tsx b/packages/editor/src/components/StructureTree/ComponentTree.tsx index f4ade1f5..f398b43a 100644 --- a/packages/editor/src/components/StructureTree/ComponentTree.tsx +++ b/packages/editor/src/components/StructureTree/ComponentTree.tsx @@ -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 => { +const observeSelected = (Component: React.FC) => { + const ObserveActive: React.FC = props => { + const { services } = props; + const { editorStore } = services; + const { selectedComponentId } = editorStore; + + return ( + + ); + }; + + 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 => { if (slotChildren && slotChildren.length > 0) { slotContent = slotChildren.map(c => { return ( - = props => { return ( {/* 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} {slotContent} @@ -101,7 +117,6 @@ export const ComponentTree: React.FC = props => { slots, childrenMap, component.id, - selectedComponentId, onSelectComponent, services, isAncestorDragging, @@ -110,14 +125,20 @@ export const ComponentTree: React.FC = 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 ( = props => { { - 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} /> @@ -157,3 +176,7 @@ export const ComponentTree: React.FC = props => { ); }; + +export const ComponentTreeWrapper: React.FC = observeSelected( + React.memo(ComponentTree) +); diff --git a/packages/editor/src/components/StructureTree/DropComponentWrapper.tsx b/packages/editor/src/components/StructureTree/DropComponentWrapper.tsx index e88f7d26..c7137885 100644 --- a/packages/editor/src/components/StructureTree/DropComponentWrapper.tsx +++ b/packages/editor/src/components/StructureTree/DropComponentWrapper.tsx @@ -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 => { const [dragDirection, setDragDirection] = useState<'prev' | 'next' | undefined>(); const [isDragOver, setIsDragOver] = useState(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 => { ref={ref} width="full" boxShadow={boxShadow} - background={ isDragOver ? '#ffc6c6' : undefined} + background={isDragOver ? '#ffc6c6' : undefined} onDrop={onDrop} onDragOver={onDragOver} onDragLeave={onDragLeave} diff --git a/packages/editor/src/components/StructureTree/StructureTree.tsx b/packages/editor/src/components/StructureTree/StructureTree.tsx index a54ecfc5..d87d7aa9 100644 --- a/packages/editor/src/components/StructureTree/StructureTree.tsx +++ b/packages/editor/src/components/StructureTree/StructureTree.tsx @@ -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 => { resolveApplicationComponents(realComponents); return topLevelComponents.map(c => ( - )); - }, [realComponents, selectedComponentId, onSelectComponent, services]); + }, [realComponents, onSelectComponent, services]); useEffect(() => { wrapperRef.current ?.querySelector(`#tree-item-${selectedComponentId}`) ?.scrollIntoView({ - behavior:'smooth', - block:'center' + block: 'nearest', + inline: 'nearest' }); }, [selectedComponentId]);