feat(runtime): support Component DataSource

This commit is contained in:
Bowen Tan 2022-11-24 16:12:03 +08:00
parent 7a8656574f
commit bac129037b
53 changed files with 800 additions and 1149 deletions

View File

@ -7,6 +7,7 @@ export type Metadata<
annotations?: Record<string, any> & TAnnotations;
exampleProperties?: TExample;
deprecated?: boolean;
isDataSource?: boolean;
};
type ComponentCategory =

View File

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

View File

@ -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<typeof FetchTraitPropertiesSpec>;
component: ComponentSchema;
services: EditorServicesInterface;
onChange: (value: Static<typeof FetchTraitPropertiesSpec>) => 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> = props => {
const { value, onChange, component, services } = props;
const [tabIndex, setTabIndex] = useState(0);
const [fetchResult, setFetchResult] = useState<FetchResultType | undefined>();
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<HTMLSelectElement>) => {
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 (
<VStack
backgroundColor="#fff"
padding="4"
paddingBottom="0"
align="stretch"
spacing="4"
height="100%"
onKeyDown={onKeyDown}
>
<Text
title={component.id}
fontSize="lg"
fontWeight="bold"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{component.id}
</Text>
<HStack spacing={1} flex="0 1 auto" alignItems="start">
<Select
width={200}
name="method"
value={values.method}
onChange={onMethodChange}
size="md"
>
{METHODS.map(method => (
<option key={method} value={method}>
{method.toLocaleUpperCase()}
</option>
))}
</Select>
<Box width="0" flex="1">
<ExpressionWidget
component={component}
spec={URLSpec}
value={values.url}
path={EMPTY_ARRAY}
level={1}
services={services}
onChange={onURLChange}
/>
</Box>
<Button colorScheme="blue" isLoading={fetchResult?.loading} onClick={onFetch}>
Run
</Button>
</HStack>
<Tabs
flex="1 1 0"
overflow="hidden"
index={tabIndex}
onChange={index => {
setTabIndex(index);
}}
>
<VStack height="100%" alignItems="stretch">
<TabList>
<Tab>Basic</Tab>
<Tab>Headers</Tab>
<Tab>Params</Tab>
{values.method !== 'get' ? <Tab>Body</Tab> : null}
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<Basic api={component} formik={formik} services={services} />
</TabPanel>
<TabPanel>
<HeadersForm
api={component}
spec={FetchTraitPropertiesSpec.properties.headers as WidgetProps['spec']}
services={services}
formik={formik}
/>
</TabPanel>
<TabPanel>
<Params api={component} services={services} formik={formik} />
</TabPanel>
<TabPanel>
<Body
api={component}
spec={FetchTraitPropertiesSpec.properties.body as WidgetProps['spec']}
services={services}
formik={formik}
/>
</TabPanel>
</TabPanels>
</VStack>
</Tabs>
<ResponseInfo {...fetchResult} />
</VStack>
);
};

View File

@ -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<typeof FetchTraitPropertiesSpec>;
interface Props {
api: ComponentSchema;
formik: FormikHelpers<Values> & FormikHandlers & FormikState<Values>;
services: EditorServices;
services: EditorServicesInterface;
}
const DisabledSpec = Type.Boolean({

View File

@ -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<typeof FetchTraitPropertiesSpec>;
interface Props {
api: ComponentSchema;
spec: WidgetProps['spec'];
formik: FormikHelpers<Values> & FormikHandlers & FormikState<Values>;
services: EditorServices;
services: EditorServicesInterface;
}
const EMPTY_ARRAY: string[] = [];

View File

@ -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<typeof FetchTraitPropertiesSpec>;
interface Props {
api: ComponentSchema;
spec: WidgetProps['spec'];
formik: FormikHelpers<Values> & FormikHandlers & FormikState<Values>;
services: EditorServices;
services: EditorServicesInterface;
}
const EMPTY_ARRAY: string[] = [];

View File

@ -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<typeof FetchTraitPropertiesSpec>;
interface Props {
api: ComponentSchema;
formik: FormikHelpers<Values> & FormikHandlers & FormikState<Values>;
services: EditorServices;
services: EditorServicesInterface;
}
const EMPTY_ARRAY: string[] = [];

View File

@ -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> = 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 ? (
<Accordion
onChange={i => setIsOpen(i === 0)}
allowToggle
@ -52,7 +51,7 @@ export const Response: React.FC<Props> = props => {
<AccordionButton>
<HStack flex="1" textAlign="left" spacing={2}>
<span>Response</span>
{props.data || props.error ? (
{props.data || props.error || props.codeText ? (
<Tag colorScheme={CODE_MAP[String(props.code)[0]] || 'red'}>
{props.code} {(props.codeText || '').toLocaleUpperCase()}
</Tag>
@ -62,22 +61,20 @@ export const Response: React.FC<Props> = props => {
</AccordionButton>
</h2>
<AccordionPanel pb={4} padding={0} height="250px">
<Flex alignItems="center" justifyContent="center" height="100%">
<Flex alignItems="center" justifyContent="center" height="100%" overflow="auto">
{props.loading || !isOpen ? (
<Spinner />
) : (
<CodeEditor
<pre
className={css`
width: 100%;
height: 100%;
width: 100%;
overflow: auto;
padding: 0 20px;
`}
mode={{
name: 'javascript',
json: true,
}}
defaultCode={error || data}
readOnly
/>
>
<code>{error || data}</code>
</pre>
)}
</Flex>
</AccordionPanel>

View File

@ -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<WidgetProps<FetchWidgetType>> = props => {
const { value, onChange, component, services } = props;
const [isOpen, setIsOpen] = useState(true);
return (
<Box>
<Button onClick={() => setIsOpen(true)}>Edit In Modal</Button>
<Modal onClose={() => setIsOpen(false)} isOpen={isOpen}>
<ModalContent height="75vh" minWidth="75vw">
<ModalCloseButton />
<ModalBody>
<ApiForm
value={value}
onChange={onChange}
component={component}
services={services}
/>
</ModalBody>
</ModalContent>
</Modal>
</Box>
);
};
export default implementWidget<FetchWidgetType>({
version: CORE_VERSION,
metadata: {
name: 'fetch',
},
})(FetchWidget);

View File

@ -187,6 +187,7 @@ export const SchemaFieldWidgetOptions = Type.Object({
type SpecFieldWidgetType = `${typeof CORE_VERSION}/${CoreWidgetName.Spec}`;
type Props = WidgetProps<SpecFieldWidgetType> & {
hideCategory?: boolean;
children?:
| (React.ReactNode & {
title?: any;
@ -201,7 +202,17 @@ declare module '../../types/widget' {
}
export const SpecWidget: React.FC<Props> = 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> = props => {
Component = ExpressionWidget;
} else if (widget) {
Component = widget.impl;
} else if (level === 0) {
} else if (level === 0 && !hideCategory) {
Component = CategoryWidget;
showAsideExpressionButton = false;
}

View File

@ -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<any>[] = [
recordWidgetSpec,
eventWidgetSpec,
popoverWidgetSpec,
fetchWidgetSpec,
sizeWidgetSpec,
fontWidgetSpec,
colorWidgetSpec,

View File

@ -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<string, any>;
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<string, any>;
deepEval: (value: any, options?: EvalOptions) => any;
};
widgetManager: WidgetManager;
}

View File

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

View File

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

View File

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

View File

@ -28,7 +28,8 @@ type Props = {
export const ComponentForm: React.FC<Props> = observer(props => {
const { services } = props;
const { editorStore, registry, eventBus } = services;
const { selectedComponent, selectedComponentId } = editorStore;
const { selectedComponent, selectedComponentId, selectedComponentIsDataSource } =
editorStore;
if (!selectedComponentId) {
return (
<Text p={2} fontSize="md">
@ -109,6 +110,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
/>
</VStack>
),
hide: Object.keys(cImpl.spec.properties.properties).length === 0,
},
{
title: 'Events',
@ -123,6 +125,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
services={services}
/>
),
hide: selectedComponentIsDataSource,
},
{
title: 'Traits',
@ -142,15 +145,18 @@ export const ComponentForm: React.FC<Props> = observer(props => {
allowMultiple
onKeyDown={onKeyDown}
>
{sections.map((section, i) => (
<FormSection
style={{ position: 'relative', zIndex: sections.length - i }}
title={section.title}
key={`${section.title}-${selectedComponentId}`}
>
{section.node}
</FormSection>
))}
{sections.map((section, i) => {
if (section.hide) return undefined;
return (
<FormSection
style={{ position: 'relative', zIndex: sections.length - i }}
title={section.title}
key={`${section.title}-${selectedComponentId}`}
>
{section.node}
</FormSection>
);
})}
</Accordion>
</ErrorBoundary>
);

View File

@ -67,7 +67,7 @@ export const AddTraitButton: React.FC<Props> = props => {
Add Trait
</MenuButton>
<Portal containerRef={containerRef}>
<MenuList>{menuItems}</MenuList>
<MenuList zIndex={100}>{menuItems}</MenuList>
</Portal>
</Menu>
</Box>

View File

@ -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> = 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];
const onChange = (newValue: any) => {
const operation = genOperation(registry, 'modifyTraitProperty', {
componentId: component.id,
traitIndex: traitIndex,
properties: newValue,
});
if (!propertySpec) return undefined;
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 (
<SpecWidget
key={key}
level={1}
path={[key]}
spec={{ ...specObj, title: specObj.title || key }}
value={value}
services={services}
component={component}
onChange={onChange}
/>
);
});
eventBus.send('operation', operation);
};
return (
<VStack key={trait.type} className={formWrapperCSS}>
<HStack width="full" justifyContent="space-between">
<strong>{trait.type}</strong>
{onRemove ? (
{!tImpl.metadata.isDataSource && onRemove ? (
<IconButton
aria-label="remove trait"
variant="ghost"
@ -74,7 +50,17 @@ export const GeneralTraitForm: React.FC<Props> = props => {
/>
) : null}
</HStack>
{fields}
<SpecWidget
level={0}
path={[]}
// don't show category in trait form
hideCategory
spec={tImpl.spec.properties}
value={properties}
services={services}
component={component}
onChange={onChange}
/>
</VStack>
);
};

View File

@ -12,7 +12,6 @@ import {
InputRightElement,
Tag,
} from '@chakra-ui/react';
import { CoreComponentName } from '@sunmao-ui/shared';
import { groupBy, sortBy } from 'lodash';
import { EditorServices } from '../../types';
import { ExplorerMenuTabs } from '../../constants/enum';
@ -66,8 +65,6 @@ const tagStyle = css`
white-space: nowrap;
`;
const IGNORE_COMPONENTS: string[] = [CoreComponentName.Dummy];
export const ComponentList: React.FC<Props> = ({ services }) => {
const { registry, editorStore } = services;
const [filterText, setFilterText] = useState('');
@ -85,7 +82,7 @@ export const ComponentList: React.FC<Props> = ({ services }) => {
const grouped = groupBy(
registry.getAllComponents().filter(c => {
if (
IGNORE_COMPONENTS.includes(c.metadata.name) ||
c.metadata.isDataSource ||
c.metadata.deprecated ||
(checkedVersions.length && !checkedVersions.includes(c.version))
) {

View File

@ -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> = 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 (
<DataSourceItem
key={api.id}
dataSource={api}
tag={method}
name={api.id}
active={active === api.id}
colorMap={COLOR_MAP}
onItemClick={onItemClick}
onItemRemove={onItemRemove}
/>
);
})}
</>
);
return (
<AccordionItem>
<h2>
<AccordionButton borderBottom="solid" borderColor="inherit">
<Box flex="1" textAlign="left">
API
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="4" padding="0">
<Input
placeholder="filter the apis"
value={search}
onChange={e => {
setSearch(e.target.value);
}}
/>
{list.length ? <ApiItems /> : <Text padding="2">No APIs.</Text>}
</AccordionPanel>
</AccordionItem>
);
};

View File

@ -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<string, any>;
className: string;
}
const METHODS = ['get', 'post', 'put', 'delete', 'patch'];
const EMPTY_ARRAY: string[] = [];
export const ApiForm: React.FC<Props> = props => {
const { api, services, store, className } = props;
const { editorStore } = services;
const [reactiveStore, setReactiveStore] = useState<Record<string, any>>({ ...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<typeof FetchTraitPropertiesSpec>),
},
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<HTMLInputElement>) => {
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<HTMLSelectElement>) => {
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<typeof FetchTraitPropertiesSpec>),
});
// 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 (
<VStack
className={className}
backgroundColor="#fff"
padding="4"
paddingBottom="0"
align="stretch"
spacing="4"
onKeyDown={onKeyDown}
>
<HStack
alignItems="center"
justifyContent="space-between"
maxWidth="100%"
spacing={46}
>
{isEditing ? (
<Input
value={name}
onChange={e => setName(e.target.value)}
onBlur={onNameInputBlur}
autoFocus
/>
) : (
<HStack alignItems="center" flex="1" overflow="hidden">
<Text
title={api.id}
fontSize="lg"
fontWeight="bold"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{api.id}
</Text>
<IconButton
icon={<EditIcon />}
aria-label="edit"
size="sm"
variant="ghost"
onClick={() => setIsEditing(true)}
/>
</HStack>
)}
<CloseButton
onClick={() => {
editorStore.setActiveDataSourceId(null);
}}
/>
</HStack>
<HStack display="flex" spacing={4}>
<HStack display="flex" spacing={1} flex={1} alignItems="stretch">
<Select
width={200}
name="method"
value={values.method}
onChange={onMethodChange}
size="md"
>
{METHODS.map(method => (
<option key={method} value={method}>
{method.toLocaleUpperCase()}
</option>
))}
</Select>
<Box flex={1}>
<ExpressionWidget
component={api}
spec={URLSpec}
value={values.url}
path={EMPTY_ARRAY}
level={1}
services={services}
onChange={onURLChange}
/>
</Box>
</HStack>
<Button colorScheme="blue" isLoading={result.loading} onClick={onFetch}>
Run
</Button>
</HStack>
<Tabs
flex={1}
overflow="hidden"
index={tabIndex}
onChange={index => {
setTabIndex(index);
}}
>
<VStack height="100%" alignItems="stretch">
<TabList>
<Tab>Basic</Tab>
<Tab>Headers</Tab>
<Tab>Params</Tab>
{values.method !== 'get' ? <Tab>Body</Tab> : null}
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<Basic api={api} formik={formik} services={services} />
</TabPanel>
<TabPanel>
<HeadersForm
api={api}
spec={FetchTraitPropertiesSpec.properties.headers as WidgetProps['spec']}
services={services}
formik={formik}
/>
</TabPanel>
<TabPanel>
<Params api={api} services={services} formik={formik} />
</TabPanel>
<TabPanel>
<Body
api={api}
spec={FetchTraitPropertiesSpec.properties.body as WidgetProps['spec']}
services={services}
formik={formik}
/>
</TabPanel>
</TabPanels>
</VStack>
</Tabs>
<ResponseInfo {...result} />
</VStack>
);
};

View File

@ -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<string, string> = {
undefined: 'Any',
boolean: 'Boolean',
string: 'String',
number: 'Number',
object: 'Object',
};
export const Data: React.FC<Props> = 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<Record<string, any>>({...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 (
<DataSourceItem
key={state.id}
dataSource={state}
tag={
Array.isArray(reactiveStore[state.id]?.value)
? 'Array'
: STATE_MAP[typeof reactiveStore[state.id]?.value] ?? 'Any'
}
name={state.id}
active={active === state.id}
onItemClick={onItemClick}
onItemRemove={onItemRemove}
/>
);
})}
</>
);
return (
<AccordionItem>
<h2>
<AccordionButton borderBottom="solid" borderColor="inherit">
<Box flex="1" textAlign="left">
{title}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="4" padding="0">
<Input
placeholder={filterPlaceholder}
value={search}
onChange={e => {
setSearch(e.target.value);
}}
/>
{list.length ? <StateItems /> : <Text padding="2">{emptyPlaceholder}</Text>}
</AccordionPanel>
</AccordionItem>
);
};

View File

@ -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> = 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<HTMLInputElement>) => {
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 (
<VStack
p="2"
spacing="2"
background="gray.50"
alignItems="stretch"
onKeyDown={onKeyDown}
>
<FormControl>
<FormLabel>
<span className={LabelStyle}>Name</span>
</FormLabel>
<Input
value={name}
onChange={e => {
setName(e.target.value);
}}
onBlur={onNameInputBlur}
/>
</FormControl>
<ObjectField
spec={mergeWidgetOptionsIntoSpec<'core/v1/object'>(traitSpec.spec.properties, {
ignoreKeys: ['key'],
})}
level={0}
path={[]}
component={datasource}
services={services}
value={trait.properties}
onChange={onChange}
/>
</VStack>
);
};

View File

@ -1 +0,0 @@
export * from './DataForm';

View File

@ -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> = 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 => (
<MenuItem key={type} onClick={() => onMenuItemClick(type)}>
{type}
</MenuItem>
))}
</>
);
return (
<VStack spacing="2" alignItems="stretch">
<Flex padding="4" paddingBottom="0">
<Text fontSize="lg" fontWeight="bold">
DataSource
</Text>
<Spacer />
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label="add event"
size="sm"
variant="ghost"
colorScheme="blue"
icon={<AddIcon />}
rightIcon={<ChevronDownIcon />}
/>
<MenuList>
<MenuItems />
</MenuList>
</Menu>
</Flex>
<Accordion
reduceMotion
defaultIndex={[0].concat(NORMAL_DATASOURCES.map((_, i) => i + 1))}
allowMultiple
>
<Api
apis={editorStore.dataSources[DataSourceType.API] || []}
active={active}
onItemClick={onApiItemClick}
onItemRemove={onApiItemRemove}
/>
{NORMAL_DATASOURCES.map(dataSourceItem => (
<Data
key={dataSourceItem.title}
title={dataSourceItem.title}
filterPlaceholder={dataSourceItem.filterPlaceholder}
emptyPlaceholder={dataSourceItem.emptyPlaceholder}
traitType={dataSourceItem.traitType}
datas={dataSourceItem.datas}
active={active}
services={services}
onItemClick={onDataSourceItemClick}
onItemRemove={onStateItemRemove}
/>
))}
</Accordion>
</VStack>
);
};

View File

@ -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> = 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 (
<ComponentNode
id={dataSource.id}
key={dataSource.id}
component={dataSource}
parentId={null}
slot={null}
onSelectComponent={editorStore.setSelectedComponentId}
services={services}
droppable={false}
depth={0}
isSelected={editorStore.selectedComponent?.id === dataSource.id}
isExpanded={false}
onToggleExpand={() => undefined}
shouldShowSelfSlotName={false}
notEmptySlots={[]}
onDragStart={() => undefined}
onDragEnd={() => undefined}
prefix={
tag ? (
<Tag
size="sm"
colorScheme={COLOR_MAP[tag as keyof typeof COLOR_MAP]}
marginLeft="-3"
marginRight="1"
>
{tag}
</Tag>
) : undefined
}
/>
);
})}
</>
);
return (
<AccordionItem>
<AccordionButton justifyContent="space-between">
<HStack>
{type === 'component' ? <Tag colorScheme="blue">C</Tag> : undefined}
<Text fontWeight="bold">{title}</Text>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel padding="0">
{dataSources.length ? <StateItems /> : <Text padding="2">Empty</Text>}
</AccordionPanel>
</AccordionItem>
);
};

View File

@ -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<string, string>;
onItemClick: (dataSource: ComponentSchema) => void;
onItemRemove: (dataSource: ComponentSchema) => void;
}
export const DataSourceItem: React.FC<Props> = props => {
const {
dataSource,
active,
colorMap = {},
tag,
name,
onItemClick,
onItemRemove,
} = props;
return (
<HStack padding="2" display="flex" className={ItemStyle}>
<HStack
flex={1}
onClick={() => onItemClick(dataSource)}
cursor="pointer"
className={cx(TextStyle, active ? 'active' : '')}
overflow="hidden"
>
<Tag colorScheme={colorMap[tag]}>{tag}</Tag>
<Text
flex={1}
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
title={name}
>
{name}
</Text>
</HStack>
<CloseButton size="sm" onClick={() => onItemRemove(dataSource)} />
</HStack>
);
};

View File

@ -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> = 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 (
<VStack spacing="2" alignItems="stretch">
<HStack padding="4" paddingBottom="0">
<Text fontSize="lg" fontWeight="bold">
DataSources
</Text>
<Spacer />
<Menu isLazy>
<MenuButton
as={Button}
size="sm"
variant="ghost"
rightIcon={<ChevronDownIcon />}
colorScheme="blue"
>
Add
</MenuButton>
<MenuList>
{cdsTypes.length ? (
<MenuGroup title="From Component">
{cdsTypes.map(type => (
<MenuItem key={type} onClick={() => onCreateDSFromComponent(type)}>
{type}
</MenuItem>
))}
</MenuGroup>
) : undefined}
<MenuGroup title="From Trait">
{tdsTypes.map(type => (
<MenuItem key={type} onClick={() => onCreateDSFromTrait(type)}>
{type}
</MenuItem>
))}
</MenuGroup>
</MenuList>
</Menu>
</HStack>
<ComponentSearch
components={dataSources}
onChange={id => setSelectedComponentId(id)}
services={props.services}
/>
<Accordion
reduceMotion
defaultIndex={[0].concat(dsGroups.map((_, i) => i + 1))}
allowMultiple
>
{dsGroups.map(group => (
<DataSourceGroup
key={group.title}
title={group.title}
type={group.type}
dataSources={group.dataSources}
services={services}
/>
))}
</Accordion>
</VStack>
);
};

View File

@ -1 +1 @@
export * from './DataSource';
export * from './DataSourceList';

View File

@ -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<Props> = observer(
({ App, stateStore, services, libs, dependencies, onRefresh: onRefreshApp }) => {
const { editorStore } = services;
@ -52,8 +39,6 @@ export const Editor: React.FC<Props> = observer(
components,
selectedComponentId,
modules,
activeDataSource,
activeDataSourceType,
toolMenuTab,
explorerMenuTab,
setToolMenuTab,
@ -87,19 +72,7 @@ export const Editor: React.FC<Props> = observer(
) : null;
}, [App, app, isDisplayApp]);
const inspectForm = useMemo(() => {
if (activeDataSource && activeDataSourceType) {
return activeDataSourceType === DataSourceType.API ? null : (
<DataForm
datasource={activeDataSource}
services={services}
traitType={DATASOURCE_TRAIT_TYPE_MAP[activeDataSourceType]}
/>
);
} else {
return <ComponentForm services={services} />;
}
}, [activeDataSource, services, activeDataSourceType]);
const inspectForm = <ComponentForm services={services} />;
const onRefresh = useCallback(() => {
setIsDisplayApp(false);
@ -189,10 +162,7 @@ export const Editor: React.FC<Props> = observer(
<StructureTree services={services} />
</TabPanel>
<TabPanel height="full" overflow="auto" p={0}>
<DataSource
active={activeDataSource?.id ?? ''}
services={services}
/>
<DataSourceList services={services} />
</TabPanel>
<TabPanel overflow="auto" p={0} height="100%">
<StateViewer store={stateStore} />
@ -246,15 +216,6 @@ export const Editor: React.FC<Props> = observer(
</Box>
</Resizable>
) : null}
{activeDataSource && activeDataSourceType === DataSourceType.API ? (
<ApiForm
key={activeDataSource.id}
api={activeDataSource}
services={services}
store={stateStore}
className={ApiFormStyle}
/>
) : null}
</Flex>
</>
);

View File

@ -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> = props => {
onMouseLeave,
paddingLeft,
actionMenu,
prefix,
} = props;
const [isHover, setIsHover] = useState(false);
@ -117,6 +119,7 @@ export const ComponentItemView: React.FC<Props> = props => {
paddingLeft={`${paddingLeft + (noChevron ? ChevronWidth : 0)}px`}
>
{noChevron ? null : expandIcon}
{prefix}
<Text
cursor="pointer"
overflow="hidden"

View File

@ -27,6 +27,7 @@ type Props = ComponentNodeWithState & {
onToggleExpand: (id: string) => 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}
/>
</DropComponentWrapper>
{emptyChildrenSlotsPlaceholder}

View File

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

View File

@ -15,7 +15,7 @@ type Props = {
export const StructureTree: React.FC<Props> = observer(props => {
const { editorStore } = props.services;
const { setSelectedComponentId, selectedComponentId } = editorStore;
const { setSelectedComponentId, selectedComponentId, components } = editorStore;
const scrollWrapper = useRef<HTMLDivElement>(null);
const {
@ -97,7 +97,11 @@ export const StructureTree: React.FC<Props> = observer(props => {
<Text fontSize="lg" fontWeight="bold">
Components
</Text>
<ComponentSearch services={props.services} />
<ComponentSearch
components={components}
onChange={id => setSelectedComponentId(id)}
services={props.services}
/>
</VStack>
<Box
ref={scrollWrapper}

View File

@ -1,5 +1,4 @@
import { ComponentSchema } from '@sunmao-ui/core';
import { CoreComponentName, CORE_VERSION } from '@sunmao-ui/shared';
import { useCallback, useMemo, useState } from 'react';
import { EditorStore } from '../../services/EditorStore';
import {
@ -17,10 +16,7 @@ export function useStructureTreeState(editorStore: EditorStore) {
const nodes: ComponentNode[] = [];
const nodesMapCache: Record<string, ComponentNode> = {};
const depthMap: Record<string, number> = {};
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 => {

View File

@ -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.',
},
];

View File

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

View File

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

View File

@ -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<string, any>;
type: string;
defaultProperties?: Record<string, any>;
};
export class CreateDataSourceBranchOperation extends BaseBranchOperation<CreateDataSourceBranchOperationContext> {
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<CreateD
this.operationStack.insert(
new CreateTraitLeafOperation(this.registry, {
componentId: id,
traitType,
properties:
type === DataSourceType.API
? {
...initProperties,
method: 'get',
}
: { ...initProperties, ...defaultProperties },
traitType: type,
properties: defaultProperties || initProperties,
})
);

View File

@ -1,4 +1,3 @@
import { AppModel } from '../../../AppModel/AppModel';
import { ComponentId, TraitId, TraitType } from '../../../AppModel/IAppModel';
import { BaseLeafOperation } from '../../type';
@ -15,11 +14,14 @@ export class CreateTraitLeafOperation extends BaseLeafOperation<CreateTraitLeafO
do(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (!component) {
return prev
return prev;
}
const trait = component.addTrait(this.context.traitType as TraitType, this.context.properties);
const trait = component.addTrait(
this.context.traitType as TraitType,
this.context.properties
);
this.traitId = trait.id;
return prev
return prev;
}
redo(prev: AppModel): AppModel {
@ -29,9 +31,9 @@ export class CreateTraitLeafOperation extends BaseLeafOperation<CreateTraitLeafO
undo(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (!component) {
return prev
return prev;
}
component.removeTrait(this.traitId);
return prev
return prev;
}
}

View File

@ -5,15 +5,8 @@ import { RegistryInterface, StateManagerInterface } from '@sunmao-ui/runtime';
import { EventBusType } from './eventBus';
import { AppStorage } from './AppStorage';
import type { SchemaValidator, ValidateErrorResult } from '../validator';
import {
DataSourceType,
DATASOURCE_NAME_MAP,
DATASOURCE_TRAIT_TYPE_MAP,
} from '../constants/dataSource';
import { genOperation } from '../operations';
import { ExplorerMenuTabs, ToolMenuTabs } from '../constants/enum';
import { CORE_VERSION, CoreComponentName } from '@sunmao-ui/shared';
import { isEqual } from 'lodash';
import { AppModelManager } from '../operations/AppModelManager';
import type { Metadata } from '@sunmao-ui/core';
@ -52,8 +45,7 @@ export class EditorStore {
lastSavedComponentsVersion = 0;
schemaValidator?: SchemaValidator;
// data source
activeDataSourceId: string | null = null;
private isDataSourceTypeCache: Record<string, boolean> = {};
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<string, ComponentSchema[]> {
const dataSources: Record<string, ComponentSchema[]> = {};
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<string, any> = {}
) => {
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;
};

View File

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

View File

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

View File

@ -12,6 +12,7 @@ export default implementRuntimeComponent({
annotations: {
category: 'Advance',
},
isDataSource: true,
},
spec: {
properties: Type.Object({}),

View File

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

View File

@ -61,6 +61,7 @@ export function initSunmaoUI(props: SunmaoUIRuntimeProps = {}) {
globalHandlerMap,
apiService,
eleMap,
slotReceiver
};
}

View File

@ -17,6 +17,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';
@ -261,6 +262,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);

View File

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

View File

@ -28,6 +28,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.LocalStorage,
description: 'localStorage trait',
isDataSource: true,
},
spec: {
properties: LocalStorageTraitPropertiesSpec,

View File

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

View File

@ -18,6 +18,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.State,
description: 'add state to component',
isDataSource: true,
},
spec: {
properties: StateTraitPropertiesSpec,

View File

@ -16,6 +16,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.Transformer,
description: 'transform the value',
isDataSource: true,
},
spec: {
properties: TransformerTraitPropertiesSpec,