mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
Merge branch 'main' of https://github.com/webzard-io/sunmao-ui into feat/shared
This commit is contained in:
commit
f304529899
@ -10,7 +10,7 @@ describe('trait', () => {
|
||||
},
|
||||
|
||||
spec: {
|
||||
properties: [{ name: 'width', type: 'number' }],
|
||||
properties: { name: 'width', type: 'number' },
|
||||
state: {
|
||||
type: 'string',
|
||||
},
|
||||
@ -29,6 +29,7 @@ describe('trait', () => {
|
||||
Object {
|
||||
"kind": "Trait",
|
||||
"metadata": Object {
|
||||
"annotations": Object {},
|
||||
"description": "",
|
||||
"name": "test_trait",
|
||||
},
|
||||
@ -45,12 +46,10 @@ describe('trait', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
"properties": Array [
|
||||
Object {
|
||||
"name": "width",
|
||||
"type": "number",
|
||||
},
|
||||
],
|
||||
"properties": Object {
|
||||
"name": "width",
|
||||
"type": "number",
|
||||
},
|
||||
"state": Object {
|
||||
"type": "string",
|
||||
},
|
||||
|
@ -5,10 +5,12 @@ import { parseVersion, Version } from './version';
|
||||
|
||||
// spec
|
||||
|
||||
type TraitMetaData = Metadata<{ beforeRender?: boolean }>;
|
||||
|
||||
export type Trait = {
|
||||
version: string;
|
||||
kind: 'Trait';
|
||||
metadata: Metadata;
|
||||
metadata: TraitMetaData;
|
||||
spec: TraitSpec;
|
||||
};
|
||||
|
||||
@ -26,7 +28,7 @@ export type RuntimeTrait = Trait & {
|
||||
// partial some fields, use as param createModule
|
||||
export type CreateTraitOptions = {
|
||||
version: string;
|
||||
metadata: Metadata;
|
||||
metadata: TraitMetaData;
|
||||
spec?: Partial<TraitSpec>;
|
||||
};
|
||||
|
||||
@ -39,6 +41,7 @@ export function createTrait(options: CreateTraitOptions): RuntimeTrait {
|
||||
metadata: {
|
||||
name: options.metadata.name,
|
||||
description: options.metadata.description || '',
|
||||
annotations: options.metadata.annotations || {},
|
||||
},
|
||||
spec: {
|
||||
properties: {},
|
||||
|
@ -117,6 +117,14 @@ export const EditorMask: React.FC<Props> = observer((props: Props) => {
|
||||
[resizeObserver]
|
||||
);
|
||||
|
||||
// because this useEffect would run after sunmao didMount hook, so it cannot subscribe the first HTMLElementsUpdated event
|
||||
// we should call the callback function after first render
|
||||
useEffect(() => {
|
||||
observeResize(eleMap);
|
||||
updateCoordinateSystem(eleMap);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eventBus.on('HTMLElementsUpdated', () => {
|
||||
observeResize(eleMap);
|
||||
|
@ -51,7 +51,6 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
|
||||
);
|
||||
|
||||
const didMount = () => {
|
||||
editorStore.eleMap = ui.eleMap;
|
||||
eventBus.send('HTMLElementsUpdated');
|
||||
};
|
||||
const didUpdate = () => {
|
||||
@ -61,7 +60,10 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
|
||||
eventBus.send('HTMLElementsUpdated');
|
||||
};
|
||||
|
||||
const ui = initSunmaoUI({ ...props.runtimeProps, hooks: { didMount, didUpdate, didDomUpdate } });
|
||||
const ui = initSunmaoUI({
|
||||
...props.runtimeProps,
|
||||
hooks: { didMount, didUpdate, didDomUpdate },
|
||||
});
|
||||
|
||||
const App = ui.App;
|
||||
const registry = ui.registry;
|
||||
@ -84,6 +86,8 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
|
||||
);
|
||||
const widgetManager = new WidgetManager();
|
||||
const editorStore = new EditorStore(eventBus, registry, stateManager, appStorage);
|
||||
editorStore.eleMap = ui.eleMap;
|
||||
|
||||
const services = {
|
||||
App,
|
||||
registry: ui.registry,
|
||||
@ -97,7 +101,7 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
|
||||
|
||||
const Editor: React.FC = () => {
|
||||
const [store, setStore] = useState(stateManager.store);
|
||||
const onRefresh = useCallback(()=> {
|
||||
const onRefresh = useCallback(() => {
|
||||
setStore(stateManager.store);
|
||||
}, []);
|
||||
|
||||
@ -111,7 +115,7 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
|
||||
services={services}
|
||||
libs={props.libs || []}
|
||||
onRefresh={onRefresh}
|
||||
uiProps={props.uiProps||{}}
|
||||
uiProps={props.uiProps || {}}
|
||||
/>
|
||||
</ChakraProvider>
|
||||
);
|
||||
|
@ -28,7 +28,7 @@ describe('evalExpression function', () => {
|
||||
};
|
||||
const stateManager = new StateManager();
|
||||
it('can eval {{}} expression', () => {
|
||||
const evalOptions = { evalListItem: false, scopeObject: scope };
|
||||
const evalOptions = { evalListItem: false, scopeObject: scope, noConsoleError: true };
|
||||
|
||||
expect(stateManager.maskedEval('value', evalOptions)).toEqual('value');
|
||||
expect(stateManager.maskedEval('{{true}}', evalOptions)).toEqual(true);
|
||||
@ -83,6 +83,7 @@ describe('evalExpression function', () => {
|
||||
stateManager.maskedEval('{{value}}', {
|
||||
scopeObject: { override: 'foo' },
|
||||
overrideScope: true,
|
||||
noConsoleError: true,
|
||||
})
|
||||
).toBeInstanceOf(ExpressionError);
|
||||
expect(
|
||||
@ -97,6 +98,7 @@ describe('evalExpression function', () => {
|
||||
expect(
|
||||
stateManager.maskedEval('{{wrongExp}}', {
|
||||
fallbackWhenError: exp => exp,
|
||||
noConsoleError: true,
|
||||
})
|
||||
).toEqual('{{wrongExp}}');
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { ImplWrapperProps } from '../../../types';
|
||||
import { shallowCompareArray } from '@sunmao-ui/shared';
|
||||
import { ImplWrapperMain } from './ImplWrapperMain';
|
||||
import { UnmountImplWrapper } from './UnmountImplWrapper';
|
||||
|
||||
export const ImplWrapper = React.memo<ImplWrapperProps>(
|
||||
ImplWrapperMain,
|
||||
UnmountImplWrapper,
|
||||
(prevProps, nextProps) => {
|
||||
const prevChildren = prevProps.childrenMap[prevProps.component.id]?._grandChildren;
|
||||
const nextChildren = nextProps.childrenMap[nextProps.component.id]?._grandChildren;
|
||||
|
@ -49,9 +49,9 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
|
||||
newResults[i] = traitResult;
|
||||
return newResults;
|
||||
});
|
||||
stops.push(stop);
|
||||
}
|
||||
);
|
||||
stops.push(stop);
|
||||
properties.push(result);
|
||||
});
|
||||
// although traitResults has initialized in useState, it must be set here again
|
||||
|
@ -0,0 +1,65 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { RuntimeTraitSchema } from '@sunmao-ui/core';
|
||||
import { ImplWrapperMain } from './ImplWrapperMain';
|
||||
import { useRuntimeFunctions } from './hooks/useRuntimeFunctions';
|
||||
import { initSingleComponentState } from '../../../utils/initStateAndMethod';
|
||||
import { ImplWrapperProps, TraitResult } from '../../../types';
|
||||
import { watch } from '../../..';
|
||||
|
||||
export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
|
||||
(props, ref) => {
|
||||
const { component: c, services } = props;
|
||||
const { stateManager, registry } = services;
|
||||
const { executeTrait } = useRuntimeFunctions(props);
|
||||
|
||||
const unmountTraits = useMemo(
|
||||
() => c.traits.filter(t => registry.getTraitByType(t.type).metadata.annotations?.beforeRender),
|
||||
[c.traits, registry]
|
||||
);
|
||||
|
||||
const [isHidden, setIsHidden] = useState(() => {
|
||||
const results: TraitResult<string, string>[] = unmountTraits.map(t => {
|
||||
const properties = stateManager.deepEval(t.properties);
|
||||
return executeTrait(t, properties);
|
||||
});
|
||||
return results.some(result => result.unmount);
|
||||
});
|
||||
|
||||
const traitChangeCallback = useCallback(
|
||||
(trait: RuntimeTraitSchema, properties: Record<string, unknown>) => {
|
||||
const result = executeTrait(trait, properties);
|
||||
setIsHidden(!!result.unmount);
|
||||
if (result.unmount) {
|
||||
// Every component's state is initialized in initStateAnd Method.
|
||||
// So if if it should not render, we should remove it from store.
|
||||
delete stateManager.store[c.id];
|
||||
}
|
||||
},
|
||||
[c.id, executeTrait, stateManager]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const stops: ReturnType<typeof watch>[] = [];
|
||||
if (unmountTraits.length > 0) {
|
||||
unmountTraits.forEach(t => {
|
||||
const { result, stop } = stateManager.deepEvalAndWatch(t.properties, newValue =>
|
||||
traitChangeCallback(t, newValue.result)
|
||||
);
|
||||
traitChangeCallback(t, result);
|
||||
stops.push(stop);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
stops.forEach(stop => stop());
|
||||
};
|
||||
}, [c, executeTrait, unmountTraits, stateManager, traitChangeCallback]);
|
||||
|
||||
// If a component is unmount, its state would be removed.
|
||||
// So if it mount again, we should init its state again.
|
||||
if (!isHidden && !stateManager.store[c.id]) {
|
||||
initSingleComponentState(registry, stateManager, c);
|
||||
}
|
||||
|
||||
return !isHidden ? <ImplWrapperMain {...props} ref={ref} /> : null;
|
||||
}
|
||||
);
|
@ -28,7 +28,7 @@ export function useGlobalHandlerMap(props: ImplWrapperProps) {
|
||||
|
||||
return () => {
|
||||
apiService.off('uiMethod', handler);
|
||||
handlerMap.delete(c.id);
|
||||
globalHandlerMap.delete(c.id);
|
||||
};
|
||||
}, [apiService, c.id, globalHandlerMap]);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ type EvalOptions = {
|
||||
scopeObject?: Record<string, any>;
|
||||
overrideScope?: boolean;
|
||||
fallbackWhenError?: (exp: string) => any;
|
||||
noConsoleError?: boolean
|
||||
};
|
||||
|
||||
// TODO: use web worker
|
||||
@ -76,7 +77,7 @@ export class StateManager {
|
||||
};
|
||||
|
||||
maskedEval(raw: string, options: EvalOptions = {}): unknown | ExpressionError {
|
||||
const { evalListItem = false, fallbackWhenError } = options;
|
||||
const { evalListItem = false, fallbackWhenError, noConsoleError } = options;
|
||||
let result: unknown[] = [];
|
||||
|
||||
try {
|
||||
@ -104,8 +105,10 @@ export class StateManager {
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const expressionError = new ExpressionError(error.message);
|
||||
|
||||
consoleError(ConsoleType.Expression, '', expressionError.message);
|
||||
|
||||
if (!noConsoleError) {
|
||||
consoleError(ConsoleType.Expression, '', expressionError.message);
|
||||
}
|
||||
|
||||
return fallbackWhenError ? fallbackWhenError(raw) : expressionError;
|
||||
}
|
||||
|
@ -12,6 +12,9 @@ export default implementRuntimeTrait({
|
||||
metadata: {
|
||||
name: HIDDEN_TRAIT_NAME,
|
||||
description: 'render component with condition',
|
||||
annotations: {
|
||||
beforeRender: true,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
properties: HiddenTraitPropertiesSpec,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RuntimeApplication } from '@sunmao-ui/core';
|
||||
import { RuntimeComponentSchema } from '@sunmao-ui/core';
|
||||
import { Static, TSchema } from '@sinclair/typebox';
|
||||
import { RegistryInterface } from '../services/Registry';
|
||||
import { StateManagerInterface } from '../services/StateManager';
|
||||
@ -12,32 +12,38 @@ import {
|
||||
export function initStateAndMethod(
|
||||
registry: RegistryInterface,
|
||||
stateManager: StateManagerInterface,
|
||||
components: RuntimeApplication['spec']['components']
|
||||
components: RuntimeComponentSchema[]
|
||||
) {
|
||||
components.forEach(c => {
|
||||
if (stateManager.store[c.id]) {
|
||||
return false;
|
||||
}
|
||||
let state = {};
|
||||
c.traits.forEach(t => {
|
||||
const tSpec = registry.getTrait(t.parsedType.version, t.parsedType.name).spec;
|
||||
state = { ...state, ...parseTypeBox(tSpec.state as TSchema) };
|
||||
});
|
||||
const cSpec = registry.getComponent(c.parsedType.version, c.parsedType.name).spec;
|
||||
state = { ...state, ...parseTypeBox(cSpec.state as TSchema) };
|
||||
|
||||
stateManager.store[c.id] = state;
|
||||
|
||||
if (c.type === `${CORE_VERSION}/${MODULE_CONTAINER_COMPONENT_NAME}`) {
|
||||
const moduleSchema = c.properties as Static<typeof ModuleSpec>;
|
||||
try {
|
||||
const mSpec = registry.getModuleByType(moduleSchema.type).spec;
|
||||
const moduleInitState: Record<string, unknown> = {};
|
||||
for (const key in mSpec) {
|
||||
moduleInitState[key] = undefined;
|
||||
}
|
||||
stateManager.store[moduleSchema.id] = moduleInitState;
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
components.forEach(c => initSingleComponentState(registry, stateManager, c));
|
||||
}
|
||||
|
||||
export function initSingleComponentState(
|
||||
registry: RegistryInterface,
|
||||
stateManager: StateManagerInterface,
|
||||
c: RuntimeComponentSchema
|
||||
) {
|
||||
if (stateManager.store[c.id]) {
|
||||
return false;
|
||||
}
|
||||
let state = {};
|
||||
c.traits.forEach(t => {
|
||||
const tSpec = registry.getTrait(t.parsedType.version, t.parsedType.name).spec;
|
||||
state = { ...state, ...parseTypeBox(tSpec.state as TSchema) };
|
||||
});
|
||||
const cSpec = registry.getComponent(c.parsedType.version, c.parsedType.name).spec;
|
||||
state = { ...state, ...parseTypeBox(cSpec.state as TSchema) };
|
||||
|
||||
stateManager.store[c.id] = state;
|
||||
|
||||
if (c.type === `${CORE_VERSION}/${MODULE_CONTAINER_COMPONENT_NAME}`) {
|
||||
const moduleSchema = c.properties as Static<typeof ModuleSpec>;
|
||||
try {
|
||||
const mSpec = registry.getModuleByType(moduleSchema.type).spec;
|
||||
const moduleInitState: Record<string, unknown> = {};
|
||||
for (const key in mSpec) {
|
||||
moduleInitState[key] = undefined;
|
||||
}
|
||||
stateManager.store[moduleSchema.id] = moduleInitState;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user