From 93ba7e3152694e5ead198e51670b74855ad50e9b Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Thu, 24 Nov 2022 16:12:03 +0800 Subject: [PATCH] feat(runtime): support Component DataSource --- packages/core/src/metadata.ts | 1 + packages/core/src/trait.ts | 2 +- .../src/components/ApiForm/ApiForm.tsx | 211 +++++++++++++ .../src/components}/ApiForm/Basic.tsx | 7 +- .../src/components}/ApiForm/Body.tsx | 11 +- .../src/components}/ApiForm/Headers.tsx | 11 +- .../src/components}/ApiForm/Params.tsx | 7 +- .../src/components}/ApiForm/Response.tsx | 23 +- .../src/components}/ApiForm/index.ts | 0 .../src/components/Widgets/FetchWidget.tsx | 51 ++++ .../src/components/Widgets/SpecWidget.tsx | 15 +- .../src/components/Widgets/index.ts | 3 + packages/editor-sdk/src/types/editor.ts | 15 +- packages/editor-sdk/src/types/widget.ts | 4 +- .../__tests__/model/componentModel.spec.ts | 2 +- .../editor/src/AppModel/ComponentModel.ts | 8 +- .../ComponentForm/ComponentForm.tsx | 26 +- .../GeneralTraitFormList/AddTraitButton.tsx | 2 +- .../GeneralTraitFormList/GeneralTraitForm.tsx | 58 ++-- .../ComponentsList/ComponentList.tsx | 10 +- .../editor/src/components/DataSource/Api.tsx | 84 ------ .../components/DataSource/ApiForm/ApiForm.tsx | 284 ------------------ .../editor/src/components/DataSource/Data.tsx | 108 ------- .../DataSource/DataForm/DataForm.tsx | 92 ------ .../components/DataSource/DataForm/index.ts | 1 - .../src/components/DataSource/DataSource.tsx | 119 -------- .../components/DataSource/DataSourceGroup.tsx | 94 ++++++ .../components/DataSource/DataSourceItem.tsx | 61 ---- .../components/DataSource/DataSourceList.tsx | 183 +++++++++++ .../editor/src/components/DataSource/index.ts | 2 +- packages/editor/src/components/Editor.tsx | 42 +-- .../StructureTree/ComponentItemView.tsx | 3 + .../StructureTree/ComponentNode.tsx | 3 + .../StructureTree/ComponentSearch.tsx | 10 +- .../StructureTree/StructureTree.tsx | 8 +- .../StructureTree/useStructureTreeState.ts | 6 +- packages/editor/src/constants/dataSource.ts | 44 --- packages/editor/src/constants/index.ts | 8 +- packages/editor/src/init.tsx | 5 +- .../createDataSourceBranchOperation.ts | 26 +- .../leaf/trait/createTraitLeafOperation.ts | 14 +- packages/editor/src/services/EditorStore.ts | 141 ++------- packages/editor/src/types.ts | 18 +- .../src/utils/resolveApplicationComponents.ts | 8 +- .../runtime/src/components/core/Dummy.tsx | 1 + packages/runtime/src/components/core/List.tsx | 3 +- packages/runtime/src/index.ts | 1 + packages/runtime/src/services/Registry.tsx | 2 + packages/runtime/src/traits/core/Fetch.tsx | 72 +++-- .../runtime/src/traits/core/LocalStorage.tsx | 1 + packages/runtime/src/traits/core/SlotV2.tsx | 38 +++ packages/runtime/src/traits/core/State.tsx | 1 + .../runtime/src/traits/core/Transformer.tsx | 1 + 53 files changed, 802 insertions(+), 1149 deletions(-) create mode 100644 packages/editor-sdk/src/components/ApiForm/ApiForm.tsx rename packages/{editor/src/components/DataSource => editor-sdk/src/components}/ApiForm/Basic.tsx (94%) rename packages/{editor/src/components/DataSource => editor-sdk/src/components}/ApiForm/Body.tsx (91%) rename packages/{editor/src/components/DataSource => editor-sdk/src/components}/ApiForm/Headers.tsx (86%) rename packages/{editor/src/components/DataSource => editor-sdk/src/components}/ApiForm/Params.tsx (91%) rename packages/{editor/src/components/DataSource => editor-sdk/src/components}/ApiForm/Response.tsx (83%) rename packages/{editor/src/components/DataSource => editor-sdk/src/components}/ApiForm/index.ts (100%) create mode 100644 packages/editor-sdk/src/components/Widgets/FetchWidget.tsx delete mode 100644 packages/editor/src/components/DataSource/Api.tsx delete mode 100644 packages/editor/src/components/DataSource/ApiForm/ApiForm.tsx delete mode 100644 packages/editor/src/components/DataSource/Data.tsx delete mode 100644 packages/editor/src/components/DataSource/DataForm/DataForm.tsx delete mode 100644 packages/editor/src/components/DataSource/DataForm/index.ts delete mode 100644 packages/editor/src/components/DataSource/DataSource.tsx create mode 100644 packages/editor/src/components/DataSource/DataSourceGroup.tsx delete mode 100644 packages/editor/src/components/DataSource/DataSourceItem.tsx create mode 100644 packages/editor/src/components/DataSource/DataSourceList.tsx delete mode 100644 packages/editor/src/constants/dataSource.ts create mode 100644 packages/runtime/src/traits/core/SlotV2.tsx diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 6bef151f5..c772a30f5 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -6,6 +6,7 @@ export type Metadata< description?: string; annotations?: Record & TAnnotations; exampleProperties?: TExample; + isDataSource?: boolean; }; type ComponentCategory = diff --git a/packages/core/src/trait.ts b/packages/core/src/trait.ts index 927814faa..14f1baebe 100644 --- a/packages/core/src/trait.ts +++ b/packages/core/src/trait.ts @@ -39,7 +39,7 @@ export function createTrait(options: CreateTraitOptions): RuntimeTrait { kind: 'Trait' as any, parsedVersion: parseVersion(options.version), metadata: { - name: options.metadata.name, + ...options.metadata, description: options.metadata.description || '', annotations: options.metadata.annotations || {}, }, diff --git a/packages/editor-sdk/src/components/ApiForm/ApiForm.tsx b/packages/editor-sdk/src/components/ApiForm/ApiForm.tsx new file mode 100644 index 000000000..92d96a7c6 --- /dev/null +++ b/packages/editor-sdk/src/components/ApiForm/ApiForm.tsx @@ -0,0 +1,211 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { ComponentSchema } from '@sunmao-ui/core'; +import { watch, FetchTraitPropertiesSpec } from '@sunmao-ui/runtime'; +import { Static, Type } from '@sinclair/typebox'; +import { + Box, + VStack, + HStack, + Text, + Tabs, + TabPanels, + TabPanel, + TabList, + Tab, + Select, + Button, +} from '@chakra-ui/react'; +import { useFormik } from 'formik'; +import { Basic } from './Basic'; +import { Headers as HeadersForm } from './Headers'; +import { Params } from './Params'; +import { Body } from './Body'; +import { Response as ResponseInfo } from './Response'; +import { EditorServicesInterface } from '../../types/editor'; +import { ExpressionWidget } from '../Widgets'; +import { WidgetProps } from '../..'; + +enum TabIndex { + Basic, + Headers, + Params, + Body, +} +interface Props { + value: Static; + component: ComponentSchema; + services: EditorServicesInterface; + onChange: (value: Static) => void; +} + +const METHODS = ['get', 'post', 'put', 'delete', 'patch']; +const EMPTY_ARRAY: string[] = []; + +type FetchResultType = { + data?: unknown; + code?: number; + codeText?: string; + error?: string; + loading?: boolean; +}; + +export const ApiForm: React.FC = props => { + const { value, onChange, component, services } = props; + const [tabIndex, setTabIndex] = useState(0); + const [fetchResult, setFetchResult] = useState(); + const formik = useFormik({ + initialValues: value, + onSubmit: values => { + onChange(values); + }, + }); + const { values } = formik; + const URLSpec = Type.String({ + widget: 'core/v1/expression', + widgetOptions: { + compactOptions: { + paddingY: '6px', + }, + }, + }); + + const onFetch = useCallback(() => { + services.apiService.send('uiMethod', { + componentId: component.id, + name: 'triggerFetch', + parameters: {}, + }); + }, [services.apiService, component]); + const onMethodChange = useCallback( + (e: React.ChangeEvent) => { + formik.handleChange(e); + formik.handleSubmit(); + if (e.target.value === 'get' && tabIndex === TabIndex.Body) { + setTabIndex(0); + } + }, + [formik, tabIndex] + ); + const onURLChange = useCallback( + (value: string) => { + formik.setFieldValue('url', value); + formik.handleSubmit(); + }, + [formik] + ); + const onKeyDown = useCallback((e: React.KeyboardEvent) => { + // prevent form keyboard events to accidentally trigger operation shortcut + e.stopPropagation(); + }, []); + + useEffect(() => { + formik.setValues({ ...value }); + // do not add formik into dependencies, otherwise it will cause infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + useEffect(() => { + const stop = watch( + () => services.stateManager.store[component.id]?.fetch, + newValue => { + setFetchResult({ ...newValue }); + } + ); + + return stop; + }, [component.id, services.stateManager.store]); + + return ( + + + {component.id} + + + + + + + + + { + setTabIndex(index); + }} + > + + + Basic + Headers + Params + {values.method !== 'get' ? Body : null} + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/editor/src/components/DataSource/ApiForm/Basic.tsx b/packages/editor-sdk/src/components/ApiForm/Basic.tsx similarity index 94% rename from packages/editor/src/components/DataSource/ApiForm/Basic.tsx rename to packages/editor-sdk/src/components/ApiForm/Basic.tsx index b073f08fd..b6d46dba0 100644 --- a/packages/editor/src/components/DataSource/ApiForm/Basic.tsx +++ b/packages/editor-sdk/src/components/ApiForm/Basic.tsx @@ -3,16 +3,17 @@ import { VStack, FormControl, FormLabel, Switch } from '@chakra-ui/react'; import { FormikHelpers, FormikHandlers, FormikState } from 'formik'; import { FetchTraitPropertiesSpec } from '@sunmao-ui/runtime'; import { Static, Type } from '@sinclair/typebox'; -import { EditorServices } from '../../../types'; import { ComponentSchema } from '@sunmao-ui/core'; -import { SpecWidget, mergeWidgetOptionsIntoSpec } from '@sunmao-ui/editor-sdk'; import { JSONSchema7 } from 'json-schema'; +import { EditorServicesInterface } from '../../types/editor'; +import { mergeWidgetOptionsIntoSpec } from '../..'; +import { SpecWidget } from '../Widgets'; type Values = Static; interface Props { api: ComponentSchema; formik: FormikHelpers & FormikHandlers & FormikState; - services: EditorServices; + services: EditorServicesInterface; } const DisabledSpec = Type.Boolean({ diff --git a/packages/editor/src/components/DataSource/ApiForm/Body.tsx b/packages/editor-sdk/src/components/ApiForm/Body.tsx similarity index 91% rename from packages/editor/src/components/DataSource/ApiForm/Body.tsx rename to packages/editor-sdk/src/components/ApiForm/Body.tsx index 8cd4cd549..5e0f169c4 100644 --- a/packages/editor/src/components/DataSource/ApiForm/Body.tsx +++ b/packages/editor-sdk/src/components/ApiForm/Body.tsx @@ -1,22 +1,19 @@ import React, { useCallback, useMemo } from 'react'; import { Box, Select, Text, VStack } from '@chakra-ui/react'; -import { - SpecWidget, - WidgetProps, - mergeWidgetOptionsIntoSpec, -} from '@sunmao-ui/editor-sdk'; import { FormikHelpers, FormikHandlers, FormikState } from 'formik'; import { FetchTraitPropertiesSpec } from '@sunmao-ui/runtime'; import { ComponentSchema } from '@sunmao-ui/core'; import { Static } from '@sinclair/typebox'; -import { EditorServices } from '../../../types'; +import { EditorServicesInterface } from '../../types/editor'; +import { mergeWidgetOptionsIntoSpec, WidgetProps } from '../..'; +import { SpecWidget } from '../Widgets'; type Values = Static; interface Props { api: ComponentSchema; spec: WidgetProps['spec']; formik: FormikHelpers & FormikHandlers & FormikState; - services: EditorServices; + services: EditorServicesInterface; } const EMPTY_ARRAY: string[] = []; diff --git a/packages/editor/src/components/DataSource/ApiForm/Headers.tsx b/packages/editor-sdk/src/components/ApiForm/Headers.tsx similarity index 86% rename from packages/editor/src/components/DataSource/ApiForm/Headers.tsx rename to packages/editor-sdk/src/components/ApiForm/Headers.tsx index 689b3b7d2..5c74ef7e1 100644 --- a/packages/editor/src/components/DataSource/ApiForm/Headers.tsx +++ b/packages/editor-sdk/src/components/ApiForm/Headers.tsx @@ -1,22 +1,19 @@ import React, { useCallback, useMemo } from 'react'; import { Box } from '@chakra-ui/react'; -import { - RecordWidget, - WidgetProps, - mergeWidgetOptionsIntoSpec, -} from '@sunmao-ui/editor-sdk'; import { FormikHelpers, FormikHandlers, FormikState } from 'formik'; import { FetchTraitPropertiesSpec } from '@sunmao-ui/runtime'; import { ComponentSchema } from '@sunmao-ui/core'; import { Static } from '@sinclair/typebox'; -import { EditorServices } from '../../../types'; +import { EditorServicesInterface } from '../../types/editor'; +import { mergeWidgetOptionsIntoSpec, WidgetProps } from '../..'; +import { RecordWidget } from '../Widgets'; type Values = Static; interface Props { api: ComponentSchema; spec: WidgetProps['spec']; formik: FormikHelpers & FormikHandlers & FormikState; - services: EditorServices; + services: EditorServicesInterface; } const EMPTY_ARRAY: string[] = []; diff --git a/packages/editor/src/components/DataSource/ApiForm/Params.tsx b/packages/editor-sdk/src/components/ApiForm/Params.tsx similarity index 91% rename from packages/editor/src/components/DataSource/ApiForm/Params.tsx rename to packages/editor-sdk/src/components/ApiForm/Params.tsx index 4ad70180b..59aa4c975 100644 --- a/packages/editor/src/components/DataSource/ApiForm/Params.tsx +++ b/packages/editor-sdk/src/components/ApiForm/Params.tsx @@ -1,17 +1,18 @@ import React, { useCallback, useMemo } from 'react'; import { Box } from '@chakra-ui/react'; -import { RecordWidget, mergeWidgetOptionsIntoSpec } from '@sunmao-ui/editor-sdk'; import { FormikHelpers, FormikHandlers, FormikState } from 'formik'; import { Type, Static } from '@sinclair/typebox'; import { FetchTraitPropertiesSpec } from '@sunmao-ui/runtime'; import { ComponentSchema } from '@sunmao-ui/core'; -import { EditorServices } from '../../../types'; +import { EditorServicesInterface } from '../../types/editor'; +import { RecordWidget } from '../Widgets'; +import { mergeWidgetOptionsIntoSpec } from '../..'; type Values = Static; interface Props { api: ComponentSchema; formik: FormikHelpers & FormikHandlers & FormikState; - services: EditorServices; + services: EditorServicesInterface; } const EMPTY_ARRAY: string[] = []; diff --git a/packages/editor/src/components/DataSource/ApiForm/Response.tsx b/packages/editor-sdk/src/components/ApiForm/Response.tsx similarity index 83% rename from packages/editor/src/components/DataSource/ApiForm/Response.tsx rename to packages/editor-sdk/src/components/ApiForm/Response.tsx index 92cb70e6f..cbfe3e13a 100644 --- a/packages/editor/src/components/DataSource/ApiForm/Response.tsx +++ b/packages/editor-sdk/src/components/ApiForm/Response.tsx @@ -10,7 +10,6 @@ import { Spinner, Tag, } from '@chakra-ui/react'; -import { CodeEditor } from '../../CodeEditor'; import { css } from '@emotion/css'; interface Props { @@ -39,7 +38,7 @@ export const Response: React.FC = props => { const error = useMemo(() => { return stringify(props.error); }, [props.error]); - return props.data || props.error || props.loading ? ( + return props.data || props.error || props.loading || props.codeText ? ( setIsOpen(i === 0)} allowToggle @@ -52,7 +51,7 @@ export const Response: React.FC = props => { Response - {props.data || props.error ? ( + {props.data || props.error || props.codeText ? ( {props.code} {(props.codeText || '').toLocaleUpperCase()} @@ -62,22 +61,20 @@ export const Response: React.FC = props => { - + {props.loading || !isOpen ? ( ) : ( - + > + {error || data} + )} diff --git a/packages/editor/src/components/DataSource/ApiForm/index.ts b/packages/editor-sdk/src/components/ApiForm/index.ts similarity index 100% rename from packages/editor/src/components/DataSource/ApiForm/index.ts rename to packages/editor-sdk/src/components/ApiForm/index.ts diff --git a/packages/editor-sdk/src/components/Widgets/FetchWidget.tsx b/packages/editor-sdk/src/components/Widgets/FetchWidget.tsx new file mode 100644 index 000000000..82205f534 --- /dev/null +++ b/packages/editor-sdk/src/components/Widgets/FetchWidget.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { WidgetProps } from '../../types/widget'; +import { implementWidget } from '../../utils/widget'; +import { CORE_VERSION } from '@sunmao-ui/shared'; +import { + Box, + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, +} from '@chakra-ui/react'; +import { ApiForm } from '../ApiForm'; + +type FetchWidgetType = `${typeof CORE_VERSION}/fetch`; + +declare module '../../types/widget' { + interface WidgetOptionsMap { + 'core/v1/fetch': {}; + } +} + +export const FetchWidget: React.FC> = props => { + const { value, onChange, component, services } = props; + const [isOpen, setIsOpen] = useState(true); + return ( + + + setIsOpen(false)} isOpen={isOpen}> + + + + + + + + + ); +}; + +export default implementWidget({ + version: CORE_VERSION, + metadata: { + name: 'fetch', + }, +})(FetchWidget); diff --git a/packages/editor-sdk/src/components/Widgets/SpecWidget.tsx b/packages/editor-sdk/src/components/Widgets/SpecWidget.tsx index cb4594d8e..8aa062719 100644 --- a/packages/editor-sdk/src/components/Widgets/SpecWidget.tsx +++ b/packages/editor-sdk/src/components/Widgets/SpecWidget.tsx @@ -187,6 +187,7 @@ export const SchemaFieldWidgetOptions = Type.Object({ type SpecFieldWidgetType = `${typeof CORE_VERSION}/${CoreWidgetName.Spec}`; type Props = WidgetProps & { + hideCategory?: boolean; children?: | (React.ReactNode & { title?: any; @@ -201,7 +202,17 @@ declare module '../../types/widget' { } export const SpecWidget: React.FC = props => { - const { component, spec, level, path, value, services, children, onChange } = props; + const { + component, + spec, + level, + path, + value, + services, + children, + onChange, + hideCategory, + } = props; const { title, widgetOptions } = spec; const { isShowAsideExpressionButton, expressionOptions, isHidden } = widgetOptions || {}; @@ -226,7 +237,7 @@ export const SpecWidget: React.FC = props => { Component = ExpressionWidget; } else if (widget) { Component = widget.impl; - } else if (level === 0) { + } else if (level === 0 && !hideCategory) { Component = CategoryWidget; showAsideExpressionButton = false; } diff --git a/packages/editor-sdk/src/components/Widgets/index.ts b/packages/editor-sdk/src/components/Widgets/index.ts index 37ff66bc6..51b624c6e 100644 --- a/packages/editor-sdk/src/components/Widgets/index.ts +++ b/packages/editor-sdk/src/components/Widgets/index.ts @@ -14,6 +14,7 @@ import moduleWidgetSpec from './ModuleWidget'; import recordWidgetSpec from './RecordField'; import eventWidgetSpec from './EventWidget'; import popoverWidgetSpec from './PopoverWidget'; +import fetchWidgetSpec from './FetchWidget'; import sizeWidgetSpec from './StyleWidgets/SizeWidget'; import fontWidgetSpec from './StyleWidgets/FontWidget'; import colorWidgetSpec from './StyleWidgets/ColorWidget'; @@ -34,6 +35,7 @@ export * from './ModuleWidget'; export * from './RecordField'; export * from './EventWidget'; export * from './PopoverWidget'; +export * from './FetchWidget'; export * from './StyleWidgets/SizeWidget'; export * from './StyleWidgets/FontWidget'; export * from './StyleWidgets/ColorWidget'; @@ -56,6 +58,7 @@ export const widgets: ImplementedWidget[] = [ recordWidgetSpec, eventWidgetSpec, popoverWidgetSpec, + fetchWidgetSpec, sizeWidgetSpec, fontWidgetSpec, colorWidgetSpec, diff --git a/packages/editor-sdk/src/types/editor.ts b/packages/editor-sdk/src/types/editor.ts index 9b5a1e8a3..24df5b88c 100644 --- a/packages/editor-sdk/src/types/editor.ts +++ b/packages/editor-sdk/src/types/editor.ts @@ -1,16 +1,9 @@ import { ComponentSchema } from '@sunmao-ui/core'; -import { RegistryInterface } from '@sunmao-ui/runtime'; +import { RegistryInterface, UIServices } from '@sunmao-ui/runtime'; import WidgetManager from '../models/WidgetManager'; import type { Operations } from '../types/operation'; -type EvalOptions = { - scopeObject?: Record; - overrideScope?: boolean; - fallbackWhenError?: (exp: string) => any; - ignoreEvalError?: boolean; -}; - -export interface EditorServices { +export interface EditorServicesInterface extends UIServices { registry: RegistryInterface; editorStore: { components: ComponentSchema[]; @@ -19,9 +12,5 @@ export interface EditorServices { appModel: any; doOperations: (operations: Operations) => void; }; - stateManager: { - store: Record; - deepEval: (value: any, options?: EvalOptions) => any; - }; widgetManager: WidgetManager; } diff --git a/packages/editor-sdk/src/types/widget.ts b/packages/editor-sdk/src/types/widget.ts index 92c7d4d87..dc486c342 100644 --- a/packages/editor-sdk/src/types/widget.ts +++ b/packages/editor-sdk/src/types/widget.ts @@ -1,7 +1,7 @@ import React from 'react'; import { JSONSchema7 } from 'json-schema'; import { ComponentSchema } from '@sunmao-ui/core'; -import { EditorServices } from './editor'; +import { EditorServicesInterface } from './editor'; import * as TypeBox from '@sinclair/typebox'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -144,7 +144,7 @@ export type WidgetProps< widget?: keyof WidgetOptionsMap; widgetOptions?: WidgetOptionsMap[WidgetType]; }; - services: EditorServices; + services: EditorServicesInterface; path: string[]; level: number; value: ValueType; diff --git a/packages/editor/__tests__/model/componentModel.spec.ts b/packages/editor/__tests__/model/componentModel.spec.ts index 0ef1d9265..bd0e220eb 100644 --- a/packages/editor/__tests__/model/componentModel.spec.ts +++ b/packages/editor/__tests__/model/componentModel.spec.ts @@ -85,7 +85,7 @@ describe('append to another component', () => { expect(newComponent.parentSlot).toEqual('content'); it('create slot trait', () => { - expect(newComponent.traits[0].type).toEqual('core/v1/slot'); + expect(newComponent.traits[0].type).toEqual('core/v2/slot'); expect(newComponent.traits[0].rawProperties).toEqual({ container: { id: 'vstack1', slot: 'content' }, ifCondition: true, diff --git a/packages/editor/src/AppModel/ComponentModel.ts b/packages/editor/src/AppModel/ComponentModel.ts index a34be8a53..72e1362fa 100644 --- a/packages/editor/src/AppModel/ComponentModel.ts +++ b/packages/editor/src/AppModel/ComponentModel.ts @@ -31,6 +31,7 @@ import { TraitModel } from './TraitModel'; import { FieldModel } from './FieldModel'; const SlotTraitType: TraitType = `${CORE_VERSION}/${CoreTraitName.Slot}` as TraitType; +const SlotTraitTypeV2: TraitType = `core/v2/${CoreTraitName.Slot}` as TraitType; const DynamicStateTrait = [ `${CORE_VERSION}/${CoreTraitName.State}`, `${CORE_VERSION}/${CoreTraitName.LocalStorage}`, @@ -132,7 +133,10 @@ export class ComponentModel implements IComponentModel { } get _slotTrait() { - return this.traits.find(t => t.type === SlotTraitType) || null; + return ( + this.traits.find(t => t.type === SlotTraitType || t.type === SlotTraitTypeV2) || + null + ); } get allComponents(): IComponentModel[] { @@ -292,7 +296,7 @@ export class ComponentModel implements IComponentModel { this._slotTrait.properties.update({ container: { id: parent, slot } }); this._slotTrait._isDirty = true; } else { - this.addTrait(SlotTraitType, { + this.addTrait(SlotTraitTypeV2, { container: { id: parent, slot }, ifCondition: true, }); diff --git a/packages/editor/src/components/ComponentForm/ComponentForm.tsx b/packages/editor/src/components/ComponentForm/ComponentForm.tsx index f23fdbb78..c97031eb9 100644 --- a/packages/editor/src/components/ComponentForm/ComponentForm.tsx +++ b/packages/editor/src/components/ComponentForm/ComponentForm.tsx @@ -28,7 +28,8 @@ type Props = { export const ComponentForm: React.FC = observer(props => { const { services } = props; const { editorStore, registry, eventBus } = services; - const { selectedComponent, selectedComponentId } = editorStore; + const { selectedComponent, selectedComponentId, selectedComponentIsDataSource } = + editorStore; if (!selectedComponentId) { return ( @@ -109,6 +110,7 @@ export const ComponentForm: React.FC = observer(props => { /> ), + hide: Object.keys(cImpl.spec.properties.properties).length === 0, }, { title: 'Events', @@ -123,6 +125,7 @@ export const ComponentForm: React.FC = observer(props => { services={services} /> ), + hide: selectedComponentIsDataSource, }, { title: 'Traits', @@ -142,15 +145,18 @@ export const ComponentForm: React.FC = observer(props => { allowMultiple onKeyDown={onKeyDown} > - {sections.map((section, i) => ( - - {section.node} - - ))} + {sections.map((section, i) => { + if (section.hide) return undefined; + return ( + + {section.node} + + ); + })} ); diff --git a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx index ab0fe7fd2..112e83cad 100644 --- a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx +++ b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx @@ -62,7 +62,7 @@ export const AddTraitButton: React.FC = props => { Add Trait - {menuItems} + {menuItems} diff --git a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx index 6d0bd7d4b..06b3bdac4 100644 --- a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx +++ b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { ComponentSchema, TraitSchema } from '@sunmao-ui/core'; import { HStack, IconButton, VStack } from '@chakra-ui/react'; -import { generateDefaultValueFromSpec } from '@sunmao-ui/shared'; import { CloseIcon } from '@chakra-ui/icons'; +import { SpecWidget } from '@sunmao-ui/editor-sdk'; +import { generateDefaultValueFromSpec } from '@sunmao-ui/shared'; import { formWrapperCSS } from '../style'; import { EditorServices } from '../../../types'; -import { SpecWidget } from '@sunmao-ui/editor-sdk'; import { genOperation } from '../../../operations'; type Props = { @@ -25,45 +25,21 @@ export const GeneralTraitForm: React.FC = props => { generateDefaultValueFromSpec(tImpl.spec.properties)!, trait.properties ); - const fields = Object.keys(properties || []).map((key: string) => { - const value = trait.properties[key]; - const propertySpec = tImpl.spec.properties.properties?.[key]; - - if (!propertySpec) return undefined; + const onChange = (newValue: any) => { + const operation = genOperation(registry, 'modifyTraitProperty', { + componentId: component.id, + traitIndex: traitIndex, + properties: newValue, + }); - const onChange = (newValue: any) => { - const operation = genOperation(registry, 'modifyTraitProperty', { - componentId: component.id, - traitIndex: traitIndex, - properties: { - [key]: newValue, - }, - }); - - eventBus.send('operation', operation); - }; - - const specObj = propertySpec === true ? {} : propertySpec; - - return ( - - ); - }); + eventBus.send('operation', operation); + }; return ( {trait.type} - {onRemove ? ( + {!tImpl.metadata.isDataSource && onRemove ? ( = props => { /> ) : null} - {fields} + ); }; diff --git a/packages/editor/src/components/ComponentsList/ComponentList.tsx b/packages/editor/src/components/ComponentsList/ComponentList.tsx index b9f510462..588682863 100644 --- a/packages/editor/src/components/ComponentsList/ComponentList.tsx +++ b/packages/editor/src/components/ComponentsList/ComponentList.tsx @@ -12,7 +12,7 @@ import { InputRightElement, Tag, } from '@chakra-ui/react'; -import { CoreComponentName, CORE_VERSION } from '@sunmao-ui/shared'; +import { CORE_VERSION } from '@sunmao-ui/shared'; import { groupBy, sortBy } from 'lodash'; import { EditorServices } from '../../types'; import { ExplorerMenuTabs } from '../../constants/enum'; @@ -66,8 +66,6 @@ const tagStyle = css` white-space: nowrap; `; -const IGNORE_COMPONENTS: string[] = [CoreComponentName.Dummy]; - export const ComponentList: React.FC = ({ services }) => { const { registry, editorStore } = services; const [filterText, setFilterText] = useState(''); @@ -84,10 +82,8 @@ export const ComponentList: React.FC = ({ services }) => { const categories = useMemo(() => { const grouped = groupBy( registry.getAllComponents().filter(c => { - if ( - IGNORE_COMPONENTS.includes(c.metadata.name) || - (checkedVersions.length && !checkedVersions.includes(c.version)) - ) { + if (c.metadata.isDataSource) return false; + if (checkedVersions.length && !checkedVersions.includes(c.version)) { return false; } else if (!filterText) { return true; diff --git a/packages/editor/src/components/DataSource/Api.tsx b/packages/editor/src/components/DataSource/Api.tsx deleted file mode 100644 index f3dc6355f..000000000 --- a/packages/editor/src/components/DataSource/Api.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { - Box, - Text, - Input, - AccordionItem, - AccordionButton, - AccordionIcon, - AccordionPanel, -} from '@chakra-ui/react'; -import { ComponentSchema } from '@sunmao-ui/core'; -import { DataSourceItem } from './DataSourceItem'; -import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; - -const COLOR_MAP = { - GET: 'green', - POST: 'orange', - PUT: 'yellow', - PATCH: 'yellow', - DELETE: 'red', -}; - -interface Props { - apis: ComponentSchema[]; - active: string; - onItemClick: (api: ComponentSchema) => void; - onItemRemove: (api: ComponentSchema) => void; -} - -export const Api: React.FC = props => { - const [search, setSearch] = useState(''); - const { apis, active, onItemClick, onItemRemove } = props; - const list = useMemo( - () => apis.filter(({ id }) => id.includes(search)), - [search, apis] - ); - const ApiItems = () => ( - <> - {list.map(api => { - const trait = api.traits.find(({ type }) => type === `${CORE_VERSION}/${CoreTraitName.Fetch}`); - const properties = trait!.properties; - const method = ( - properties.method as string - ).toUpperCase() as keyof typeof COLOR_MAP; - - return ( - - ); - })} - - ); - - return ( - -

- - - API - - - -

- - { - setSearch(e.target.value); - }} - /> - {list.length ? : No APIs.} - -
- ); -}; diff --git a/packages/editor/src/components/DataSource/ApiForm/ApiForm.tsx b/packages/editor/src/components/DataSource/ApiForm/ApiForm.tsx deleted file mode 100644 index f19f1c1ed..000000000 --- a/packages/editor/src/components/DataSource/ApiForm/ApiForm.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { ComponentSchema } from '@sunmao-ui/core'; -import { watch, FetchTraitPropertiesSpec } from '@sunmao-ui/runtime'; -import { Static, Type } from '@sinclair/typebox'; -import { - Box, - VStack, - HStack, - IconButton, - Text, - Tabs, - TabPanels, - TabPanel, - TabList, - Tab, - Select, - Input, - Button, - CloseButton, -} from '@chakra-ui/react'; -import { ExpressionWidget, WidgetProps } from '@sunmao-ui/editor-sdk'; -import { EditIcon } from '@chakra-ui/icons'; -import { useFormik } from 'formik'; -import { Basic } from './Basic'; -import { Headers as HeadersForm } from './Headers'; -import { Params } from './Params'; -import { Body } from './Body'; -import { Response as ResponseInfo } from './Response'; -import { EditorServices } from '../../../types'; -import { genOperation } from '../../../operations'; -import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; - -enum TabIndex { - Basic, - Headers, - Params, - Body, -} -interface Props { - api: ComponentSchema; - services: EditorServices; - store: Record; - className: string; -} - -const METHODS = ['get', 'post', 'put', 'delete', 'patch']; -const EMPTY_ARRAY: string[] = []; - -export const ApiForm: React.FC = props => { - const { api, services, store, className } = props; - const { editorStore } = services; - const [reactiveStore, setReactiveStore] = useState>({ ...store }); - const [isEditing, setIsEditing] = useState(false); - const [name, setName] = useState(api.id); - const [tabIndex, setTabIndex] = useState(0); - const { registry, eventBus } = services; - const result = useMemo(() => { - return reactiveStore[api.id]?.fetch ?? {}; - }, [api.id, reactiveStore]); - const traitIndex = useMemo( - () => - api.traits.findIndex( - ({ type }) => type === `${CORE_VERSION}/${CoreTraitName.Fetch}` - ), - [api.traits] - ); - const trait = useMemo(() => api.traits[traitIndex], [api.traits, traitIndex]); - const compactOptions = useMemo( - () => ({ - height: '40px', - paddingY: '6px', - }), - [] - ); - const formik = useFormik({ - initialValues: { - ...(trait?.properties as Static), - }, - onSubmit: values => { - eventBus.send( - 'operation', - genOperation(registry, 'modifyTraitProperty', { - componentId: api.id, - traitIndex: traitIndex, - properties: values, - }) - ); - }, - }); - const { values } = formik; - const URLSpec = Type.String({ - widget: 'core/v1/expression', - widgetOptions: { compactOptions }, - }); - - const onFetch = useCallback(async () => { - services.apiService.send('uiMethod', { - componentId: api.id, - name: 'triggerFetch', - parameters: {}, - }); - }, [services.apiService, api]); - const onNameInputBlur = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - - if (value) { - if (value !== api.id) { - editorStore.changeDataSourceName(api, value); - } - setIsEditing(false); - } - }, - [api, editorStore] - ); - const onMethodChange = useCallback( - (e: React.ChangeEvent) => { - formik.handleChange(e); - formik.handleSubmit(); - if (e.target.value === 'get' && tabIndex === TabIndex.Body) { - setTabIndex(0); - } - }, - [formik, tabIndex] - ); - const onURLChange = useCallback( - (value: string) => { - formik.setFieldValue('url', value); - formik.handleSubmit(); - }, - [formik] - ); - const onKeyDown = useCallback((e: React.KeyboardEvent) => { - // prevent form keyboard events to accidentally trigger operation shortcut - e.stopPropagation(); - }, []); - - useEffect(() => { - formik.setValues({ - ...(trait?.properties as Static), - }); - // do not add formik into dependencies, otherwise it will cause infinite loop - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [trait?.properties]); - useEffect(() => { - if (api.id) { - setName(api.id); - setTabIndex(0); - } - }, [api.id]); - useEffect(() => { - const stop = watch(store, newValue => { - setReactiveStore({ ...newValue }); - }); - - return stop; - }, [store]); - - return ( - - - {isEditing ? ( - setName(e.target.value)} - onBlur={onNameInputBlur} - autoFocus - /> - ) : ( - - - {api.id} - - } - aria-label="edit" - size="sm" - variant="ghost" - onClick={() => setIsEditing(true)} - /> - - )} - { - editorStore.setActiveDataSourceId(null); - }} - /> - - - - - - - - - - - { - setTabIndex(index); - }} - > - - - Basic - Headers - Params - {values.method !== 'get' ? Body : null} - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/editor/src/components/DataSource/Data.tsx b/packages/editor/src/components/DataSource/Data.tsx deleted file mode 100644 index 1587632ce..000000000 --- a/packages/editor/src/components/DataSource/Data.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState, useMemo, useEffect } from 'react'; -import { - Box, - Text, - Input, - AccordionItem, - AccordionButton, - AccordionIcon, - AccordionPanel, -} from '@chakra-ui/react'; -import { DataSourceItem } from './DataSourceItem'; -import { EditorServices } from '../../types'; -import { ComponentSchema } from '@sunmao-ui/core'; -import { watch } from '@sunmao-ui/runtime'; - -interface Props { - datas: ComponentSchema[]; - active: string; - title: string; - traitType: string; - filterPlaceholder: string; - emptyPlaceholder: string; - services: EditorServices; - onItemClick: (state: ComponentSchema) => void; - onItemRemove: (state: ComponentSchema) => void; -} - -const STATE_MAP: Record = { - undefined: 'Any', - boolean: 'Boolean', - string: 'String', - number: 'Number', - object: 'Object', -}; - -export const Data: React.FC = props => { - const { - datas = [], - active, - onItemClick, - onItemRemove, - filterPlaceholder, - emptyPlaceholder, - title, - services, - } = props; - const { stateManager } = services; - const { store } = stateManager; - const [search, setSearch] = useState(''); - const [reactiveStore, setReactiveStore] = useState>({...store}); - const list = useMemo( - () => datas.filter(({ id }) => id.includes(search)), - [search, datas] - ); - - useEffect(() => { - const stop = watch(store, newValue => { - setReactiveStore({ ...newValue }); - }); - - return stop; - }, [store]); - - const StateItems = () => ( - <> - {list.map(state => { - return ( - - ); - })} - - ); - - return ( - -

- - - {title} - - - -

- - { - setSearch(e.target.value); - }} - /> - {list.length ? : {emptyPlaceholder}} - -
- ); -}; diff --git a/packages/editor/src/components/DataSource/DataForm/DataForm.tsx b/packages/editor/src/components/DataSource/DataForm/DataForm.tsx deleted file mode 100644 index 690777bcd..000000000 --- a/packages/editor/src/components/DataSource/DataForm/DataForm.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { VStack, FormControl, FormLabel, Input } from '@chakra-ui/react'; -import { ComponentSchema } from '@sunmao-ui/core'; -import { ObjectField, mergeWidgetOptionsIntoSpec } from '@sunmao-ui/editor-sdk'; -import { EditorServices } from '../../../types'; -import { genOperation } from '../../../operations'; -import { css } from '@emotion/css'; - -interface Props { - datasource: ComponentSchema; - services: EditorServices; - traitType: string; -} - -const LabelStyle = css` - font-weight: normal; - font-size: 14px; -`; - -export const DataForm: React.FC = props => { - const { datasource, services, traitType } = props; - const [name, setName] = useState(datasource.id); - const { registry, eventBus, editorStore } = services; - const traitSpec = registry.getTraitByType(traitType); - const traitIndex = datasource.traits.findIndex(({ type }) => type === traitType); - const trait = datasource.traits[traitIndex]; - const onChange = (values: any) => { - eventBus.send( - 'operation', - genOperation(registry, 'modifyTraitProperty', { - componentId: datasource.id, - traitIndex: traitIndex, - properties: { - key: 'value', - ...values, - }, - }) - ); - }; - - const onNameInputBlur = (e: React.ChangeEvent) => { - const value = e.target.value; - - if (value) { - if (value !== datasource.id) { - editorStore.changeDataSourceName(datasource, name); - } - } - }; - const onKeyDown = (e: React.KeyboardEvent) => { - // prevent form keyboard events to accidentally trigger operation shortcut - e.stopPropagation(); - }; - - useEffect(() => { - setName(datasource.id); - }, [datasource.id]); - - return ( - - - - Name - - { - setName(e.target.value); - }} - onBlur={onNameInputBlur} - /> - - (traitSpec.spec.properties, { - ignoreKeys: ['key'], - })} - level={0} - path={[]} - component={datasource} - services={services} - value={trait.properties} - onChange={onChange} - /> - - ); -}; diff --git a/packages/editor/src/components/DataSource/DataForm/index.ts b/packages/editor/src/components/DataSource/DataForm/index.ts deleted file mode 100644 index 150f07c0f..000000000 --- a/packages/editor/src/components/DataSource/DataForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DataForm'; diff --git a/packages/editor/src/components/DataSource/DataSource.tsx b/packages/editor/src/components/DataSource/DataSource.tsx deleted file mode 100644 index ceceaf1c4..000000000 --- a/packages/editor/src/components/DataSource/DataSource.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { - VStack, - Flex, - Spacer, - Text, - Menu, - MenuItem, - MenuButton, - MenuList, - IconButton, - Accordion, -} from '@chakra-ui/react'; -import { AddIcon, ChevronDownIcon } from '@chakra-ui/icons'; -import { ComponentSchema } from '@sunmao-ui/core'; -import { Api } from './Api'; -import { Data } from './Data'; -import { EditorServices } from '../../types'; -import { ToolMenuTabs } from '../../constants/enum'; -import { DataSourceType, DATA_DATASOURCES } from '../../constants/dataSource'; - -interface Props { - active: string; - services: EditorServices; -} - -const DATASOURCE_TYPES = Object.values(DataSourceType); - -export const DataSource: React.FC = props => { - const { active, services } = props; - const { editorStore } = services; - const NORMAL_DATASOURCES = DATA_DATASOURCES.map(item => ({ - ...item, - title: item.type, - datas: editorStore.dataSources[item.type], - })); - const onMenuItemClick = (type: DataSourceType) => { - editorStore.createDataSource( - type, - type === DataSourceType.API ? {} : { key: 'value' } - ); - editorStore.setSelectedComponentId(''); - }; - const onApiItemClick = (api: ComponentSchema) => { - editorStore.setActiveDataSourceId(api.id); - editorStore.setSelectedComponentId(''); - }; - const onDataSourceItemClick = (dataSource: ComponentSchema) => { - editorStore.setActiveDataSourceId(dataSource.id); - editorStore.setToolMenuTab(ToolMenuTabs.INSPECT); - editorStore.setSelectedComponentId(''); - }; - const onApiItemRemove = (api: ComponentSchema) => { - editorStore.removeDataSource(api); - }; - const onStateItemRemove = (state: ComponentSchema) => { - editorStore.removeDataSource(state); - }; - const MenuItems = () => ( - <> - {DATASOURCE_TYPES.map(type => ( - onMenuItemClick(type)}> - {type} - - ))} - - ); - - return ( - - - - DataSource - - - - } - rightIcon={} - /> - - - - - - i + 1))} - allowMultiple - > - - {NORMAL_DATASOURCES.map(dataSourceItem => ( - - ))} - - - ); -}; diff --git a/packages/editor/src/components/DataSource/DataSourceGroup.tsx b/packages/editor/src/components/DataSource/DataSourceGroup.tsx new file mode 100644 index 000000000..1a18c65c6 --- /dev/null +++ b/packages/editor/src/components/DataSource/DataSourceGroup.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { ComponentSchema } from '@sunmao-ui/core'; +import { + Text, + AccordionItem, + AccordionButton, + AccordionIcon, + AccordionPanel, + Tag, + HStack, +} from '@chakra-ui/react'; +import { EditorServices } from '../../types'; +import { ComponentNode } from '../StructureTree/ComponentNode'; + +interface Props { + dataSources: ComponentSchema[]; + title: string; + services: EditorServices; + type: string; +} + +const COLOR_MAP = { + GET: 'green', + POST: 'orange', + PUT: 'yellow', + PATCH: 'yellow', + DELETE: 'red', +}; + +export const DataSourceGroup: React.FC = props => { + const { dataSources = [], title, services, type } = props; + const { editorStore } = services; + + const StateItems = () => ( + <> + {dataSources.map(dataSource => { + let tag = ''; + + const fetchTrait = dataSource.traits.find(({ type }) => type === `core/v1/fetch`); + if (fetchTrait?.properties) { + tag = ((fetchTrait.properties as any)?.method as string)?.toUpperCase(); + } + + return ( + undefined} + shouldShowSelfSlotName={false} + notEmptySlots={[]} + onDragStart={() => undefined} + onDragEnd={() => undefined} + prefix={ + tag ? ( + + {tag} + + ) : undefined + } + /> + ); + })} + + ); + + return ( + + + + {type === 'component' ? C : undefined} + {title} + + + + + {dataSources.length ? : Empty} + + + ); +}; diff --git a/packages/editor/src/components/DataSource/DataSourceItem.tsx b/packages/editor/src/components/DataSource/DataSourceItem.tsx deleted file mode 100644 index 049ce0ef0..000000000 --- a/packages/editor/src/components/DataSource/DataSourceItem.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { HStack, Tag, Text, CloseButton } from '@chakra-ui/react'; -import { ComponentSchema } from '@sunmao-ui/core'; -import { css, cx } from '@emotion/css'; - -const ItemStyle = css` - &:hover { - background: var(--chakra-colors-blue-50); - } -`; -const TextStyle = css` - &.active { - color: var(--chakra-colors-blue-600); - } -`; - -interface Props { - dataSource: ComponentSchema; - tag: string; - name: string; - active?: boolean; - colorMap?: Record; - onItemClick: (dataSource: ComponentSchema) => void; - onItemRemove: (dataSource: ComponentSchema) => void; -} - -export const DataSourceItem: React.FC = props => { - const { - dataSource, - active, - colorMap = {}, - tag, - name, - onItemClick, - onItemRemove, - } = props; - - return ( - - onItemClick(dataSource)} - cursor="pointer" - className={cx(TextStyle, active ? 'active' : '')} - overflow="hidden" - > - {tag} - - {name} - - - onItemRemove(dataSource)} /> - - ); -}; diff --git a/packages/editor/src/components/DataSource/DataSourceList.tsx b/packages/editor/src/components/DataSource/DataSourceList.tsx new file mode 100644 index 000000000..9bdd7e4cb --- /dev/null +++ b/packages/editor/src/components/DataSource/DataSourceList.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useMemo } from 'react'; +import { + VStack, + Spacer, + Text, + Menu, + MenuItem, + MenuButton, + MenuList, + Accordion, + MenuGroup, + HStack, + Button, +} from '@chakra-ui/react'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { DataSourceGroup } from './DataSourceGroup'; +import { EditorServices } from '../../types'; +import { groupBy } from 'lodash'; +import { genOperation } from '../../operations'; +import { generateDefaultValueFromSpec } from '@sunmao-ui/shared'; +import { JSONSchema7 } from 'json-schema'; +import { ToolMenuTabs } from '../../constants/enum'; +import { ComponentSearch } from '../StructureTree/ComponentSearch'; + +interface Props { + services: EditorServices; +} + +export const DataSourceList: React.FC = props => { + const { services } = props; + const { editorStore, eventBus, registry } = services; + const { dataSources, setSelectedComponentId, setToolMenuTab } = editorStore; + const tDataSources = dataSources.filter(ds => ds.type === 'core/v1/dummy'); + const cDataSources = dataSources.filter(ds => ds.type !== 'core/v1/dummy'); + const cdsMap = groupBy(cDataSources, c => c.type); + const tdsMap = groupBy(tDataSources, c => c.traits[0]?.type); + const cdsGroups = Object.keys(cdsMap).map(type => { + return { + title: type, + dataSources: cdsMap[type], + type: 'component', + }; + }); + const tdsGroups = Object.keys(tdsMap).map(type => { + return { + title: type, + dataSources: tdsMap[type], + type: 'trait', + }; + }); + + const dsGroups = cdsGroups.concat(tdsGroups); + + // cdsTypes: component data source types + // tdsTypes: trait data source types + const { cdsTypes, tdsTypes } = useMemo(() => { + const cdsTypes = registry + .getAllComponents() + .filter(c => c.metadata.isDataSource && c.metadata.name !== 'dummy') + .map(c => `${c.version}/${c.metadata.name}`); + const tdsTypes = registry + .getAllTraits() + .filter(t => t.metadata.isDataSource) + .map(t => `${t.version}/${t.metadata.name}`); + return { cdsTypes, tdsTypes }; + }, [registry]); + + const getNewId = useCallback( + (name: string): string => { + let count = dataSources.length; + let id = `${name}${count}`; + const ids = dataSources.map(({ id }) => id); + + while (ids.includes(id)) { + id = `${name}${++count}`; + } + + return `${name}${count}`; + }, + [dataSources] + ); + const onCreateDSFromComponent = useCallback( + (type: string) => { + const name = type.split('/')[2]; + const id = getNewId(name); + + eventBus.send( + 'operation', + genOperation(registry, 'createComponent', { + componentType: type, + componentId: id, + }) + ); + + setSelectedComponentId(id); + setToolMenuTab(ToolMenuTabs.INSPECT); + }, + [eventBus, getNewId, registry, setSelectedComponentId, setToolMenuTab] + ); + const onCreateDSFromTrait = useCallback( + (type: string) => { + const propertiesSpec = registry.getTraitByType(type).spec.properties; + const defaultProperties = generateDefaultValueFromSpec(propertiesSpec, { + genArrayItemDefaults: false, + }); + const name = type.split('/')[2]; + const id = getNewId(name); + + eventBus.send( + 'operation', + genOperation(registry, 'createDataSource', { + id, + type, + defaultProperties: defaultProperties as JSONSchema7, + }) + ); + + setSelectedComponentId(id); + setToolMenuTab(ToolMenuTabs.INSPECT); + }, + [eventBus, getNewId, registry, setSelectedComponentId, setToolMenuTab] + ); + + return ( + + + + DataSources + + + + } + colorScheme="blue" + > + Add + + + {cdsTypes.length ? ( + + {cdsTypes.map(type => ( + onCreateDSFromComponent(type)}> + {type} + + ))} + + ) : undefined} + + {tdsTypes.map(type => ( + onCreateDSFromTrait(type)}> + {type} + + ))} + + + + + setSelectedComponentId(id)} + services={props.services} + /> + i + 1))} + allowMultiple + > + {dsGroups.map(group => ( + + ))} + + + ); +}; diff --git a/packages/editor/src/components/DataSource/index.ts b/packages/editor/src/components/DataSource/index.ts index 25381cb65..6b88ba142 100644 --- a/packages/editor/src/components/DataSource/index.ts +++ b/packages/editor/src/components/DataSource/index.ts @@ -1 +1 @@ -export * from './DataSource'; +export * from './DataSourceList'; diff --git a/packages/editor/src/components/Editor.tsx b/packages/editor/src/components/Editor.tsx index 817abc697..7e620e34c 100644 --- a/packages/editor/src/components/Editor.tsx +++ b/packages/editor/src/components/Editor.tsx @@ -8,17 +8,13 @@ import { ComponentList } from './ComponentsList'; import { EditorHeader } from './EditorHeader'; import { KeyboardEventWrapper } from './KeyboardEventWrapper'; import { StateViewer } from './CodeEditor'; -import { DataSource } from './DataSource'; -import { DataSourceType, DATASOURCE_TRAIT_TYPE_MAP } from '../constants/dataSource'; -import { ApiForm } from './DataSource/ApiForm'; +import { DataSourceList } from './DataSource'; import { ComponentForm } from './ComponentForm'; import ErrorBoundary from './ErrorBoundary'; import { PreviewModal } from './PreviewModal'; import { WarningArea } from './WarningArea'; import { EditorServices } from '../types'; -import { css } from '@emotion/css'; import { EditorMaskWrapper } from './EditorMaskWrapper'; -import { DataForm } from './DataSource/DataForm'; import { Explorer } from './Explorer'; import { Resizable } from 're-resizable'; import { CodeModeModal } from './CodeModeModal'; @@ -36,15 +32,6 @@ type Props = { onRefresh: () => void; }; -const ApiFormStyle = css` - width: 100%; - height: 100%; - position: absolute; - z-index: 1; - top: 0; - left: 0; -`; - export const Editor: React.FC = observer( ({ App, stateStore, services, libs, dependencies, onRefresh: onRefreshApp }) => { const { editorStore } = services; @@ -52,8 +39,6 @@ export const Editor: React.FC = observer( components, selectedComponentId, modules, - activeDataSource, - activeDataSourceType, toolMenuTab, explorerMenuTab, setToolMenuTab, @@ -86,19 +71,7 @@ export const Editor: React.FC = observer( ) : null; }, [App, app, isDisplayApp]); - const inspectForm = useMemo(() => { - if (activeDataSource && activeDataSourceType) { - return activeDataSourceType === DataSourceType.API ? null : ( - - ); - } else { - return ; - } - }, [activeDataSource, services, activeDataSourceType]); + const inspectForm = ; const onRefresh = useCallback(() => { setIsDisplayApp(false); @@ -184,7 +157,7 @@ export const Editor: React.FC = observer( - + @@ -235,15 +208,6 @@ export const Editor: React.FC = observer( - {activeDataSource && activeDataSourceType === DataSourceType.API ? ( - - ) : null} ); diff --git a/packages/editor/src/components/StructureTree/ComponentItemView.tsx b/packages/editor/src/components/StructureTree/ComponentItemView.tsx index 88a6edd2d..bacbbbd6f 100644 --- a/packages/editor/src/components/StructureTree/ComponentItemView.tsx +++ b/packages/editor/src/components/StructureTree/ComponentItemView.tsx @@ -16,6 +16,7 @@ type Props = { onMouseLeave: () => void; paddingLeft: number; actionMenu?: React.ReactNode; + prefix?: React.ReactNode; }; const ChevronWidth = 24; @@ -35,6 +36,7 @@ export const ComponentItemView: React.FC = props => { onMouseLeave, paddingLeft, actionMenu, + prefix, } = props; const [isHover, setIsHover] = useState(false); @@ -117,6 +119,7 @@ export const ComponentItemView: React.FC = props => { paddingLeft={`${paddingLeft + (noChevron ? ChevronWidth : 0)}px`} > {noChevron ? null : expandIcon} + {prefix} void; onDragStart: (id: string) => void; onDragEnd: (id: string) => void; + prefix?: React.ReactNode; }; const ComponentNodeImpl = (props: Props) => { @@ -45,6 +46,7 @@ const ComponentNodeImpl = (props: Props) => { notEmptySlots, onDragStart, onDragEnd, + prefix, } = props; const { registry, eventBus, appModelManager } = services; const slots = Object.keys(registry.getComponentByType(component.type).spec.slots); @@ -210,6 +212,7 @@ const ComponentNodeImpl = (props: Props) => { onMouseLeave={onMouseLeave} paddingLeft={paddingLeft} actionMenu={actionMenu} + prefix={prefix} /> {emptyChildrenSlotsPlaceholder} diff --git a/packages/editor/src/components/StructureTree/ComponentSearch.tsx b/packages/editor/src/components/StructureTree/ComponentSearch.tsx index 7a1b07eb0..11f813efd 100644 --- a/packages/editor/src/components/StructureTree/ComponentSearch.tsx +++ b/packages/editor/src/components/StructureTree/ComponentSearch.tsx @@ -9,7 +9,10 @@ import { } from '@choc-ui/chakra-autocomplete'; import { css } from '@emotion/css'; import { observer } from 'mobx-react-lite'; +import { ComponentSchema } from '@sunmao-ui/core'; type Props = { + components: ComponentSchema[]; + onChange: (id: string) => void; services: EditorServices; }; @@ -19,14 +22,13 @@ const AutoCompleteStyle = css` `; export const ComponentSearch: React.FC = observer(props => { - const { editorStore } = props.services; - const { setSelectedComponentId, components } = editorStore; + const { components, onChange } = props; const onSelectOption = useCallback( ({ item }: { item: Item }) => { - setSelectedComponentId(item.value); + onChange(item.value); }, - [setSelectedComponentId] + [onChange] ); const options = useMemo(() => { diff --git a/packages/editor/src/components/StructureTree/StructureTree.tsx b/packages/editor/src/components/StructureTree/StructureTree.tsx index 9783cfb48..cc95880f0 100644 --- a/packages/editor/src/components/StructureTree/StructureTree.tsx +++ b/packages/editor/src/components/StructureTree/StructureTree.tsx @@ -15,7 +15,7 @@ type Props = { export const StructureTree: React.FC = observer(props => { const { editorStore } = props.services; - const { setSelectedComponentId, selectedComponentId } = editorStore; + const { setSelectedComponentId, selectedComponentId, components } = editorStore; const scrollWrapper = useRef(null); const { @@ -97,7 +97,11 @@ export const StructureTree: React.FC = observer(props => { Components - + setSelectedComponentId(id)} + services={props.services} + /> = {}; const depthMap: Record = {}; - const uiComponents = editorStore.components.filter( - c => c.type !== `${CORE_VERSION}/${CoreComponentName.Dummy}` - ); - const resolvedComponents = resolveApplicationComponents(uiComponents); + const resolvedComponents = resolveApplicationComponents(editorStore.uiComponents); const { topLevelComponents, childrenMap } = resolvedComponents; topLevelComponents.forEach(c => { diff --git a/packages/editor/src/constants/dataSource.ts b/packages/editor/src/constants/dataSource.ts deleted file mode 100644 index ed8eba1a9..000000000 --- a/packages/editor/src/constants/dataSource.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; - - -export enum DataSourceType { - API = 'API', - STATE = 'State', - LOCALSTORAGE = 'LocalStorage', - TRANSFORMER = 'Transformer', -} - -export const DATASOURCE_NAME_MAP = { - [DataSourceType.API]: 'api', - [DataSourceType.STATE]: 'state', - [DataSourceType.LOCALSTORAGE]: 'localStorage', - [DataSourceType.TRANSFORMER]: 'transformer', -}; - -export const DATASOURCE_TRAIT_TYPE_MAP = { - [DataSourceType.API]: `${CORE_VERSION}/${CoreTraitName.Fetch}`, - [DataSourceType.STATE]: `${CORE_VERSION}/${CoreTraitName.State}`, - [DataSourceType.LOCALSTORAGE]: `${CORE_VERSION}/${CoreTraitName.LocalStorage}`, - [DataSourceType.TRANSFORMER]: `${CORE_VERSION}/${CoreTraitName.Transformer}`, -}; - -export const DATA_DATASOURCES = [ - { - type: DataSourceType.STATE, - traitType: DATASOURCE_TRAIT_TYPE_MAP[DataSourceType.STATE], - filterPlaceholder: 'filter the states', - emptyPlaceholder: 'No States.', - }, - { - type: DataSourceType.LOCALSTORAGE, - traitType: DATASOURCE_TRAIT_TYPE_MAP[DataSourceType.LOCALSTORAGE], - filterPlaceholder: 'filter the localStorages', - emptyPlaceholder: 'No LocalStorages.', - }, - { - type: DataSourceType.TRANSFORMER, - traitType: DATASOURCE_TRAIT_TYPE_MAP[DataSourceType.TRANSFORMER], - filterPlaceholder: 'filter the transformers', - emptyPlaceholder: 'No Transformers.', - }, -]; diff --git a/packages/editor/src/constants/index.ts b/packages/editor/src/constants/index.ts index 7c4812f45..5301a1ddc 100644 --- a/packages/editor/src/constants/index.ts +++ b/packages/editor/src/constants/index.ts @@ -2,19 +2,21 @@ import { Application } from '@sunmao-ui/core'; import { ImplementedRuntimeModule } from '@sunmao-ui/runtime'; import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; -export const unremovableTraits = [`${CORE_VERSION}/${CoreTraitName.Slot}`]; +export const unremovableTraits = [ + `${CORE_VERSION}/${CoreTraitName.Slot}`, + `core/v2/${CoreTraitName.Slot}`, +]; export const hideCreateTraitsList = [ `${CORE_VERSION}/${CoreTraitName.Event}`, `${CORE_VERSION}/${CoreTraitName.Style}`, - `${CORE_VERSION}/${CoreTraitName.Fetch}`, `${CORE_VERSION}/${CoreTraitName.Slot}`, + `core/v2/${CoreTraitName.Slot}`, ]; export const hasSpecialFormTraitList = [ `${CORE_VERSION}/${CoreTraitName.Event}`, `${CORE_VERSION}/${CoreTraitName.Style}`, - `${CORE_VERSION}/${CoreTraitName.Fetch}`, ]; export const RootId = '__root__'; diff --git a/packages/editor/src/init.tsx b/packages/editor/src/init.tsx index bd7bd6ddf..18691dd20 100644 --- a/packages/editor/src/init.tsx +++ b/packages/editor/src/init.tsx @@ -92,10 +92,7 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) { editorStore.eleMap = ui.eleMap; const services = { - App, - registry: ui.registry, - apiService: ui.apiService, - stateManager, + ...ui, appModelManager, widgetManager, eventBus, diff --git a/packages/editor/src/operations/branch/datasource/createDataSourceBranchOperation.ts b/packages/editor/src/operations/branch/datasource/createDataSourceBranchOperation.ts index 1ff1ac0a3..0c8ea3a3b 100644 --- a/packages/editor/src/operations/branch/datasource/createDataSourceBranchOperation.ts +++ b/packages/editor/src/operations/branch/datasource/createDataSourceBranchOperation.ts @@ -2,7 +2,6 @@ import { AppModel } from '../../../AppModel/AppModel'; import { BaseBranchOperation } from '../../type'; import { CreateComponentBranchOperation } from '../index'; import { CreateTraitLeafOperation } from '../../leaf'; -import { DataSourceType, DATASOURCE_TRAIT_TYPE_MAP } from '../../../constants/dataSource'; import { generateDefaultValueFromSpec, CORE_VERSION, @@ -12,18 +11,17 @@ import { JSONSchema7Object } from 'json-schema'; export type CreateDataSourceBranchOperationContext = { id: string; - type: DataSourceType; - defaultProperties: Record; + type: string; + defaultProperties?: Record; }; export class CreateDataSourceBranchOperation extends BaseBranchOperation { do(prev: AppModel): AppModel { - const { id, type, defaultProperties = {} } = this.context; - const traitType = DATASOURCE_TRAIT_TYPE_MAP[type]; - const traitSpec = this.registry.getTraitByType(traitType).spec; - const initProperties = generateDefaultValueFromSpec( - traitSpec.properties - ) as JSONSchema7Object; + const { id, type, defaultProperties } = this.context; + const traitSpec = this.registry.getTraitByType(type).spec; + const initProperties = generateDefaultValueFromSpec(traitSpec.properties, { + genArrayItemDefaults: true, + }) as JSONSchema7Object; this.operationStack.insert( new CreateComponentBranchOperation(this.registry, { @@ -34,14 +32,8 @@ export class CreateDataSourceBranchOperation extends BaseBranchOperation = {}; constructor( private eventBus: EventBusType, @@ -118,7 +110,6 @@ export class EditorStore { () => { if (this.selectedComponentId) { this.setToolMenuTab(ToolMenuTabs.INSPECT); - this.setActiveDataSourceId(null); } } ); @@ -157,6 +148,11 @@ export class EditorStore { return this._selectedComponentId; } + get selectedComponentIsDataSource() { + if (!this.selectedComponent) return false; + return !!this.isDataSourceTypeCache[this.selectedComponent.type]; + } + get dragOverComponentId() { return this._dragOverComponentId; } @@ -182,47 +178,28 @@ export class EditorStore { } } - get dataSources(): Record { - const dataSources: Record = {}; - - this.components.forEach(component => { - if (component.type === `${CORE_VERSION}/${CoreComponentName.Dummy}`) { - component.traits.forEach(trait => { - Object.entries(DATASOURCE_TRAIT_TYPE_MAP).forEach( - ([dataSourceType, traitType]) => { - if (trait.type === traitType) { - dataSources[dataSourceType] = (dataSources[dataSourceType] || []).concat( - component - ); - } - } - ); - }); + get uiComponents(): ComponentSchema[] { + return this.components.filter(component => { + if (this.isDataSourceTypeCache[component.type]) return false; + const spec = this.registry.getComponentByType(component.type); + if (spec.metadata.isDataSource) { + this.isDataSourceTypeCache[component.type] = true; + return false; } + return true; }); - - return dataSources; } - get activeDataSource(): ComponentSchema | null { - return ( - this.components.find(component => component.id === this.activeDataSourceId) || null - ); - } - - get activeDataSourceType(): DataSourceType | null { - for (const trait of this.activeDataSource?.traits || []) { - const [dataSourceType] = - Object.entries(DATASOURCE_TRAIT_TYPE_MAP).find( - ([, traitType]) => trait.type === traitType - ) || []; - - if (dataSourceType) { - return dataSourceType as DataSourceType; + get dataSources(): ComponentSchema[] { + return this.components.filter(component => { + if (this.isDataSourceTypeCache[component.type]) return true; + const spec = this.registry.getComponentByType(component.type); + if (spec.metadata.isDataSource) { + this.isDataSourceTypeCache[component.type] = true; + return true; } - } - - return null; + return false; + }); } clearSunmaoGlobalState() { @@ -287,82 +264,10 @@ export class EditorStore { this.lastSavedComponentsVersion = val; }; - setActiveDataSourceId = (dataSourceId: string | null) => { - this.activeDataSourceId = dataSourceId; - }; - setValidateResult = (validateResult: ValidateErrorResult[]) => { this.validateResult = validateResult; }; - createDataSource = ( - type: DataSourceType, - defaultProperties: Record = {} - ) => { - const getCount = ( - dataSources: ComponentSchema[] = [], - dataSourceName = '' - ): number => { - let count = dataSources.length; - let id = `${dataSourceName}${count}`; - const ids = dataSources.map(({ id }) => id); - - while (ids.includes(id)) { - id = `${dataSourceName}${++count}`; - } - - return count; - }; - - const id = `${DATASOURCE_NAME_MAP[type]}${getCount( - this.dataSources[type], - DATASOURCE_NAME_MAP[type] - )}`; - - this.eventBus.send( - 'operation', - genOperation(this.registry, 'createDataSource', { - id, - type, - defaultProperties, - }) - ); - - const component = this.components.find(({ id: componentId }) => id === componentId); - - this.setActiveDataSourceId(component!.id); - - if (type === DataSourceType.STATE || type === DataSourceType.LOCALSTORAGE) { - this.setToolMenuTab(ToolMenuTabs.INSPECT); - } - }; - - removeDataSource = (dataSource: ComponentSchema) => { - this.eventBus.send( - 'operation', - genOperation(this.registry, 'removeComponent', { - componentId: dataSource.id, - }) - ); - if (this.activeDataSource?.id === dataSource.id) { - this.setActiveDataSourceId(null); - } - }; - - changeDataSourceName = (dataSource: ComponentSchema, name: string) => { - this.eventBus.send( - 'operation', - genOperation(this.registry, 'modifyComponentId', { - componentId: dataSource.id, - newId: name, - }) - ); - - const component = this.components.find(({ id: componentId }) => componentId === name); - - this.setActiveDataSourceId(component!.id); - }; - setExplorerMenuTab = (val: ExplorerMenuTabs) => { this.explorerMenuTab = val; }; diff --git a/packages/editor/src/types.ts b/packages/editor/src/types.ts index 059fd7254..24c7b9795 100644 --- a/packages/editor/src/types.ts +++ b/packages/editor/src/types.ts @@ -1,24 +1,14 @@ import { Application, Module } from '@sunmao-ui/core'; -import { - initSunmaoUI, - RegistryInterface, - StateManagerInterface, -} from '@sunmao-ui/runtime'; +import { UIServices } from '@sunmao-ui/runtime'; import { WidgetManager } from '@sunmao-ui/editor-sdk'; import { EditorStore } from './services/EditorStore'; -import { EventBusType } from './services/eventBus'; import { AppModelManager } from './operations/AppModelManager'; +import { EventBusType } from './services/eventBus'; -type ReturnOfInit = ReturnType; - -export type EditorServices = { - App: ReturnOfInit['App']; - registry: RegistryInterface; - apiService: ReturnOfInit['apiService']; - stateManager: StateManagerInterface; +export type EditorServices = UIServices & { + eventBus: EventBusType; appModelManager: AppModelManager; widgetManager: WidgetManager; - eventBus: EventBusType; editorStore: EditorStore; }; diff --git a/packages/editor/src/utils/resolveApplicationComponents.ts b/packages/editor/src/utils/resolveApplicationComponents.ts index f2eddac68..8b0334698 100644 --- a/packages/editor/src/utils/resolveApplicationComponents.ts +++ b/packages/editor/src/utils/resolveApplicationComponents.ts @@ -11,7 +11,11 @@ export function resolveApplicationComponents(components: ComponentSchema[]): { const topLevelComponents: ComponentSchema[] = []; const childrenMap: ChildrenMap = new Map(); components.forEach(c => { - const slotTrait = c.traits.find(t => t.type === `${CORE_VERSION}/${CoreTraitName.Slot}`); + const slotTrait = c.traits.find( + t => + t.type === `${CORE_VERSION}/${CoreTraitName.Slot}` || + t.type === `core/v2/${CoreTraitName.Slot}` + ); if (slotTrait) { const { id: parentId, slot } = slotTrait.properties.container as any; if (!childrenMap.has(parentId)) { @@ -28,6 +32,6 @@ export function resolveApplicationComponents(components: ComponentSchema[]): { }); return { topLevelComponents, - childrenMap + childrenMap, }; } diff --git a/packages/runtime/src/components/core/Dummy.tsx b/packages/runtime/src/components/core/Dummy.tsx index a0e9c0c74..fdf839433 100644 --- a/packages/runtime/src/components/core/Dummy.tsx +++ b/packages/runtime/src/components/core/Dummy.tsx @@ -12,6 +12,7 @@ export default implementRuntimeComponent({ annotations: { category: 'Advance', }, + isDataSource: true, }, spec: { properties: Type.Object({}), diff --git a/packages/runtime/src/components/core/List.tsx b/packages/runtime/src/components/core/List.tsx index 5944fb3f0..da3dbcc9f 100644 --- a/packages/runtime/src/components/core/List.tsx +++ b/packages/runtime/src/components/core/List.tsx @@ -51,7 +51,8 @@ export default implementRuntimeComponent({ const childrenSchema = app.spec.components.filter(c => { return c.traits.find( t => - t.type === 'core/v1/slot' && (t.properties.container as any).id === component.id + (t.type === 'core/v1/slot' || t.type === 'core/v2/slot') && + (t.properties.container as any).id === component.id ); }); diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index d8a513593..f34a4b190 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -61,6 +61,7 @@ export function initSunmaoUI(props: SunmaoUIRuntimeProps = {}) { globalHandlerMap, apiService, eleMap, + slotReceiver }; } diff --git a/packages/runtime/src/services/Registry.tsx b/packages/runtime/src/services/Registry.tsx index b39971c97..851aa65dd 100644 --- a/packages/runtime/src/services/Registry.tsx +++ b/packages/runtime/src/services/Registry.tsx @@ -16,6 +16,7 @@ import CoreArrayState from '../traits/core/ArrayState'; import CoreState from '../traits/core/State'; import CoreEvent from '../traits/core/Event'; import CoreSlot from '../traits/core/Slot'; +import CoreSlotV2 from '../traits/core/SlotV2'; import CoreStyle from '../traits/core/Style'; import CoreHidden from '../traits/core/Hidden'; import CoreFetch from '../traits/core/Fetch'; @@ -259,6 +260,7 @@ export function initRegistry( registry.registerTrait(CoreArrayState); registry.registerTrait(CoreEvent); registry.registerTrait(CoreSlot); + registry.registerTrait(CoreSlotV2); registry.registerTrait(CoreStyle); registry.registerTrait(CoreHidden); registry.registerTrait(CoreFetch); diff --git a/packages/runtime/src/traits/core/Fetch.tsx b/packages/runtime/src/traits/core/Fetch.tsx index c6963860c..ae5331480 100644 --- a/packages/runtime/src/traits/core/Fetch.tsx +++ b/packages/runtime/src/traits/core/Fetch.tsx @@ -10,44 +10,53 @@ import { import { runEventHandler } from '../../utils/runEventHandler'; import { implementRuntimeTrait } from '../../utils/buildKit'; -export const FetchTraitPropertiesSpec = Type.Object({ - url: Type.String({ title: 'URL' }), // {format:uri}?; - method: Type.KeyOf( - Type.Object({ - get: Type.String(), - post: Type.String(), - put: Type.String(), - delete: Type.String(), - patch: Type.String(), +export const FetchTraitPropertiesSpec = Type.Object( + { + url: Type.String({ title: 'URL' }), // {format:uri}?; + method: Type.KeyOf( + Type.Object({ + get: Type.String(), + post: Type.String(), + put: Type.String(), + delete: Type.String(), + patch: Type.String(), + }), + { title: 'Method' } + ), // {pattern: /^(get|post|put|delete)$/i} + lazy: Type.Boolean({ title: 'Lazy' }), + disabled: Type.Boolean({ title: 'Disabled' }), + headers: Type.Record(Type.String(), Type.String(), { + title: 'Headers', }), - { title: 'Method' } - ), // {pattern: /^(get|post|put|delete)$/i} - lazy: Type.Boolean({ title: 'Lazy' }), - disabled: Type.Boolean({ title: 'Disabled' }), - headers: Type.Record(Type.String(), Type.String(), { - title: 'Headers', - }), - body: Type.Record(Type.String(), Type.Any(), { - title: 'Body', - widget: `${CORE_VERSION}/${CoreWidgetName.RecordField}`, - }), - bodyType: Type.KeyOf( - Type.Object({ - json: Type.String(), - formData: Type.String(), - raw: Type.String(), + body: Type.Record(Type.String(), Type.Any(), { + title: 'Body', + widget: `${CORE_VERSION}/${CoreWidgetName.RecordField}`, }), - { title: 'Body Type' } - ), - onComplete: Type.Array(EventCallBackHandlerSpec), - onError: Type.Array(EventCallBackHandlerSpec), -}); + bodyType: Type.KeyOf( + Type.Object({ + json: Type.String(), + formData: Type.String(), + raw: Type.String(), + }), + { title: 'Body Type' } + ), + onComplete: Type.Array(EventCallBackHandlerSpec), + onError: Type.Array(EventCallBackHandlerSpec), + }, + { + widget: 'core/v1/fetch', + widgetOptions: { + isDisplayLabel: false, + }, + } +); export default implementRuntimeTrait({ version: CORE_VERSION, metadata: { name: CoreTraitName.Fetch, description: 'fetch data to store', + isDataSource: true, }, spec: { properties: FetchTraitPropertiesSpec, @@ -99,10 +108,9 @@ export default implementRuntimeTrait({ headers.append(key, _headers[key]); } } - mergeState({ fetch: { - ...(services.stateManager.store[componentId].fetch || {}), + ...(services.stateManager.store[componentId]?.fetch || {}), code: undefined, codeText: '', loading: true, diff --git a/packages/runtime/src/traits/core/LocalStorage.tsx b/packages/runtime/src/traits/core/LocalStorage.tsx index fa726bf6a..6afaf0f80 100644 --- a/packages/runtime/src/traits/core/LocalStorage.tsx +++ b/packages/runtime/src/traits/core/LocalStorage.tsx @@ -28,6 +28,7 @@ export default implementRuntimeTrait({ metadata: { name: CoreTraitName.LocalStorage, description: 'localStorage trait', + isDataSource: true, }, spec: { properties: LocalStorageTraitPropertiesSpec, diff --git a/packages/runtime/src/traits/core/SlotV2.tsx b/packages/runtime/src/traits/core/SlotV2.tsx new file mode 100644 index 000000000..a9ce7796e --- /dev/null +++ b/packages/runtime/src/traits/core/SlotV2.tsx @@ -0,0 +1,38 @@ +import { Type } from '@sinclair/typebox'; +import { CoreTraitName } from '@sunmao-ui/shared'; +import { implementRuntimeTrait } from '../../utils/buildKit'; + +const ContainerPropertySpec = Type.Object( + { + id: Type.String({ isComponentId: true }), + slot: Type.String(), + }, + // don't show this property in the editor + { widgetOptions: { isHidden: true } } +); + +export const PropsSpec = Type.Object({ + container: ContainerPropertySpec, + ifCondition: Type.Boolean(), +}); + +export default implementRuntimeTrait({ + version: 'core/v2', + metadata: { + name: CoreTraitName.Slot, + description: 'nested components by slots', + annotations: { + beforeRender: true, + }, + }, + spec: { + properties: PropsSpec, + state: Type.Object({}), + methods: [], + }, +})(() => { + return ({ ifCondition }) => ({ + props: null, + unmount: !ifCondition, + }); +}); diff --git a/packages/runtime/src/traits/core/State.tsx b/packages/runtime/src/traits/core/State.tsx index 4c29541fd..f108bddd3 100644 --- a/packages/runtime/src/traits/core/State.tsx +++ b/packages/runtime/src/traits/core/State.tsx @@ -18,6 +18,7 @@ export default implementRuntimeTrait({ metadata: { name: CoreTraitName.State, description: 'add state to component', + isDataSource: true, }, spec: { properties: StateTraitPropertiesSpec, diff --git a/packages/runtime/src/traits/core/Transformer.tsx b/packages/runtime/src/traits/core/Transformer.tsx index cf775d395..68f5b0103 100644 --- a/packages/runtime/src/traits/core/Transformer.tsx +++ b/packages/runtime/src/traits/core/Transformer.tsx @@ -16,6 +16,7 @@ export default implementRuntimeTrait({ metadata: { name: CoreTraitName.Transformer, description: 'transform the value', + isDataSource: true, }, spec: { properties: TransformerTraitPropertiesSpec,