Merge branch 'main' of https://github.com/webzard-io/sunmao-ui into fix/warning-area

This commit is contained in:
MrWindlike 2022-01-27 17:44:56 +08:00
commit 4b99a4bc24
15 changed files with 78 additions and 24 deletions

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { sortBy } from 'lodash-es';
import { isArray, sortBy } from 'lodash-es';
import {
Table as BaseTable,
Thead,
@ -63,6 +63,7 @@ export const TableImpl = implementTable(
}, [data, updateSelectedItem, updateSelectedItems]);
const sortedData = useMemo(() => {
if (!isArray(data)) return [];
if (!sortRule) return data;
const sorted = sortBy(data, sortRule.key);
return sortRule.desc ? sorted.reverse() : sorted;

View File

@ -6,6 +6,7 @@ import {
ComponentType,
IAppModel,
IComponentModel,
ModuleId,
SlotName,
} from './IAppModel';
import { genComponent } from './utils';
@ -36,6 +37,10 @@ export class AppModel implements IAppModel {
return Object.values(this.componentMap);
}
get moduleIds(): ModuleId[] {
return this.allComponents.filter(c => c.type === 'core/v1/moduleContainer').map(c => c.properties.rawValue.id);
}
toSchema(): ComponentSchema[] {
this.schema = this.allComponents.map(c => {
return c.toSchema();

View File

@ -39,6 +39,7 @@ export type EventName = string & {
export interface IAppModel {
topComponents: IComponentModel[];
// modules: IModuleModel[];
moduleIds: ModuleId[];
// generated by traverse the tree. Component will be overwritten if its id is duplicated.
allComponents: IComponentModel[];
// all components, including orphan component

View File

@ -153,7 +153,7 @@ export const ComponentForm: React.FC<Props> = observer(props => {
</VStack>
</VStack>
<EventTraitForm component={selectedComponent} services={services} />
{ hasFetchTrait ? <FetchTraitForm component={selectedComponent} services={services} /> : null }
{ hasFetchTrait ? <FetchTraitForm key={selectedComponent.id} component={selectedComponent} services={services} /> : null }
<StyleTraitForm component={selectedComponent} services={services} />
<GeneralTraitFormList component={selectedComponent} services={services} />
</VStack>

View File

@ -13,7 +13,7 @@ import { Static } from '@sinclair/typebox';
import { CloseIcon } from '@chakra-ui/icons';
import { observer } from 'mobx-react-lite';
import { useFormik } from 'formik';
import { EventHandlerSchema } from '@sunmao-ui/runtime';
import { EventHandlerSchema, GLOBAL_UTILS_ID } from '@sunmao-ui/runtime';
import { formWrapperCSS } from '../style';
import { KeyValueEditor } from '../../KeyValueEditor';
import { EditorServices } from '../../../types';
@ -32,20 +32,26 @@ type Props = {
export const EventHandlerForm: React.FC<Props> = observer(props => {
const { handler, eventTypes, onChange, onRemove, hideEventType, services } = props;
const { registry, editorStore } = services;
const { utilMethods } = registry;
const { components } = editorStore;
const [methods, setMethods] = useState<string[]>([]);
const updateMethods = useCallback(
(componentId: string) => {
const component = components.find(c => c.id === componentId);
if (component) {
const componentModel = new ComponentModel(
new AppModel([], registry),
component,
registry
);
if (componentId === GLOBAL_UTILS_ID) {
setMethods(Array.from(utilMethods.keys()));
} else {
const component = components.find(c => c.id === componentId);
setMethods(componentModel.methods.map(m => m.name));
if (component) {
const componentModel = new ComponentModel(
new AppModel([], registry),
component,
registry
);
setMethods(componentModel.methods.map(m => m.name));
}
}
},
[components, registry]
@ -99,7 +105,7 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
onBlur={() => formik.submitForm()}
value={formik.values.componentId}
>
{components.map(c => (
{[{ id: GLOBAL_UTILS_ID }].concat(components).map(c => (
<option key={c.id} value={c.id}>
{c.id}
</option>

View File

@ -41,7 +41,7 @@ export const FetchTraitForm: React.FC<Props> = props => {
?.properties as Static<typeof FetchTraitPropertiesSchema>;
const formik = useFormik({
initialValues: fetchTrait,
initialValues: { onComplete: [], onError: [], ...fetchTrait },
onSubmit: values => {
const index = component.traits.findIndex(t => t.type === 'core/v1/fetch');
eventBus.send(

View File

@ -33,7 +33,7 @@ const ArrayField: React.FC<Props> = props => {
<>
{formData.map((v, idx) => {
return (
<Box key={idx} mb={2}>
<Box key={idx} mb={2} border='1px solid black' borderColor='gray.200' borderRadius='4' padding='8px'>
<ButtonGroup
spacing={0}
size="xs"

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import { Application } from '@sunmao-ui/core';
import { GridCallbacks, DIALOG_CONTAINER_ID, initSunmaoUI } from '@sunmao-ui/runtime';
import { Box, Tabs, TabList, Tab, TabPanels, TabPanel, Flex } from '@chakra-ui/react';
@ -35,6 +35,12 @@ 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 gridCallbacks: GridCallbacks = useMemo(() => {
return {
@ -83,7 +89,10 @@ export const Editor: React.FC<Props> = observer(
const appComponent = useMemo(() => {
return (
<ErrorBoundary>
<ErrorBoundary
key={recoverKey}
onError={onError}
>
<App
options={app}
debugEvent={false}
@ -93,7 +102,7 @@ export const Editor: React.FC<Props> = observer(
/>
</ErrorBoundary>
);
}, [App, ComponentWrapper, app, gridCallbacks]);
}, [App, ComponentWrapper, app, gridCallbacks, recoverKey]);
const renderMain = () => {
const appBox = (
@ -205,6 +214,12 @@ export const Editor: React.FC<Props> = observer(
);
};
useEffect(() => {
if (isError) {
setRecoverKey(recoverKey + 1);
}
}, [app]);
return (
<KeyboardEventWrapper
components={components}

View File

@ -1,10 +1,14 @@
import React from 'react';
type Props = {
onError?: (error: Error | null) => void;
}
class ErrorBoundary extends React.Component<
Record<string, unknown>,
Props,
{ error: unknown }
> {
constructor(props: Record<string, unknown>) {
constructor(props: Props) {
super(props);
this.state = { error: null };
}
@ -13,6 +17,14 @@ class ErrorBoundary extends React.Component<
return { error };
}
componentDidMount() {
this.props.onError?.(null);
}
componentDidCatch(error: Error) {
this.props.onError?.(error);
}
render() {
if (this.state.error) {
return String(this.state.error);

View File

@ -75,6 +75,7 @@ export const WarningArea: React.FC<Props> = observer(({ services }) => {
paddingY="2"
paddingX="4"
boxShadow="0 0 4px rgba(0, 0, 0, 0.1)"
background='white'
>
<HStack width="full" justifyContent="space-between">
<Text fontSize="md" fontWeight="bold">

View File

@ -1,5 +1,5 @@
import { get, has } from 'lodash-es';
import { ComponentId } from '../../AppModel/IAppModel';
import { ComponentId, ModuleId } from '../../AppModel/IAppModel';
import {
PropertiesValidatorRule,
PropertiesValidateContext,
@ -97,8 +97,11 @@ class ExpressionValidatorRule implements PropertiesValidatorRule {
break;
}
}
} else if (appModel.moduleIds.includes(id as ModuleId)) {
// case 3: id is a module
// TODO: check module stateMap
} else {
// case 3: id doesn't exist
// case 4: id doesn't exist
results.push({
message: `Cannot find '${id}' in store or window.`,
componentId: component.id,

View File

@ -3,3 +3,5 @@ export const LIST_ITEM_INDEX_EXP = '$i';
export const GRID_HEIGHT = 40;
export const DIALOG_CONTAINER_ID = 'sunmao-ui-dialog-container';
export const DROP_EXAMPLE_SIZE_PREFIX = 'exampleSize: ';
export const GLOBAL_UTILS_ID = '$utils';
export const GLOBAL_MODULE_ID = '$module';

View File

@ -1,4 +1,8 @@
import mitt from 'mitt';
import {
GLOBAL_UTILS_ID,
GLOBAL_MODULE_ID
} from '../constants';
export type ApiService = ReturnType<typeof initApiService>;
@ -32,13 +36,13 @@ function mountSystemMethods(apiService: ApiService) {
apiService.on('uiMethod', ({ componentId, name, parameters }) => {
switch (componentId) {
// hanlder as module event
case '$module':
case GLOBAL_MODULE_ID:
apiService.send('moduleEvent', {
fromId: parameters.moduleId,
eventType: name,
});
break;
case '$utils':
case GLOBAL_UTILS_ID:
// handle as window function
if (name in window) {
const method = window[name as keyof Window];

View File

@ -1,4 +1,5 @@
import { parseType } from '@sunmao-ui/core';
import { GLOBAL_UTILS_ID } from '../constants';
// components
/* --- plain --- */
import PlainButton from '../components/plain/Button';
@ -183,7 +184,7 @@ export class Registry {
private mountUtilMethods() {
this.apiService.on('uiMethod', ({ componentId, name, parameters }) => {
if (componentId === '$utils') {
if (componentId === GLOBAL_UTILS_ID) {
const utilMethod = this.utilMethods.get(name);
if (utilMethod) {
utilMethod(parameters);

View File

@ -9,6 +9,7 @@ import {
TSchema,
OptionalModifier,
UnionKind,
AnyKind,
} from '@sinclair/typebox';
export function parseTypeBox(tSchema: TSchema, noOptional = false): Static<typeof tSchema> {
@ -40,6 +41,8 @@ export function parseTypeBox(tSchema: TSchema, noOptional = false): Static<typeo
const subSchema = (tSchema.anyOf || tSchema.oneOf)[0];
return parseTypeBox(subSchema, noOptional);
}
case tSchema.kind === AnyKind:
return undefined;
default:
return {};
}