Merge branch 'main' of github.com:webzard-io/sunmao-ui into feat/arco

This commit is contained in:
xzdry 2022-04-24 13:37:20 +08:00
commit 0efb3824ea
37 changed files with 397 additions and 259 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/arco-lib",
"version": "0.1.7",
"version": "0.1.8",
"homepage": "https://github.com/webzard-io/sunmao-ui-arco-lib",
"license": "MIT",
"publishConfig": {
@ -31,8 +31,8 @@
"@arco-design/web-react": "^2.29.0",
"@emotion/css": "^11.7.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/runtime": "^0.5.7",
"eslint-plugin-react-hooks": "^4.3.0",
"lodash-es": "^4.17.21",
"react": "^17.0.2",
@ -42,9 +42,9 @@
"devDependencies": {
"@types/lodash": "^4.14.170",
"@types/lodash-es": "^4.17.5",
"@types/react-resizable": "^1.7.4",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-resizable": "^1.7.4",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"@vitejs/plugin-react": "^1.0.0",

View File

@ -306,12 +306,15 @@ export const Table = implementRuntimeComponent({
}
newColumn.render = (ceilValue: any, record: any, index: number) => {
const evalOptions = {
evalListItem: true,
scopeObject: {
[LIST_ITEM_EXP]: record,
},
};
const evaledColumn: ColumnProperty = services.stateManager.deepEval(
column,
true,
{
[LIST_ITEM_EXP]: record,
}
evalOptions
);
const value = record[evaledColumn.dataIndex];
@ -324,8 +327,7 @@ export const Table = implementRuntimeComponent({
if (!rawColumn.btnCfg) return;
const evaledButtonConfig = services.stateManager.deepEval(
rawColumn.btnCfg,
true,
{ [LIST_ITEM_EXP]: record }
evalOptions
);
evaledButtonConfig.handlers.forEach(handler => {

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/chakra-ui-lib",
"version": "0.3.7",
"version": "0.3.8",
"description": "sunmao-ui chakra-ui library",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -33,9 +33,9 @@
"@chakra-ui/react": "^1.7.1",
"@emotion/styled": "^11.6.0",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/editor-sdk": "^0.1.8",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/editor-sdk": "^0.1.9",
"@sunmao-ui/runtime": "^0.5.7",
"chakra-react-select": "^1.3.2",
"framer-motion": "^4",
"lodash-es": "^4.17.21",

View File

@ -14,27 +14,20 @@ const PropsSpec = Type.Object({
listData: Type.Array(Type.Record(Type.String(), Type.String()), {
title: 'Data',
category: BASIC,
widget: 'core/v1/Expression'
}),
template: ModuleSpec,
});
const exampleProperties = {
listData: [
{
id: '1',
name: 'Bowen Tan',
},
],
listData: [{ id: '1' }, { id: '2' }, { id: '3' }],
template: {
id: 'listItemName-{{$listItem.id}}',
type: 'core/v1/text',
type: 'custom/v1/myModule0',
properties: {
value: {
raw: 'Name{{$listItem.name}}',
format: 'plain',
},
value: '{{$listItem.id}}',
},
traits: [],
handlers: [],
},
};

View File

@ -7,34 +7,36 @@ import {
LIST_ITEM_INDEX_EXP,
ModuleRenderer,
UIServices,
ExpressionError
ExpressionError,
} from '@sunmao-ui/runtime';
export const TableTd: React.FC<{
index: number;
item: any;
column: Static<typeof ColumnSpec>;
rawColumn: Static<typeof ColumnsPropertySpec>[0]
rawColumn: Static<typeof ColumnsPropertySpec>[0];
onClickItem: () => void;
services: UIServices;
app?: RuntimeApplication;
}> = props => {
const { item, index, column, rawColumn, onClickItem, services, app } = props;
const evalOptions = {
evalListItem: true,
scopeObject: {
[LIST_ITEM_EXP]: item,
},
};
let value = item[column.key];
let buttonConfig = column.buttonConfig;
if (column.displayValue) {
const result = services.stateManager.maskedEval(column.displayValue, true, {
[LIST_ITEM_EXP]: item,
});
const result = services.stateManager.maskedEval(column.displayValue, evalOptions);
value = result instanceof ExpressionError ? '' : result;
}
if (column.buttonConfig) {
buttonConfig = services.stateManager.deepEval(column.buttonConfig, true, {
[LIST_ITEM_EXP]: item,
});
buttonConfig = services.stateManager.deepEval(column.buttonConfig, evalOptions);
}
let content = value;
@ -57,9 +59,7 @@ export const TableTd: React.FC<{
const onClick = () => {
onClickItem();
rawColumn.buttonConfig.handlers.forEach(handler => {
const evaledHandler = services.stateManager.deepEval(handler, true, {
[LIST_ITEM_EXP]: item,
});
const evaledHandler = services.stateManager.deepEval(handler, evalOptions);
services.apiService.send('uiMethod', {
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/core",
"version": "0.5.4",
"version": "0.5.5",
"description": "sunmao-ui core",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",

View File

@ -1,7 +1,6 @@
import { JSONSchema7Object } from 'json-schema';
import { parseVersion } from './version';
import { parseVersion, Version } from './version';
import { Metadata } from './metadata';
import { Version } from './version';
import { ComponentSchema } from './application';
// spec

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/editor-sdk",
"version": "0.1.8",
"version": "0.1.9",
"description": "The SDK for SunMao Editor",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -29,8 +29,8 @@
"@emotion/css": "^11.7.1",
"@emotion/react": "^11.1.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/runtime": "^0.5.7",
"codemirror": "^5.63.3",
"formik": "^2.2.9",
"immer": "^9.0.6",

View File

@ -45,7 +45,7 @@ export const ArrayTable: React.FC<Props> = props => {
<Th width="24px" />
{keys.map((key: string) => {
const propertySpec = itemSpec.properties?.[key];
const title = isJSONSchema(propertySpec) ? propertySpec.title : key;
const title = isJSONSchema(propertySpec) ? (propertySpec.title || key) : key;
return <Th key={key}>{title}</Th>;
})}

View File

@ -155,7 +155,8 @@ export const ExpressionWidget: React.FC<
const evalCode = useCallback(
(code: string) => {
try {
const result = services.stateManager.maskedEval(getParsedValue(code, type));
const value = getParsedValue(code, type);
const result = isExpression(code) ? services.stateManager.maskedEval(value) : value;
if (result instanceof ExpressionError) {
throw result;

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/editor",
"version": "0.5.8",
"version": "0.5.9",
"description": "sunmao-ui editor",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -36,11 +36,11 @@
"@emotion/css": "^11.7.1",
"@emotion/react": "^11.1.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/arco-lib": "^0.1.7",
"@sunmao-ui/chakra-ui-lib": "^0.3.7",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/editor-sdk": "^0.1.8",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/arco-lib": "^0.1.8",
"@sunmao-ui/chakra-ui-lib": "^0.3.8",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/editor-sdk": "^0.1.9",
"@sunmao-ui/runtime": "^0.5.7",
"acorn": "^8.7.0",
"acorn-loose": "^8.3.0",
"acorn-walk": "^8.2.0",

View File

@ -18,6 +18,7 @@ import {
import { Static, Type } from '@sinclair/typebox';
import { EditorServices } from '../../../types';
import { ComponentSchema } from '@sunmao-ui/core';
import { SpecWidget } from '@sunmao-ui/editor-sdk';
type Values = Static<typeof FetchTraitPropertiesSpec>;
type EventHandler = Static<typeof EventCallBackHandlerSpec>;
@ -93,8 +94,14 @@ const Handler = (props: HandlerProps) => {
);
};
const DisabledSpec = Type.Boolean({
widgetOptions: { isShowAsideExpressionButton: true },
});
const EmptyArray: string[] = [];
export const Basic: React.FC<Props> = props => {
const { formik } = props;
const { formik, api, services } = props;
const onAddHandler = (type: HandlerType) => {
const newHandler: EventHandler = {
@ -113,6 +120,14 @@ export const Basic: React.FC<Props> = props => {
formik.setFieldValue(type, [...(formik.values[type] || []), newHandler]);
};
const onDisabledChange = useCallback(
val => {
formik.setFieldValue('disabled', val);
formik.handleSubmit();
},
[formik]
);
const generateHandlers = (type: HandlerType) => (
<FormControl>
<HStack width="full" alignItems="center" mb={0}>
@ -145,6 +160,21 @@ export const Basic: React.FC<Props> = props => {
onBlur={() => formik.handleSubmit()}
/>
</FormControl>
<FormControl display="flex" alignItems="center">
<FormLabel margin="0" marginRight="2">
Disabled
</FormLabel>
<SpecWidget
component={api}
spec={DisabledSpec}
value={formik.values.disabled}
path={EmptyArray}
level={1}
services={services}
onChange={onDisabledChange}
/>
</FormControl>
{generateHandlers('onComplete')}
{generateHandlers('onError')}
</VStack>

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect } from 'react';
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { Application } from '@sunmao-ui/core';
import {
GridCallbacks,
@ -26,6 +26,7 @@ import { css } from '@emotion/css';
import { EditorMaskWrapper } from './EditorMaskWrapper';
import { AppModel } from '../AppModel/AppModel';
import { LocalStorageForm } from './DataSource/LocalStorageForm';
import { Explorer } from './Explorer';
import { Resizable } from 're-resizable';
type ReturnOfInit = ReturnType<typeof initSunmaoUI>;
@ -37,7 +38,8 @@ type Props = {
stateStore: ReturnOfInit['stateManager']['store'];
services: EditorServices;
libs: SunmaoLib[];
uiProps: UIPros
onRefresh: () => void;
uiProps: UIPros;
};
const ApiFormStyle = css`
@ -50,7 +52,7 @@ const ApiFormStyle = css`
`;
export const Editor: React.FC<Props> = observer(
({ App, registry, stateStore, services, libs, uiProps }) => {
({ App, registry, stateStore, services, libs, uiProps, onRefresh: onRefreshApp }) => {
const { eventBus, editorStore } = services;
const {
components,
@ -68,12 +70,7 @@ export const Editor: React.FC<Props> = observer(
const [preview, setPreview] = useState(false);
const [codeMode, setCodeMode] = useState(false);
const [code, setCode] = useState('');
const [recoverKey, setRecoverKey] = useState(0);
const [isError, setIsError] = useState<boolean>(false);
const onError = (err: Error | null) => {
setIsError(err !== null);
};
const [isDisplayApp, setIsDisplayApp] = useState(true);
const gridCallbacks: GridCallbacks = useMemo(() => {
return {
@ -117,8 +114,8 @@ export const Editor: React.FC<Props> = observer(
}, [components]);
const appComponent = useMemo(() => {
return (
<ErrorBoundary key={recoverKey} onError={onError}>
return isDisplayApp ? (
<ErrorBoundary>
<App
options={app}
debugEvent={false}
@ -126,8 +123,8 @@ export const Editor: React.FC<Props> = observer(
gridCallbacks={gridCallbacks}
/>
</ErrorBoundary>
);
}, [App, app, gridCallbacks, recoverKey]);
) : null;
}, [App, app, gridCallbacks, isDisplayApp]);
const dataSourceForm = useMemo(() => {
let component: React.ReactNode = <ComponentForm services={services} />;
@ -147,16 +144,29 @@ export const Editor: React.FC<Props> = observer(
return component;
}, [activeDataSource, services, activeDataSourceType]);
useEffect(() => {
// when errors happened, `ErrorBoundary` wouldn't update until rerender
// so after the errors are fixed, would trigger this effect before `setError(false)`
// the process to handle the error is:
// app change -> error happen -> setError(true) -> setRecoverKey(recoverKey + 1) -> app change -> setRecoverKey(recoverKey + 1) -> setError(false)
if (isError) {
setRecoverKey(recoverKey + 1);
const onRefresh = useCallback(()=> {
services.stateManager.clear();
setIsDisplayApp(false);
onRefreshApp();
}, [services.stateManager, onRefreshApp]);
useEffect(()=> {
// Wait until the app is completely unmounted before remounting it
if (isDisplayApp === false) {
setIsDisplayApp(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app, isError]); // it only should depend on the app schema and `isError` to update
}, [isDisplayApp]);
const onCodeMode = useCallback(v => {
setCodeMode(v);
if (!v && code) {
eventBus.send(
'operation',
genOperation(registry, 'replaceApp', {
app: new AppModel(JSON.parse(code).spec.components, registry),
})
);
}
}, [code, eventBus, registry]);
const onPreview = useCallback(() => setPreview(true), []);
const renderMain = () => {
const appBox = (
@ -228,15 +238,15 @@ export const Editor: React.FC<Props> = observer(
}}
>
<TabList background="gray.50" whiteSpace="nowrap" justifyContent="center">
{/* <Tab>Explorer</Tab> */}
<Tab>Explorer</Tab>
<Tab>UI</Tab>
<Tab>Data</Tab>
<Tab>State</Tab>
</TabList>
<TabPanels flex="1" overflow="auto">
{/* <TabPanel>
<Explorer services={services} />
</TabPanel> */}
<TabPanel p={0}>
<Explorer services={services} />
</TabPanel>
<TabPanel p={0}>
<StructureTree
components={components}
@ -323,19 +333,10 @@ export const Editor: React.FC<Props> = observer(
<EditorHeader
scale={scale}
setScale={setScale}
onPreview={() => setPreview(true)}
onPreview={onPreview}
codeMode={codeMode}
onCodeMode={v => {
setCodeMode(v);
if (!v && code) {
eventBus.send(
'operation',
genOperation(registry, 'replaceApp', {
app: new AppModel(JSON.parse(code).spec.components, registry),
})
);
}
}}
onRefresh={onRefresh}
onCodeMode={onCodeMode}
/>
<Box display="flex" flex="1" overflow="auto">
{renderMain()}

View File

@ -7,7 +7,8 @@ export const EditorHeader: React.FC<{
onPreview: () => void;
codeMode: boolean;
onCodeMode: (v: boolean) => void;
}> = ({ scale, setScale, onPreview, onCodeMode, codeMode }) => {
onRefresh: () => void;
}> = ({ scale, setScale, onPreview, onCodeMode, onRefresh, codeMode }) => {
return (
<Flex p={2} borderBottomWidth="2px" borderColor="gray.200" align="center">
<Flex flex="1">
@ -31,6 +32,9 @@ export const EditorHeader: React.FC<{
</Button>
</Flex>
<Flex flex="1" justify="end">
<Button colorScheme="blue" marginRight="8px" onClick={onRefresh}>
refresh
</Button>
<Button colorScheme="blue" onClick={onPreview}>
preview
</Button>

View File

@ -2,18 +2,15 @@ import React from 'react';
type Props = {
onError?: (error: Error | null) => void;
}
};
class ErrorBoundary extends React.Component<
Props,
{ error: unknown }
> {
class ErrorBoundary extends React.Component<Props, { error: Error | null }> {
constructor(props: Props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error: unknown) {
static getDerivedStateFromError(error: Error) {
return { error };
}
@ -27,7 +24,7 @@ class ErrorBoundary extends React.Component<
render() {
if (this.state.error) {
return String(this.state.error);
return <div style={{ whiteSpace: 'pre-line' }}>{this.state.error.stack}</div>;
}
return this.props.children;

View File

@ -3,6 +3,7 @@ import ErrorBoundary from '../ErrorBoundary';
import { ExplorerForm } from './ExplorerForm/ExplorerForm';
import { ExplorerTree } from './ExplorerTree';
import { EditorServices } from '../../types';
import { Box } from '@chakra-ui/react';
type Props = {
services: EditorServices;
@ -25,21 +26,25 @@ export const Explorer: React.FC<Props> = ({ services }) => {
if (isEditingMode) {
return (
<ErrorBoundary>
<ExplorerForm
formType={formType}
version={currentVersion}
name={currentName}
setCurrentVersion={setCurrentVersion}
setCurrentName={setCurrentName}
onBack={onBack}
services={services}
/>
<Box padding={4}>
<ExplorerForm
formType={formType}
version={currentVersion}
name={currentName}
setCurrentVersion={setCurrentVersion}
setCurrentName={setCurrentName}
onBack={onBack}
services={services}
/>
</Box>
</ErrorBoundary>
);
}
return (
<ErrorBoundary>
<ExplorerTree onEdit={onEdit} services={services} />
<Box padding={4}>
<ExplorerTree onEdit={onEdit} services={services} />
</Box>
</ErrorBoundary>
);
};

View File

@ -1,8 +1,8 @@
enum ExplorerMenuTabs {
// EXPLORER = 0,
UI_TREE = 0,
DATA = 1,
STATE = 2
EXPLORER = 0,
UI_TREE = 1,
DATA = 2,
STATE = 3
}
enum ToolMenuTabs {

View File

@ -1,7 +1,7 @@
import { Editor as _Editor } from './components/Editor';
import { initSunmaoUI, SunmaoLib, SunmaoUIRuntimeProps } from '@sunmao-ui/runtime';
import { AppModelManager } from './operations/AppModelManager';
import React from 'react';
import React, { useState, useCallback } from 'react';
import {
widgets as internalWidgets,
WidgetManager,
@ -96,15 +96,21 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
};
const Editor: React.FC = () => {
const [store, setStore] = useState(stateManager.store);
const onRefresh = useCallback(()=> {
setStore(stateManager.store);
}, []);
return (
<ChakraProvider theme={editorTheme}>
<_Editor
App={App}
eleMap={ui.eleMap}
registry={registry}
stateStore={stateManager.store}
stateStore={store}
services={services}
libs={props.libs || []}
onRefresh={onRefresh}
uiProps={props.uiProps||{}}
/>
</ChakraProvider>

View File

@ -2,12 +2,14 @@ import { observable, makeObservable, action, toJS } from 'mobx';
import { Application, ComponentSchema, Module, RuntimeModule } from '@sunmao-ui/core';
import { produce } from 'immer';
import { DefaultNewModule, EmptyAppSchema } from '../constants';
import { addModuleId } from '../utils/addModuleId';
import { addModuleId, removeModuleId } from '../utils/addModuleId';
import { StorageHandler } from '../types';
export class AppStorage {
app: Application;
modules: Module[];
// modules that have {{$moduleId}}__
rawModules: Module[];
static AppLSKey = 'schema';
static ModulesLSKey = 'modules';
@ -17,20 +19,23 @@ export class AppStorage {
private storageHandler?: StorageHandler
) {
this.app = defaultApplication || EmptyAppSchema;
this.modules = defaultModules || [];
this.modules = defaultModules?.map(removeModuleId) || [];
this.rawModules = defaultModules || [];
makeObservable(this, {
app: observable.shallow,
modules: observable.shallow,
rawModules: observable.shallow,
setApp: action,
setModules: action,
setRawModules: action,
});
}
createModule() {
let index = this.modules.length;
this.modules.forEach((module) => {
this.modules.forEach(module => {
if (module.metadata.name === `myModule${index}`) {
index++;
}
@ -46,10 +51,11 @@ export class AppStorage {
metadata: {
...DefaultNewModule.metadata,
name,
}
},
};
this.setModules([...this.modules, newModule]);
this.setRawModules(this.modules.map(addModuleId));
this.saveModules();
}
@ -59,6 +65,7 @@ export class AppStorage {
({ version, metadata: { name } }) => version !== v || name !== n
)
);
this.setRawModules(this.modules.map(addModuleId));
this.saveModules();
}
@ -84,6 +91,8 @@ export class AppStorage {
draft[i].impl = components;
});
this.setModules(newModules);
const rawModules = newModules.map(addModuleId);
this.setRawModules(rawModules);
this.saveModules();
break;
}
@ -118,7 +127,10 @@ export class AppStorage {
draft[i].spec.stateMap = stateMap;
draft[i].version = version;
});
this.setModules(newModules);
const rawModules = newModules.map(addModuleId);
this.setRawModules(rawModules);
this.saveModules();
}
@ -127,8 +139,9 @@ export class AppStorage {
}
private saveModules() {
const modules = this.modules.map(addModuleId);
this.storageHandler?.onSaveModules && this.storageHandler?.onSaveModules(modules);
// save rawModules rather than modules because rawModules have {{$moduleId}}__
this.storageHandler?.onSaveModules &&
this.storageHandler?.onSaveModules(this.rawModules);
}
setApp(app: Application) {
@ -138,4 +151,8 @@ export class AppStorage {
setModules(modules: Module[]) {
this.modules = modules;
}
setRawModules(modules: Module[]) {
this.rawModules = modules;
}
}

View File

@ -5,7 +5,6 @@ import { Registry, StateManager } from '@sunmao-ui/runtime';
import { EventBusType } from './eventBus';
import { AppStorage } from './AppStorage';
import { SchemaValidator } from '../validator';
import { removeModuleId } from '../utils/addModuleId';
import { DataSourceType } from '../components/DataSource';
import { genOperation } from '../operations';
import { ExplorerMenuTabs, ToolMenuTabs } from '../constants/enum';
@ -27,7 +26,7 @@ enum DataSourceName {
LOCALSTORAGE = 'localStorage',
}
type DataSourceId = `${DataSourceName}${number}`
type DataSourceId = `${DataSourceName}${number}`;
export class EditorStore {
components: ComponentSchema[] = [];
// currentEditingComponents, it could be app's or module's components
@ -93,7 +92,11 @@ export class EditorStore {
this.eventBus.send('componentsRefresh', this.originComponents);
this.setComponents(this.originComponents);
if (this.APICount === -1 || this.stateCount === -1 || this.localStorageCount === -1) {
if (
this.APICount === -1 ||
this.stateCount === -1 ||
this.localStorageCount === -1
) {
this.initDataSourceCount();
}
}
@ -122,6 +125,10 @@ export class EditorStore {
return this.appStorage.modules;
}
get rawModules() {
return this.appStorage.rawModules;
}
get selectedComponent() {
return this.components.find(c => c.id === this._selectedComponentId);
}
@ -153,10 +160,7 @@ export class EditorStore {
m.version === this.currentEditingTarget.version &&
m.metadata.name === this.currentEditingTarget.name
);
if (module) {
return removeModuleId(module).impl;
}
return [];
return module?.impl || [];
case 'app':
return this.app.spec.components;
}
@ -189,8 +193,11 @@ export class EditorStore {
clearSunmaoGlobalState() {
this.stateManager.clear();
// reregister all modules
this.modules.forEach(m => {
this.setSelectedComponentId('');
// Remove old modules and re-register all modules,
this.registry.unregisterAllModules();
this.rawModules.forEach(m => {
const modules = createModule(m);
this.registry.registerModule(modules, true);
});
@ -258,7 +265,11 @@ export class EditorStore {
const { apis, states, localStorages } = this.dataSources;
let id: DataSourceId;
const getCount = (dataSource: ComponentSchema[], dataSourceName: DataSourceName, count: number): number => {
const getCount = (
dataSource: ComponentSchema[],
dataSourceName: DataSourceName,
count: number
): number => {
const ids = dataSource.map(({ id }) => id);
let id: DataSourceId = `${dataSourceName}${count}`;
while (ids.includes(id)) {
@ -277,7 +288,11 @@ export class EditorStore {
id = `state${this.stateCount}`;
break;
case DataSourceType.LOCALSTORAGE:
this.localStorageCount = getCount(localStorages, DataSourceName.LOCALSTORAGE, this.localStorageCount);
this.localStorageCount = getCount(
localStorages,
DataSourceName.LOCALSTORAGE,
this.localStorageCount
);
id = `localStorage${this.localStorageCount}`;
break;
}

View File

@ -1,4 +1,8 @@
import { StateManager, parseExpression, ExpressionError } from '../src/services/StateManager';
import {
StateManager,
parseExpression,
ExpressionError,
} from '../src/services/StateManager';
describe('parseExpression function', () => {
it('can parse {{}} expression', () => {
@ -79,44 +83,78 @@ describe('evalExpression function', () => {
value: 'ok',
},
};
const stateStore = new StateManager();
const stateManager = new StateManager();
it('can eval {{}} expression', () => {
expect(stateStore.maskedEval('value', false, scope)).toEqual('value');
expect(stateStore.maskedEval('{{true}}', false, scope)).toEqual(true);
expect(stateStore.maskedEval('{{ false }}', false, scope)).toEqual(false);
expect(stateStore.maskedEval('{{[]}}', false, scope)).toEqual([]);
expect(stateStore.maskedEval('{{ [] }}', false, scope)).toEqual([]);
expect(stateStore.maskedEval('{{ [1,2,3] }}', false, scope)).toEqual([1, 2, 3]);
const evalOptions = { evalListItem: false, scopeObject: scope };
expect(stateStore.maskedEval('{{ {} }}', false, scope)).toEqual({});
expect(stateStore.maskedEval('{{ {id: 123} }}', false, scope)).toEqual({ id: 123 });
expect(stateStore.maskedEval('{{nothing}}', false, scope) instanceof ExpressionError).toEqual(true);
expect(stateManager.maskedEval('value', evalOptions)).toEqual('value');
expect(stateManager.maskedEval('{{true}}', evalOptions)).toEqual(true);
expect(stateManager.maskedEval('{{ false }}', evalOptions)).toEqual(false);
expect(stateManager.maskedEval('{{[]}}', evalOptions)).toEqual([]);
expect(stateManager.maskedEval('{{ [] }}', evalOptions)).toEqual([]);
expect(stateManager.maskedEval('{{ [1,2,3] }}', evalOptions)).toEqual([1, 2, 3]);
expect(stateStore.maskedEval('{{input1.value}}', false, scope)).toEqual('world');
expect(stateStore.maskedEval('{{checkbox.value}}', false, scope)).toEqual(true);
expect(stateStore.maskedEval('{{fetch.data}}', false, scope)).toMatchObject([
expect(stateManager.maskedEval('{{ {} }}', evalOptions)).toEqual({});
expect(stateManager.maskedEval('{{ {id: 123} }}', evalOptions)).toEqual({ id: 123 });
expect(stateManager.maskedEval('{{nothing}}', evalOptions)).toBeInstanceOf(
ExpressionError
);
expect(stateManager.maskedEval('{{input1.value}}', evalOptions)).toEqual('world');
expect(stateManager.maskedEval('{{checkbox.value}}', evalOptions)).toEqual(true);
expect(stateManager.maskedEval('{{fetch.data}}', evalOptions)).toMatchObject([
{ id: 1 },
{ id: 2 },
]);
expect(stateStore.maskedEval('{{{{}}}}', false, scope)).toEqual(undefined);
expect(stateManager.maskedEval('{{{{}}}}', evalOptions)).toEqual(undefined);
expect(
stateStore.maskedEval('{{ value }}, {{ input1.value }}!', false, scope)
stateManager.maskedEval('{{ value }}, {{ input1.value }}!', evalOptions)
).toEqual('Hello, world!');
});
it('can eval $listItem expression', () => {
expect(stateStore.maskedEval('{{ $listItem.value }}', false, scope)).toEqual(
'{{ $listItem.value }}'
);
expect(stateStore.maskedEval('{{ $listItem.value }}', true, scope)).toEqual('foo');
expect(
stateStore.maskedEval(
stateManager.maskedEval('{{ $listItem.value }}', {
evalListItem: false,
scopeObject: scope,
})
).toEqual('{{ $listItem.value }}');
expect(
stateManager.maskedEval('{{ $listItem.value }}', {
evalListItem: true,
scopeObject: scope,
})
).toEqual('foo');
expect(
stateManager.maskedEval(
'{{ {{$listItem.value}}Input.value + {{$moduleId}}Fetch.value }}!',
true,
scope
{ evalListItem: true, scopeObject: scope }
)
).toEqual('Yes, ok!');
});
it('can override scope', () => {
expect(
stateManager.maskedEval('{{value}}', {
scopeObject: { override: 'foo' },
overrideScope: true,
})
).toBeInstanceOf(ExpressionError);
expect(
stateManager.maskedEval('{{override}}', {
scopeObject: { override: 'foo' },
overrideScope: true,
})
).toEqual('foo');
});
it('can fallback to specific value when error', () => {
expect(
stateManager.maskedEval('{{wrongExp}}', {
fallbackWhenError: exp => exp,
})
).toEqual('{{wrongExp}}');
});
});

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/runtime",
"version": "0.5.6",
"version": "0.5.7",
"description": "sunmao-ui runtime",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -31,7 +31,7 @@
"dependencies": {
"@emotion/css": "^11.7.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/core": "^0.5.5",
"@vue/reactivity": "^3.1.5",
"@vue/shared": "^3.2.20",
"copy-to-clipboard": "^3.3.1",

View File

@ -51,6 +51,7 @@ export const App: React.FC<AppProps> = props => {
app={app}
gridCallbacks={gridCallbacks}
hooks={hooks}
isInModule={false}
/>
);
})}

View File

@ -1,13 +1,12 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { merge } from 'lodash-es';
import { RuntimeComponentSchema, RuntimeTraitSchema } from '@sunmao-ui/core';
import { ExpressionError } from '../../services/StateManager';
import { watch } from '../../utils/watchReactivity';
import { ImplWrapperProps, TraitResult } from '../../types';
import { shallowCompareArray } from '../../utils/shallowCompareArray';
const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props, ref) => {
const { component: c, app, children, services, childrenMap, hooks } = props;
const { component: c, app, children, services, childrenMap, hooks, isInModule } = props;
const { registry, stateManager, globalHandlerMap, apiService, eleMap } = props.services;
const childrenCache = new Map<RuntimeComponentSchema, React.ReactElement>();
@ -20,18 +19,24 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
const handlerMap = useRef(globalHandlerMap.get(c.id)!);
const eleRef = useRef<HTMLElement>();
const onRef = (ele: HTMLElement) => {
eleMap.set(c.id, ele);
// If a component is in module, it should not have mask, so we needn't set it
if (!isInModule) {
eleMap.set(c.id, ele);
}
hooks?.didDomUpdate && hooks?.didDomUpdate();
};
useEffect(() => {
if (eleRef.current) {
// If a component is in module, it should not have mask, so we needn't set it
if (eleRef.current && !isInModule) {
eleMap.set(c.id, eleRef.current);
}
return () => {
eleMap.delete(c.id);
if (!isInModule) {
eleMap.delete(c.id);
}
};
}, [c.id, eleMap]);
}, [c.id, eleMap, isInModule]);
useEffect(() => {
const handler = (s: { componentId: string; name: string; parameters?: any }) => {
@ -84,7 +89,11 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
);
// result returned from traits
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>([]);
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>(() => {
return c.traits.map(trait =>
executeTrait(trait, stateManager.deepEval(trait.properties))
);
});
// eval traits' properties then execute traits
useEffect(() => {
@ -102,7 +111,8 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
return newResults;
});
stops.push(stop);
}
},
{ fallbackWhenError: () => undefined }
);
properties.push(result);
});
@ -121,11 +131,15 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
}
let effects = prevProps?.effects || [];
let unmountEffects = prevProps?.unmountEffects || [];
if (result.props?.effects) {
effects = effects?.concat(result.props?.effects);
}
if (result.props?.unmountEffects) {
unmountEffects = unmountEffects?.concat(result.props?.unmountEffects);
}
return merge(prevProps, result.props, { effects });
return merge(prevProps, result.props, { effects, unmountEffects });
},
{} as TraitResult<string, string>['props']
);
@ -134,7 +148,10 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
// component properties
const [evaledComponentProperties, setEvaledComponentProperties] = useState(() => {
return merge(stateManager.deepEval(c.properties), propsFromTraits);
return merge(
stateManager.deepEval(c.properties, { fallbackWhenError: () => undefined }),
propsFromTraits
);
});
// eval component properties
useEffect(() => {
@ -142,7 +159,8 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
c.properties,
({ result: newResult }: any) => {
setEvaledComponentProperties({ ...newResult });
}
},
{ fallbackWhenError: () => undefined }
);
// must keep this line, reason is the same as above
setEvaledComponentProperties({ ...result });
@ -150,22 +168,18 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
return stop;
}, [c.properties, stateManager]);
useEffect(() => {
if (unmount) {
delete stateManager.store[c.id];
}
return () => {
delete stateManager.store[c.id];
};
}, [c.id, stateManager.store]);
const mergedProps = useMemo(() => {
const allProps: Record<string, any> = { ...evaledComponentProperties, ...propsFromTraits };
for (const key in allProps) {
if (allProps[key] instanceof ExpressionError) {
allProps[key] = undefined;
}
}
return allProps;
}, [evaledComponentProperties, propsFromTraits]);
}, [c.id, stateManager.store, unmount]);
const mergedProps = useMemo(
() => ({ ...evaledComponentProperties, ...propsFromTraits }),
[evaledComponentProperties, propsFromTraits]
);
function genSlotsElements() {
if (!childrenMap[c.id]) {

View File

@ -1,5 +1,5 @@
import { Static } from '@sinclair/typebox';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { get } from 'lodash-es';
import { useDeepCompareMemo } from 'use-deep-compare';
import {
@ -17,6 +17,7 @@ import {
ModuleSpec,
} from '../../types';
import { resolveChildrenMap } from '../../utils/resolveChildrenMap';
import { initStateAndMethod } from '../../utils/initStateAndMethod';
import { ExpressionError } from '../../services/StateManager';
type Props = Static<typeof ModuleSpec> & {
@ -29,7 +30,7 @@ export const ModuleRenderer = React.forwardRef<HTMLDivElement, Props>((props, re
const { type, services } = props;
try {
const moduleSpec = services.registry.getModuleByType(type);
return <ModuleRendererContent {...props} moduleSpec={moduleSpec} />;
return <ModuleRendererContent {...props} ref={ref} moduleSpec={moduleSpec} />;
} catch {
return <div ref={ref}>Cannot find Module {type}.</div>;
}
@ -40,32 +41,20 @@ const ModuleRendererContent = React.forwardRef<
Props & { moduleSpec: ImplementedRuntimeModule }
>((props, ref) => {
const { moduleSpec, properties, handlers, evalScope, services, app } = props;
const moduleId = services.stateManager.maskedEval(props.id, true, evalScope) as string | ExpressionError;
const evalOptions = { evalListItem: true, scopeObject: evalScope };
const moduleId = services.stateManager.maskedEval(props.id, evalOptions) as
| string
| ExpressionError;
function evalObject<T extends Record<string, any>>(obj: T): T {
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
if (typeof value === 'string') {
return services.stateManager.maskedEval(value, true, evalScope);
return services.stateManager.maskedEval(value, evalOptions);
}
return value;
}).obj;
}
const evalWithScope = useCallback(
<T extends Record<string, any>>(obj: T, scope: Record<string, any>): T => {
const hasScopeKey = (exp: string) => {
return Object.keys(scope).some(key => exp.includes('{{') && exp.includes(key));
};
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
if (typeof value === 'string' && hasScopeKey(value)) {
return services.stateManager.maskedEval(value, true, scope);
}
return value;
}).obj;
},
[services.stateManager]
);
// first eval the property, handlers, id of module
const evaledProperties = evalObject(properties);
const parsedTemplate = useMemo(
@ -76,16 +65,28 @@ const ModuleRendererContent = React.forwardRef<
// then eval the template and stateMap of module
const evaledStateMap = useMemo(() => {
// stateMap only use state i
return evalWithScope(moduleSpec.spec.stateMap, { $moduleId: moduleId });
}, [evalWithScope, moduleSpec.spec.stateMap, moduleId]);
return services.stateManager.deepEval(moduleSpec.spec.stateMap, {
evalListItem: false,
scopeObject: { $moduleId: moduleId },
overrideScope: true,
});
}, [services.stateManager, moduleSpec.spec.stateMap, moduleId]);
const evaledModuleTemplate = useDeepCompareMemo(() => {
// here should only eval with evaledProperties, any other key not in evaledProperties should be ignored
// so we can assume that template will not change if evaledProperties is the same
return evalWithScope(parsedTemplate, {
...evaledProperties,
$moduleId: moduleId,
});
return services.stateManager.deepEval(
{ template: parsedTemplate },
{
evalListItem: false,
scopeObject: {
...evaledProperties,
$moduleId: moduleId,
},
overrideScope: true,
fallbackWhenError: exp => exp,
}
).template;
}, [parsedTemplate, evaledProperties, moduleId]);
// listen component state change
@ -128,10 +129,10 @@ const ModuleRendererContent = React.forwardRef<
if (!handlers) return;
const _handlers = handlers as Array<Static<typeof EventHandlerSpec>>;
const moduleEventHandlers: any[] = [];
_handlers.forEach((h) => {
_handlers.forEach(h => {
const moduleEventHandler = ({ fromId, eventType }: Record<string, string>) => {
if (eventType === h.type && fromId === moduleId) {
const evaledHandler = services.stateManager.deepEval(h, true, evalScope);
const evaledHandler = services.stateManager.deepEval(h, evalOptions);
services.apiService.send('uiMethod', {
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,
@ -151,6 +152,8 @@ const ModuleRendererContent = React.forwardRef<
}, [evalScope, handlers, moduleId, services.apiService, services.stateManager]);
const result = useMemo(() => {
// Must init components' state, otherwise store cannot listen these components' state changing
initStateAndMethod(services.registry, services.stateManager, evaledModuleTemplate);
const { childrenMap, topLevelComponents } = resolveChildrenMap(evaledModuleTemplate);
return topLevelComponents.map(c => {
return (
@ -160,12 +163,17 @@ const ModuleRendererContent = React.forwardRef<
services={services}
app={app}
childrenMap={childrenMap}
isInModule={true}
/>
);
});
}, [evaledModuleTemplate, services, app]);
return <div ref={ref}>{result}</div>;
return (
<div className="module-container" ref={ref}>
{result}
</div>
);
});
function parseTypeComponents(

View File

@ -24,12 +24,17 @@ export default implementRuntimeComponent({
styleSlots: [],
events: [],
},
})(({ effects }) => {
})(({ effects, unmountEffects }) => {
useEffect(() => {
return () => {
effects?.forEach(e => e());
};
}, [effects]);
useEffect(() => {
return () => {
unmountEffects?.forEach(e => e());
};
}, []);
return null;
});

View File

@ -29,10 +29,10 @@ export default implementRuntimeComponent({
},
})(({ id, type, properties, handlers, services, app, elementRef }) => {
if (!type) {
return <span>Please choose a module to render.</span>;
return <span ref={elementRef}>Please choose a module to render.</span>;
}
if (!id) {
return <span>Please set a id for module.</span>;
return <span ref={elementRef}>Please set a id for module.</span>;
}
return (

View File

@ -170,6 +170,10 @@ export class Registry {
return this.getModule(version, name);
}
unregisterAllModules() {
this.modules = new Map<string, Map<string, ImplementedRuntimeModule>>();
}
registerUtilMethod<T>(m: UtilMethod<T>) {
if (this.utilMethods.get(m.name)) {
throw new Error(`Already has utilMethod ${m.name} in this registry.`);

View File

@ -16,6 +16,12 @@ dayjs.extend(LocalizedFormat);
dayjs.locale('zh-cn');
type ExpChunk = string | ExpChunk[];
type EvalOptions = {
evalListItem?: boolean;
scopeObject?: Record<string, any>;
overrideScope?: boolean;
fallbackWhenError?: (exp: string) => any;
};
// TODO: use web worker
const DefaultDependencies = {
@ -43,12 +49,13 @@ export class StateManager {
this.store = reactive<Record<string, any>>({});
};
evalExp = (expChunk: ExpChunk, scopeObject = {}): unknown => {
evalExp = (expChunk: ExpChunk, options: EvalOptions): unknown => {
if (typeof expChunk === 'string') {
return expChunk;
}
const evalText = expChunk.map(ex => this.evalExp(ex, scopeObject)).join('');
const { scopeObject = {}, overrideScope = false } = options;
const evalText = expChunk.map(ex => this.evalExp(ex, { scopeObject })).join('');
// eslint-disable-next-line no-useless-call, no-new-func
const evaled = new Function(
@ -58,12 +65,20 @@ export class StateManager {
/^\s+/g,
''
)} } } }`
).call(null, this.store, this.dependencies, scopeObject);
).call(
null,
overrideScope ? {} : this.store,
overrideScope ? {} : this.dependencies,
scopeObject
);
return evaled;
};
maskedEval(raw: string, evalListItem = false, scopeObject = {}): unknown | ExpressionError {
maskedEval(raw: string, options: EvalOptions = {}): unknown | ExpressionError {
const { evalListItem = false, fallbackWhenError } = options;
let result: unknown[] = [];
try {
if (isNumeric(raw)) {
return toNumber(raw);
@ -75,12 +90,13 @@ export class StateManager {
return false;
}
const expChunk = parseExpression(raw, evalListItem);
if (typeof expChunk === 'string') {
return expChunk;
}
const result = expChunk.map(e => this.evalExp(e, scopeObject));
result = expChunk.map(e => this.evalExp(e, options));
if (result.length === 1) {
return result[0];
}
@ -88,12 +104,10 @@ export class StateManager {
} catch (error) {
if (error instanceof Error) {
const expressionError = new ExpressionError(error.message);
console.error(expressionError);
return expressionError;
return fallbackWhenError ? fallbackWhenError(raw) : expressionError;
}
return undefined;
}
}
@ -121,17 +135,13 @@ export class StateManager {
}) as T;
}
deepEval<T extends Record<string, unknown>>(
obj: T,
evalListItem = false,
scopeObject = {}
): T {
deepEval<T extends Record<string, unknown>>(obj: T, options: EvalOptions = {}): T {
// just eval
const evaluated = this.mapValuesDeep(obj, ({ value }) => {
if (typeof value !== 'string') {
return value;
}
return this.maskedEval(value, evalListItem, scopeObject);
return this.maskedEval(value, options);
});
return evaluated;
@ -140,13 +150,12 @@ export class StateManager {
deepEvalAndWatch<T extends Record<string, unknown>>(
obj: T,
watcher: (params: { result: T }) => void,
evalListItem = false,
scopeObject = {}
options: EvalOptions = {}
) {
const stops: ReturnType<typeof watch>[] = [];
// just eval
const evaluated = this.deepEval(obj, evalListItem, scopeObject);
const evaluated = this.deepEval(obj, options);
// watch change
let resultCache: T = evaluated;
@ -159,7 +168,7 @@ export class StateManager {
const stop = watch(
() => {
const result = this.maskedEval(value, evalListItem, scopeObject);
const result = this.maskedEval(value, options);
return result;
},

View File

@ -9,7 +9,6 @@ import { generateCallback } from './Event';
const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSpec>> =
() => {
const hasFetchedMap = new Map<string, boolean>();
return ({
trait,
url,
@ -24,14 +23,13 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSpec
services,
subscribeMethods,
componentId,
disabled,
}) => {
const hashId = `#${componentId}@${'fetch'}`;
const hasFetched = hasFetchedMap.get(hashId);
const lazy = _lazy === undefined ? true : _lazy;
const fetchData = () => {
if (disabled) return;
// TODO: clear when component destroy
hasFetchedMap.set(hashId, true);
// FIXME: listen to the header change
const headers = new Headers();
if (_headers) {
@ -143,7 +141,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSpec
typeof FetchTraitPropertiesSpec
>['onError'];
rawOnError?.forEach(handler => {
const evaledHandler = services.stateManager.deepEval(handler, false);
const evaledHandler = services.stateManager.deepEval(handler, { evalListItem: false });
services.apiService.send('uiMethod', {
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,
@ -155,7 +153,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSpec
};
// non lazy query, listen to the change and query;
if (!lazy && url && !hasFetched) {
if (!lazy && url) {
fetchData();
}
@ -166,13 +164,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSpec
});
return {
props: {
effects: [
() => {
hasFetchedMap.set(hashId, false);
},
],
},
props: null,
};
};
};

View File

@ -27,7 +27,11 @@ const StateTraitFactory: TraitImplFactory<Static<typeof PropsSpec>> = () => {
}
return {
props: null,
props: {
unmountEffects: [() => {
HasInitializedMap.delete(hashId);
}]
},
};
};
};

View File

@ -12,6 +12,7 @@ export type ImplWrapperProps<KSlot extends string = string> = {
component: RuntimeComponentSchema;
childrenMap: ChildrenMap<KSlot>;
services: UIServices;
isInModule: boolean;
app?: RuntimeApplication;
} & ComponentParamsFromApp;

View File

@ -14,13 +14,14 @@ export const ModuleSpec = Type.Object({
properties: Type.Record(Type.String(), Type.Any(), {
title: 'Module Properties',
category: 'Basic',
widget: 'core/v1/KeyValue',
}),
handlers: Type.Array(EventHandlerSpec, {
title: 'Module Handlers',
category: 'Basic',
}),
}, {
category: 'Appearance',
category: 'Basic',
});
export type ImplementedRuntimeModule = RuntimeModule;

View File

@ -8,6 +8,7 @@ export type TraitResult<KStyleSlot extends string, KEvent extends string> = {
customStyle?: Record<KStyleSlot, string>;
callbackMap?: CallbackMap<KEvent>;
effects?: Array<() => void>;
unmountEffects?: Array<() => void>;
} | null;
unmount?: boolean;
};

View File

@ -50,6 +50,7 @@ export const FetchTraitPropertiesSpec = Type.Object({
{ 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',
}),

View File

@ -1,6 +1,5 @@
import { RuntimeComponentSchema } from '@sunmao-ui/core';
import { ChildrenMap } from '../types';
import { isFetchTraitComponent } from '../utils/trait';
export function resolveChildrenMap(components: RuntimeComponentSchema[]): {
childrenMap: ChildrenMap<string>;
@ -8,16 +7,11 @@ export function resolveChildrenMap(components: RuntimeComponentSchema[]): {
} {
const childrenMap: ChildrenMap<string> = {};
const topLevelComponents: RuntimeComponentSchema[] = [];
const fetchTraitComponents: RuntimeComponentSchema[] = [];
for (const c of components) {
const slotTrait = c.traits.find(t => t.parsedType.name === 'slot');
if (!slotTrait) {
if (isFetchTraitComponent(c)) {
fetchTraitComponents.push(c);
} else {
topLevelComponents.push(c);
}
topLevelComponents.push(c);
continue;
}
const { id, slot } = slotTrait.properties.container as any;
@ -64,6 +58,6 @@ export function resolveChildrenMap(components: RuntimeComponentSchema[]): {
return {
childrenMap,
topLevelComponents: [...topLevelComponents, ...fetchTraitComponents],
topLevelComponents,
};
}

View File

@ -1,5 +0,0 @@
import { ComponentSchema } from '@sunmao-ui/core';
export function isFetchTraitComponent(component: ComponentSchema): boolean {
return component.type === 'core/v1/dummy' && component.traits.filter(trait=> trait.type === 'core/v1/fetch').length > 0;
}