mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-06 21:40:23 +08:00
Merge branch 'refactor/datasource2' into publish
* refactor/datasource2: feat(DataSource): support create component datasource feat(DataSource): add fetch widget feat(DataSource): add isDataSource feat(DataSource): use ComponentNode to replace DataSourceItem feat(PopoverWidget): add appendToParent option fix(PopoverWidget): add padding to popover bottom feat(PopoverWidget): add appentToBody option refactor(editor): flatten ComponentTree # Conflicts: # packages/editor/src/components/StructureTree/ComponentItemView.tsx # packages/editor/src/components/StructureTree/ComponentTree.tsx # packages/editor/src/components/StructureTree/StructureTree.tsx # packages/editor/src/components/StructureTree/type.ts # packages/editor/src/components/StructureTree/useStructureTreeState.ts
This commit is contained in:
commit
fc8e1d309a
@ -6,6 +6,7 @@ export type Metadata<
|
||||
description?: string;
|
||||
annotations?: Record<string, any> & TAnnotations;
|
||||
exampleProperties?: TExample;
|
||||
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 || {},
|
||||
},
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
IconButton,
|
||||
Text,
|
||||
Tabs,
|
||||
TabPanels,
|
||||
@ -14,21 +13,17 @@ import {
|
||||
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';
|
||||
import { EditorServicesInterface } from '../../types/editor';
|
||||
import { ExpressionWidget } from '../Widgets';
|
||||
import { WidgetProps } from '../..';
|
||||
|
||||
enum TabIndex {
|
||||
Basic,
|
||||
@ -37,34 +32,27 @@ enum TabIndex {
|
||||
Body,
|
||||
}
|
||||
interface Props {
|
||||
api: ComponentSchema;
|
||||
services: EditorServices;
|
||||
store: Record<string, any>;
|
||||
className: string;
|
||||
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 { 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 { value, onChange, component, services } = props;
|
||||
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 [fetchResult, setFetchResult] = useState<FetchResultType | undefined>();
|
||||
const compactOptions = useMemo(
|
||||
() => ({
|
||||
height: '40px',
|
||||
@ -73,18 +61,9 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
[]
|
||||
);
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
...(trait?.properties as Static<typeof FetchTraitPropertiesSpec>),
|
||||
},
|
||||
initialValues: value,
|
||||
onSubmit: values => {
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation(registry, 'modifyTraitProperty', {
|
||||
componentId: api.id,
|
||||
traitIndex: traitIndex,
|
||||
properties: values,
|
||||
})
|
||||
);
|
||||
onChange(values);
|
||||
},
|
||||
});
|
||||
const { values } = formik;
|
||||
@ -93,26 +72,13 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
widgetOptions: { compactOptions },
|
||||
});
|
||||
|
||||
const onFetch = useCallback(async () => {
|
||||
const onFetch = useCallback(() => {
|
||||
services.apiService.send('uiMethod', {
|
||||
componentId: api.id,
|
||||
componentId: component.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]
|
||||
);
|
||||
}, [services.apiService, component]);
|
||||
const onMethodChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
formik.handleChange(e);
|
||||
@ -136,76 +102,42 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
...(trait?.properties as Static<typeof FetchTraitPropertiesSpec>),
|
||||
});
|
||||
formik.setValues({ ...value });
|
||||
// do not add formik into dependencies, otherwise it will cause infinite loop
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [trait?.properties]);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (api.id) {
|
||||
setName(api.id);
|
||||
setTabIndex(0);
|
||||
}
|
||||
}, [api.id]);
|
||||
useEffect(() => {
|
||||
const stop = watch(store, newValue => {
|
||||
setReactiveStore({ ...newValue });
|
||||
});
|
||||
const stop = watch(
|
||||
() => services.stateManager.store[component.id]?.fetch,
|
||||
newValue => {
|
||||
setFetchResult({ ...newValue });
|
||||
}
|
||||
);
|
||||
|
||||
return stop;
|
||||
}, [store]);
|
||||
}, [component.id, services.stateManager.store]);
|
||||
|
||||
return (
|
||||
<VStack
|
||||
className={className}
|
||||
backgroundColor="#fff"
|
||||
padding="4"
|
||||
paddingBottom="0"
|
||||
align="stretch"
|
||||
spacing="4"
|
||||
height="100%"
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<HStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
maxWidth="100%"
|
||||
spacing={46}
|
||||
<Text
|
||||
title={component.id}
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{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>
|
||||
{component.id}
|
||||
</Text>
|
||||
<HStack display="flex" spacing={4}>
|
||||
<HStack display="flex" spacing={1} flex={1} alignItems="stretch">
|
||||
<Select
|
||||
@ -223,7 +155,7 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
</Select>
|
||||
<Box flex={1}>
|
||||
<ExpressionWidget
|
||||
component={api}
|
||||
component={component}
|
||||
spec={URLSpec}
|
||||
value={values.url}
|
||||
path={EMPTY_ARRAY}
|
||||
@ -233,12 +165,12 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<Button colorScheme="blue" isLoading={result.loading} onClick={onFetch}>
|
||||
<Button colorScheme="blue" isLoading={fetchResult?.loading} onClick={onFetch}>
|
||||
Run
|
||||
</Button>
|
||||
</HStack>
|
||||
<Tabs
|
||||
flex={1}
|
||||
flex="1 1 0"
|
||||
overflow="hidden"
|
||||
index={tabIndex}
|
||||
onChange={index => {
|
||||
@ -254,22 +186,22 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
</TabList>
|
||||
<TabPanels flex={1} overflow="auto">
|
||||
<TabPanel>
|
||||
<Basic api={api} formik={formik} services={services} />
|
||||
<Basic api={component} formik={formik} services={services} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<HeadersForm
|
||||
api={api}
|
||||
api={component}
|
||||
spec={FetchTraitPropertiesSpec.properties.headers as WidgetProps['spec']}
|
||||
services={services}
|
||||
formik={formik}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Params api={api} services={services} formik={formik} />
|
||||
<Params api={component} services={services} formik={formik} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Body
|
||||
api={api}
|
||||
api={component}
|
||||
spec={FetchTraitPropertiesSpec.properties.body as WidgetProps['spec']}
|
||||
services={services}
|
||||
formik={formik}
|
||||
@ -278,7 +210,7 @@ export const ApiForm: React.FC<Props> = props => {
|
||||
</TabPanels>
|
||||
</VStack>
|
||||
</Tabs>
|
||||
<ResponseInfo {...result} />
|
||||
<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,8 +10,6 @@ import {
|
||||
Spinner,
|
||||
Tag,
|
||||
} from '@chakra-ui/react';
|
||||
import { CodeEditor } from '../../CodeEditor';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
interface Props {
|
||||
data?: unknown;
|
||||
@ -39,7 +37,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 +50,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>
|
||||
@ -66,18 +64,9 @@ export const Response: React.FC<Props> = props => {
|
||||
{props.loading || !isOpen ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<CodeEditor
|
||||
className={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
mode={{
|
||||
name: 'javascript',
|
||||
json: true,
|
||||
}}
|
||||
defaultCode={error || data}
|
||||
readOnly
|
||||
/>
|
||||
<pre>
|
||||
<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(false);
|
||||
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);
|
@ -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;
|
||||
|
@ -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">
|
||||
@ -123,6 +124,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
|
||||
services={services}
|
||||
/>
|
||||
),
|
||||
hide: selectedComponentIsDataSource,
|
||||
},
|
||||
{
|
||||
title: 'Traits',
|
||||
@ -142,15 +144,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>
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
InputRightElement,
|
||||
Tag,
|
||||
} from '@chakra-ui/react';
|
||||
import { CoreComponentName, CORE_VERSION } from '@sunmao-ui/shared';
|
||||
import { CORE_VERSION } from '@sunmao-ui/shared';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
import { EditorServices } from '../../types';
|
||||
import { ExplorerMenuTabs } from '../../constants/enum';
|
||||
@ -66,8 +66,6 @@ const tagStyle = css`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const IGNORE_COMPONENTS: string[] = [CoreComponentName.Dummy];
|
||||
|
||||
export const ComponentList: React.FC<Props> = ({ services }) => {
|
||||
const { registry, editorStore } = services;
|
||||
const [filterText, setFilterText] = useState('');
|
||||
@ -84,10 +82,7 @@ export const ComponentList: React.FC<Props> = ({ services }) => {
|
||||
const categories = useMemo<Category[]>(() => {
|
||||
const grouped = groupBy(
|
||||
registry.getAllComponents().filter(c => {
|
||||
if (
|
||||
IGNORE_COMPONENTS.includes(c.metadata.name) ||
|
||||
(checkedVersions.length && !checkedVersions.includes(c.version))
|
||||
) {
|
||||
if (checkedVersions.length && !checkedVersions.includes(c.version)) {
|
||||
return false;
|
||||
} else if (!filterText) {
|
||||
return true;
|
||||
|
@ -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,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>
|
||||
);
|
||||
};
|
106
packages/editor/src/components/DataSource/DataSourceGroup.tsx
Normal file
106
packages/editor/src/components/DataSource/DataSourceGroup.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
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',
|
||||
};
|
||||
const STATE_MAP: Record<string, string> = {
|
||||
undefined: 'Any',
|
||||
boolean: 'Boolean',
|
||||
string: 'String',
|
||||
number: 'Number',
|
||||
object: 'Object',
|
||||
};
|
||||
|
||||
export const DataSourceGroup: React.FC<Props> = props => {
|
||||
const { dataSources = [], title, services, type } = props;
|
||||
const { stateManager, editorStore } = services;
|
||||
const { store } = stateManager;
|
||||
|
||||
const StateItems = () => (
|
||||
<>
|
||||
{dataSources.map(dataSource => {
|
||||
let tag = '';
|
||||
|
||||
const trait = dataSource.traits.find(({ type }) => type === `core/v1/fetch`);
|
||||
if (trait?.properties) {
|
||||
tag = ((trait.properties.config as any).method as string).toUpperCase();
|
||||
} else {
|
||||
tag = Array.isArray(store[dataSource.id]?.value)
|
||||
? 'Array'
|
||||
: STATE_MAP[typeof store[dataSource.id]?.value] ?? 'Any';
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
177
packages/editor/src/components/DataSource/DataSourceList.tsx
Normal file
177
packages/editor/src/components/DataSource/DataSourceList.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Flex,
|
||||
Spacer,
|
||||
Text,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
IconButton,
|
||||
Accordion,
|
||||
MenuGroup,
|
||||
} from '@chakra-ui/react';
|
||||
import { AddIcon, 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';
|
||||
|
||||
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: true,
|
||||
});
|
||||
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">
|
||||
<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>
|
||||
{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>
|
||||
</Flex>
|
||||
<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,
|
||||
@ -86,19 +71,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);
|
||||
@ -184,7 +157,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} />
|
||||
@ -235,15 +208,6 @@ export const Editor: React.FC<Props> = observer(
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Resizable>
|
||||
{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,9 +27,10 @@ type Props = ComponentNodeWithState & {
|
||||
onToggleExpand: (id: string) => void;
|
||||
onDragStart: (id: string) => void;
|
||||
onDragEnd: (id: string) => void;
|
||||
prefix?: React.ReactNode;
|
||||
};
|
||||
|
||||
const ComponentTree = (props: Props) => {
|
||||
const ComponentNodeImpl = (props: Props) => {
|
||||
const {
|
||||
component,
|
||||
onSelectComponent,
|
||||
@ -42,9 +43,10 @@ const ComponentTree = (props: Props) => {
|
||||
parentId,
|
||||
slot,
|
||||
shouldShowSelfSlotName,
|
||||
hasChildrenSlots,
|
||||
notEmptySlots,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
prefix,
|
||||
} = props;
|
||||
const { registry, eventBus, appModelManager } = services;
|
||||
const slots = Object.keys(registry.getComponentByType(component.type).spec.slots);
|
||||
@ -96,7 +98,7 @@ const ComponentTree = (props: Props) => {
|
||||
const onMouseLeave = useCallback(() => {
|
||||
services.editorStore.setHoverComponentId('');
|
||||
}, [services.editorStore]);
|
||||
const emptySlots = xor(hasChildrenSlots, slots);
|
||||
const emptySlots = xor(notEmptySlots, slots);
|
||||
|
||||
const emptyChildrenSlotsPlaceholder = isExpanded
|
||||
? emptySlots.map(_slot => {
|
||||
@ -135,6 +137,29 @@ const ComponentTree = (props: Props) => {
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const actionMenu = (
|
||||
<Menu isLazy gutter={4}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
variant="ghost"
|
||||
height="24px"
|
||||
width="24px"
|
||||
minWidth="24px"
|
||||
marginInlineEnd="8px !important"
|
||||
icon={<HamburgerIcon width="16px" height="16px" />}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<CopyIcon />} onClick={onClickDuplicate}>
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
<MenuItem icon={<DeleteIcon />} color="red.500" onClick={onClickRemove}>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack
|
||||
key={component.id}
|
||||
@ -186,28 +211,8 @@ const ComponentTree = (props: Props) => {
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
paddingLeft={paddingLeft}
|
||||
actionMenu={
|
||||
<Menu isLazy gutter={4}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
variant="ghost"
|
||||
height="24px"
|
||||
width="24px"
|
||||
minWidth="24px"
|
||||
marginInlineEnd="8px !important"
|
||||
icon={<HamburgerIcon width="16px" height="16px" />}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<CopyIcon />} onClick={onClickDuplicate}>
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
<MenuItem icon={<DeleteIcon />} color="red.500" onClick={onClickRemove}>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
}
|
||||
actionMenu={actionMenu}
|
||||
prefix={prefix}
|
||||
/>
|
||||
</DropComponentWrapper>
|
||||
{emptyChildrenSlotsPlaceholder}
|
||||
@ -215,8 +220,8 @@ const ComponentTree = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const MemoComponentTree: React.FC<Props> = React.memo(
|
||||
ComponentTree,
|
||||
const MemoComponentNode: React.FC<Props> = React.memo(
|
||||
ComponentNodeImpl,
|
||||
(prevProps, nextProps) => {
|
||||
const isSame =
|
||||
prevProps.component === nextProps.component &&
|
||||
@ -227,9 +232,9 @@ const MemoComponentTree: React.FC<Props> = React.memo(
|
||||
prevProps.isSelected === nextProps.isSelected &&
|
||||
prevProps.isExpanded === nextProps.isExpanded &&
|
||||
prevProps.shouldShowSelfSlotName === nextProps.shouldShowSelfSlotName &&
|
||||
isEqual(prevProps.hasChildrenSlots, nextProps.hasChildrenSlots);
|
||||
isEqual(prevProps.notEmptySlots, nextProps.notEmptySlots);
|
||||
return isSame;
|
||||
}
|
||||
);
|
||||
|
||||
export const ComponentTreeWrapper = MemoComponentTree;
|
||||
export const ComponentNode = MemoComponentNode;
|
@ -1,13 +1,13 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Box, Text, VStack } from '@chakra-ui/react';
|
||||
import { ComponentTreeWrapper } from './ComponentTree';
|
||||
import { DropComponentWrapper } from './DropComponentWrapper';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { EditorServices } from '../../types';
|
||||
import scrollIntoView from 'scroll-into-view';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ComponentNode } from './ComponentNode';
|
||||
import { DropComponentWrapper } from './DropComponentWrapper';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { useStructureTreeState } from './useStructureTreeState';
|
||||
import { ComponentSearch } from './ComponentSearch';
|
||||
import { EditorServices } from '../../types';
|
||||
|
||||
type Props = {
|
||||
services: EditorServices;
|
||||
@ -27,6 +27,7 @@ export const StructureTree: React.FC<Props> = observer(props => {
|
||||
setDraggingId,
|
||||
} = useStructureTreeState(editorStore);
|
||||
|
||||
// auto expand and scroll node into view when the node is selected
|
||||
useEffect(() => {
|
||||
expandNode(selectedComponentId);
|
||||
|
||||
@ -36,6 +37,7 @@ export const StructureTree: React.FC<Props> = observer(props => {
|
||||
|
||||
const wrapperRect = scrollWrapper.current?.getBoundingClientRect();
|
||||
const eleRect = selectedElement?.getBoundingClientRect();
|
||||
// check whether selected node is outside of view
|
||||
if (
|
||||
selectedElement &&
|
||||
eleRect &&
|
||||
@ -43,8 +45,7 @@ export const StructureTree: React.FC<Props> = observer(props => {
|
||||
(eleRect.top < wrapperRect.top ||
|
||||
eleRect.top > wrapperRect.top + wrapperRect?.height)
|
||||
) {
|
||||
// check selected element is outside of view
|
||||
scrollIntoView(selectedElement, { time: 300, align: { lockX: true } });
|
||||
scrollIntoView(selectedElement, { align: { lockX: true } });
|
||||
}
|
||||
});
|
||||
}, [expandNode, selectedComponentId]);
|
||||
@ -52,6 +53,10 @@ export const StructureTree: React.FC<Props> = observer(props => {
|
||||
const componentEles = shouldRenderNodes.map((node, i) => {
|
||||
const prevNode = i > 0 ? shouldRenderNodes[i - 1] : null;
|
||||
let shouldShowSlot = false;
|
||||
// Conditions in which a component should show the slot name it belongs
|
||||
// 1. It is in a slot and the slot is not 'content'.
|
||||
// 2. And its previous node is its parent(has the same parent).
|
||||
// 3. Or its previous node is its sibling and is in different slot.
|
||||
if (node.slot && node.slot !== 'content' && prevNode) {
|
||||
const prevNodeIsParent = prevNode.id === node.parentId;
|
||||
const prevNodeInDifferentSlot =
|
||||
@ -59,7 +64,7 @@ export const StructureTree: React.FC<Props> = observer(props => {
|
||||
shouldShowSlot = prevNodeIsParent || prevNodeInDifferentSlot;
|
||||
}
|
||||
return (
|
||||
<ComponentTreeWrapper
|
||||
<ComponentNode
|
||||
id={node.id}
|
||||
key={node.id}
|
||||
component={node.component}
|
||||
@ -73,7 +78,7 @@ export const StructureTree: React.FC<Props> = observer(props => {
|
||||
isExpanded={!!expandedMap[node.id]}
|
||||
onToggleExpand={onToggleExpand}
|
||||
shouldShowSelfSlotName={shouldShowSlot}
|
||||
hasChildrenSlots={node.hasChildrenSlots}
|
||||
notEmptySlots={node.notEmptySlots}
|
||||
onDragStart={id => setDraggingId(id)}
|
||||
onDragEnd={() => setDraggingId('')}
|
||||
/>
|
||||
|
@ -7,7 +7,7 @@ export type ComponentNode = {
|
||||
parentId: string | null;
|
||||
slot: string | null;
|
||||
depth: number;
|
||||
hasChildrenSlots: string[];
|
||||
notEmptySlots: string[];
|
||||
};
|
||||
|
||||
// These fields need computed with UI State
|
||||
|
@ -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 {
|
||||
@ -12,30 +11,22 @@ export function useStructureTreeState(editorStore: EditorStore) {
|
||||
const [expandedMap, setExpandedMap] = useState<Record<string, boolean>>({});
|
||||
const [draggingId, setDraggingId] = useState('');
|
||||
|
||||
const { nodes, nodesMap, childrenMap } = useMemo(() => {
|
||||
// format components schema to ComponentNode
|
||||
const { nodes, nodesMapCache, childrenMap } = useMemo(() => {
|
||||
const nodes: ComponentNode[] = [];
|
||||
const nodesMap: Record<string, ComponentNode> = {};
|
||||
const uiComponents = editorStore.components.filter(
|
||||
c => c.type !== `${CORE_VERSION}/${CoreComponentName.Dummy}`
|
||||
);
|
||||
const nodesMapCache: Record<string, ComponentNode> = {};
|
||||
const depthMap: Record<string, number> = {};
|
||||
// const parentMap: Record<string, string | null> = {};
|
||||
uiComponents.forEach(c => {
|
||||
depthMap[c.id] = 0;
|
||||
});
|
||||
const resolvedComponents = resolveApplicationComponents(uiComponents);
|
||||
const resolvedComponents = resolveApplicationComponents(editorStore.uiComponents);
|
||||
const { topLevelComponents, childrenMap } = resolvedComponents;
|
||||
|
||||
topLevelComponents.forEach(c => {
|
||||
const cb = (params: Required<TraverseParams>) => {
|
||||
depthMap[params.root.id] = params.depth;
|
||||
// parentMap[params.root.id] = params.parentId;
|
||||
const hasChildrenSlots = [];
|
||||
const notEmptySlots = [];
|
||||
const slots = childrenMap.get(params.root.id);
|
||||
if (slots) {
|
||||
for (const slot of slots.keys()) {
|
||||
if (slots.get(slot)?.length) {
|
||||
hasChildrenSlots.push(slot);
|
||||
notEmptySlots.push(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,9 +36,10 @@ export function useStructureTreeState(editorStore: EditorStore) {
|
||||
depth: params.depth,
|
||||
parentId: params.parentId,
|
||||
slot: params.slot,
|
||||
hasChildrenSlots,
|
||||
notEmptySlots,
|
||||
};
|
||||
nodesMap[params.root.id] = node;
|
||||
nodesMapCache[params.root.id] = node;
|
||||
depthMap[params.root.id] = params.depth;
|
||||
nodes.push(node);
|
||||
};
|
||||
|
||||
@ -61,7 +53,7 @@ export function useStructureTreeState(editorStore: EditorStore) {
|
||||
});
|
||||
});
|
||||
|
||||
return { nodes, nodesMap, childrenMap };
|
||||
return { nodes, nodesMapCache, childrenMap };
|
||||
}, [editorStore.components]);
|
||||
|
||||
const onToggleExpand = useCallback(
|
||||
@ -71,48 +63,49 @@ export function useStructureTreeState(editorStore: EditorStore) {
|
||||
if (nextExpanded) {
|
||||
return { ...prevMap, [id]: nextExpanded };
|
||||
}
|
||||
// if close, close all its children
|
||||
// if collapse, collapse all its children
|
||||
const newExpandedMap = { ...prevMap };
|
||||
traverse({
|
||||
childrenMap,
|
||||
root: nodesMap[id].component,
|
||||
root: nodesMapCache[id].component,
|
||||
cb: params => delete newExpandedMap[params.root.id],
|
||||
});
|
||||
return newExpandedMap;
|
||||
});
|
||||
},
|
||||
[childrenMap, nodesMap]
|
||||
[childrenMap, nodesMapCache]
|
||||
);
|
||||
|
||||
// expand all the ancestors of a node
|
||||
const expandNode = useCallback(
|
||||
(id: string) => {
|
||||
setExpandedMap(prevMap => {
|
||||
if (prevMap[id]) return prevMap;
|
||||
const newExpandedMap = { ...prevMap };
|
||||
let curr: string = nodesMap[id]?.parentId || '';
|
||||
// don't expand its self
|
||||
let curr: string = nodesMapCache[id]?.parentId || '';
|
||||
while (curr) {
|
||||
newExpandedMap[curr] = true;
|
||||
curr = nodesMap[curr]?.parentId || '';
|
||||
curr = nodesMapCache[curr]?.parentId || '';
|
||||
}
|
||||
return newExpandedMap;
|
||||
});
|
||||
},
|
||||
[nodesMap]
|
||||
[nodesMapCache]
|
||||
);
|
||||
|
||||
const shouldRender = useCallback(
|
||||
(node: ComponentNode) => {
|
||||
if (!node.parentId) return true;
|
||||
if (expandedMap[node.parentId]) return true;
|
||||
return false;
|
||||
},
|
||||
[expandedMap]
|
||||
);
|
||||
// nodes whose parent is expanded
|
||||
const shouldRenderNodes = useMemo(
|
||||
() => nodes.filter(shouldRender),
|
||||
[nodes, shouldRender]
|
||||
() =>
|
||||
nodes.filter((node: ComponentNode) => {
|
||||
if (!node.parentId) return true;
|
||||
if (expandedMap[node.parentId]) return true;
|
||||
return false;
|
||||
}),
|
||||
[expandedMap, nodes]
|
||||
);
|
||||
|
||||
// nodes that is being dragged or its ancestor is being dragged
|
||||
const undroppableMap = useMemo(() => {
|
||||
const map: Record<string, boolean> = {};
|
||||
|
||||
@ -120,13 +113,13 @@ export function useStructureTreeState(editorStore: EditorStore) {
|
||||
|
||||
traverse({
|
||||
childrenMap,
|
||||
root: nodesMap[draggingId].component,
|
||||
root: nodesMapCache[draggingId].component,
|
||||
cb: params => {
|
||||
map[params.root.id] = true;
|
||||
},
|
||||
});
|
||||
return map;
|
||||
}, [childrenMap, draggingId, nodesMap]);
|
||||
}, [childrenMap, draggingId, nodesMapCache]);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
|
@ -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.',
|
||||
},
|
||||
];
|
@ -7,14 +7,12 @@ export const unremovableTraits = [`${CORE_VERSION}/${CoreTraitName.Slot}`];
|
||||
export const hideCreateTraitsList = [
|
||||
`${CORE_VERSION}/${CoreTraitName.Event}`,
|
||||
`${CORE_VERSION}/${CoreTraitName.Style}`,
|
||||
`${CORE_VERSION}/${CoreTraitName.Fetch}`,
|
||||
`${CORE_VERSION}/${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;
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,7 @@ export default implementRuntimeComponent({
|
||||
annotations: {
|
||||
category: 'Advance',
|
||||
},
|
||||
isDataSource: true,
|
||||
},
|
||||
spec: {
|
||||
properties: Type.Object({}),
|
||||
|
@ -61,6 +61,7 @@ export function initSunmaoUI(props: SunmaoUIRuntimeProps = {}) {
|
||||
globalHandlerMap,
|
||||
apiService,
|
||||
eleMap,
|
||||
slotReceiver,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -10,47 +10,62 @@ 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, {
|
||||
widgetOptions: { appendToParent: true },
|
||||
}),
|
||||
onError: Type.Array(EventCallBackHandlerSpec, {
|
||||
widgetOptions: { appendToParent: true },
|
||||
}),
|
||||
},
|
||||
{
|
||||
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,
|
||||
properties: Type.Object({
|
||||
config: FetchTraitPropertiesSpec,
|
||||
}),
|
||||
state: Type.Object({
|
||||
fetch: Type.Object({
|
||||
loading: Type.Boolean(),
|
||||
@ -71,22 +86,26 @@ export default implementRuntimeTrait({
|
||||
},
|
||||
})(() => {
|
||||
return ({
|
||||
config,
|
||||
trait,
|
||||
url,
|
||||
method,
|
||||
lazy: _lazy,
|
||||
headers: _headers,
|
||||
body,
|
||||
bodyType,
|
||||
onComplete,
|
||||
onError,
|
||||
mergeState,
|
||||
services,
|
||||
subscribeMethods,
|
||||
componentId,
|
||||
disabled,
|
||||
slotKey,
|
||||
}) => {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
lazy: _lazy,
|
||||
headers: _headers,
|
||||
body,
|
||||
bodyType,
|
||||
onComplete,
|
||||
onError,
|
||||
disabled,
|
||||
} = config;
|
||||
const rawConfig = trait.properties.config;
|
||||
const lazy = _lazy === undefined ? true : _lazy;
|
||||
|
||||
const fetchData = () => {
|
||||
@ -155,7 +174,8 @@ export default implementRuntimeTrait({
|
||||
error: undefined,
|
||||
},
|
||||
});
|
||||
const rawOnComplete = trait.properties.onComplete;
|
||||
const rawOnComplete =
|
||||
typeof rawConfig === 'string' ? [] : rawConfig.onComplete;
|
||||
|
||||
onComplete?.forEach((_, index) => {
|
||||
runEventHandler(
|
||||
@ -178,7 +198,7 @@ export default implementRuntimeTrait({
|
||||
error,
|
||||
},
|
||||
});
|
||||
const rawOnError = trait.properties.onError;
|
||||
const rawOnError = typeof rawConfig === 'string' ? [] : rawConfig.onError;
|
||||
|
||||
onError?.forEach((_, index) => {
|
||||
runEventHandler(onError[index], rawOnError, index, services, slotKey)();
|
||||
@ -197,7 +217,7 @@ export default implementRuntimeTrait({
|
||||
error: error.toString(),
|
||||
},
|
||||
});
|
||||
const rawOnError = trait.properties.onError;
|
||||
const rawOnError = typeof rawConfig === 'string' ? [] : rawConfig.onError;
|
||||
|
||||
onError?.forEach((_, index) => {
|
||||
runEventHandler(onError[index], rawOnError, index, services, slotKey)();
|
||||
|
@ -28,6 +28,7 @@ export default implementRuntimeTrait({
|
||||
metadata: {
|
||||
name: CoreTraitName.LocalStorage,
|
||||
description: 'localStorage trait',
|
||||
isDataSource: true,
|
||||
},
|
||||
spec: {
|
||||
properties: LocalStorageTraitPropertiesSpec,
|
||||
|
@ -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