mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-06 21:40:23 +08:00
feat(runtime): support Component DataSource
This commit is contained in:
parent
7a8656574f
commit
bac129037b
@ -7,6 +7,7 @@ export type Metadata<
|
||||
annotations?: Record<string, any> & TAnnotations;
|
||||
exampleProperties?: TExample;
|
||||
deprecated?: boolean;
|
||||
isDataSource?: boolean;
|
||||
};
|
||||
|
||||
type ComponentCategory =
|
||||
|
@ -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 || {},
|
||||
},
|
||||
|
211
packages/editor-sdk/src/components/ApiForm/ApiForm.tsx
Normal file
211
packages/editor-sdk/src/components/ApiForm/ApiForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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({
|
@ -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[] = [];
|
@ -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[] = [];
|
@ -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[] = [];
|
@ -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>
|
51
packages/editor-sdk/src/components/Widgets/FetchWidget.tsx
Normal file
51
packages/editor-sdk/src/components/Widgets/FetchWidget.tsx
Normal 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);
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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))
|
||||
) {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from './DataForm';
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
183
packages/editor/src/components/DataSource/DataSourceList.tsx
Normal file
183
packages/editor/src/components/DataSource/DataSourceList.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1 +1 @@
|
||||
export * from './DataSource';
|
||||
export * from './DataSourceList';
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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(() => {
|
||||
|
@ -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}
|
||||
|
@ -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 => {
|
||||
|
@ -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.',
|
||||
},
|
||||
];
|
@ -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__';
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export default implementRuntimeComponent({
|
||||
annotations: {
|
||||
category: 'Advance',
|
||||
},
|
||||
isDataSource: true,
|
||||
},
|
||||
spec: {
|
||||
properties: Type.Object({}),
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -61,6 +61,7 @@ export function initSunmaoUI(props: SunmaoUIRuntimeProps = {}) {
|
||||
globalHandlerMap,
|
||||
apiService,
|
||||
eleMap,
|
||||
slotReceiver
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -28,6 +28,7 @@ export default implementRuntimeTrait({
|
||||
metadata: {
|
||||
name: CoreTraitName.LocalStorage,
|
||||
description: 'localStorage trait',
|
||||
isDataSource: true,
|
||||
},
|
||||
spec: {
|
||||
properties: LocalStorageTraitPropertiesSpec,
|
||||
|
38
packages/runtime/src/traits/core/SlotV2.tsx
Normal file
38
packages/runtime/src/traits/core/SlotV2.tsx
Normal 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,
|
||||
});
|
||||
});
|
@ -18,6 +18,7 @@ export default implementRuntimeTrait({
|
||||
metadata: {
|
||||
name: CoreTraitName.State,
|
||||
description: 'add state to component',
|
||||
isDataSource: true,
|
||||
},
|
||||
spec: {
|
||||
properties: StateTraitPropertiesSpec,
|
||||
|
@ -16,6 +16,7 @@ export default implementRuntimeTrait({
|
||||
metadata: {
|
||||
name: CoreTraitName.Transformer,
|
||||
description: 'transform the value',
|
||||
isDataSource: true,
|
||||
},
|
||||
spec: {
|
||||
properties: TransformerTraitPropertiesSpec,
|
||||
|
Loading…
x
Reference in New Issue
Block a user