mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
Merge branch 'main' of github.com:webzard-io/sunmao-ui into feat/arco
This commit is contained in:
commit
0efb3824ea
@ -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",
|
||||
|
@ -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 => {
|
||||
|
@ -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",
|
||||
|
@ -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: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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>;
|
||||
})}
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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()}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}}');
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -51,6 +51,7 @@ export const App: React.FC<AppProps> = props => {
|
||||
app={app}
|
||||
gridCallbacks={gridCallbacks}
|
||||
hooks={hooks}
|
||||
isInModule={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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]) {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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 (
|
||||
|
@ -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.`);
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -27,7 +27,11 @@ const StateTraitFactory: TraitImplFactory<Static<typeof PropsSpec>> = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
props: null,
|
||||
props: {
|
||||
unmountEffects: [() => {
|
||||
HasInitializedMap.delete(hashId);
|
||||
}]
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ export type ImplWrapperProps<KSlot extends string = string> = {
|
||||
component: RuntimeComponentSchema;
|
||||
childrenMap: ChildrenMap<KSlot>;
|
||||
services: UIServices;
|
||||
isInModule: boolean;
|
||||
app?: RuntimeApplication;
|
||||
} & ComponentParamsFromApp;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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',
|
||||
}),
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user