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

This commit is contained in:
MrWindlike 2022-02-10 18:12:38 +08:00
commit 5642fde8dc
48 changed files with 548 additions and 255 deletions

View File

@ -8,8 +8,8 @@
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"standard"
"standard",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
@ -25,12 +25,15 @@
"linebreak-style": ["error", "unix"],
"no-case-declarations": "off",
"no-use-before-define": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"comma-dangle": "off",
"space-before-function-paren": "off",
"multiline-ternary": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/ban-types": "off",
"react/prop-types": "off",
"react/display-name": "off",
"react/jsx-uses-react": "off",
@ -48,5 +51,13 @@
"react/jsx-wrap-multilines": ["error"],
"react/jsx-tag-spacing": ["error"]
},
"ignorePatterns": ["node_modules", "*.d.ts", "*.js", "package.json", "*.html", "*.spec.ts", "*.spec.tsx"]
"ignorePatterns": [
"node_modules",
"*.d.ts",
"*.js",
"package.json",
"*.html",
"*.spec.ts",
"*.spec.tsx"
]
}

View File

@ -283,6 +283,9 @@ export default implementRuntimeComponent({
border: '1px solid black',
},
exampleSize: [6, 6],
annotations: {
category: 'Layout',
},
},
spec: {
properties: StyleSchema,

View File

@ -32,12 +32,15 @@ export default implementRuntimeComponent({
isLoading: false,
},
exampleSize: [2, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {
click: void 0,
click: undefined,
},
slots: [],
styleSlots: ['content'],

View File

@ -53,6 +53,9 @@ export default implementRuntimeComponent({
size: 'md',
},
exampleSize: [3, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,

View File

@ -28,6 +28,9 @@ export default implementRuntimeComponent({
defaultValue: [],
},
exampleSize: [3, 3],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,

View File

@ -1,8 +1,5 @@
import { useEffect, useState, useRef } from 'react';
import {
implementRuntimeComponent,
DIALOG_CONTAINER_ID,
} from '@sunmao-ui/runtime';
import { implementRuntimeComponent, DIALOG_CONTAINER_ID } from '@sunmao-ui/runtime';
import {
AlertDialog,
AlertDialogBody,
@ -44,6 +41,9 @@ export default implementRuntimeComponent({
disableConfirm: false,
},
exampleSize: [6, 6],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,
@ -52,8 +52,8 @@ export default implementRuntimeComponent({
openDialog: Type.Object({
title: Type.String(),
}),
confirmDialog: void 0,
cancelDialog: void 0,
confirmDialog: undefined,
cancelDialog: undefined,
},
slots: ['content'],
styleSlots: ['content'],
@ -131,9 +131,7 @@ export default implementRuntimeComponent({
{...(containerRef.current ? dialogContentProps : {})}
>
<AlertDialogHeader>{title}</AlertDialogHeader>
<AlertDialogBody>
{slotsElements.content}
</AlertDialogBody>
<AlertDialogBody>{slotsElements.content}</AlertDialogBody>
<AlertDialogFooter>
<Button

View File

@ -13,6 +13,9 @@ export default implementRuntimeComponent({
isResizable: true,
exampleProperties: {},
exampleSize: [4, 1],
annotations: {
category: 'Display',
},
},
spec: {
properties: Type.Object({}),

View File

@ -20,6 +20,9 @@ export default implementRuntimeComponent({
hideSubmit: false,
},
exampleSize: [4, 6],
annotations: {
category: 'Layout',
},
},
spec: {
properties: PropsSchema,
@ -28,7 +31,7 @@ export default implementRuntimeComponent({
disableSubmit: Type.Boolean(),
}),
methods: {
resetForm: void 0,
resetForm: undefined,
},
slots: ['content'],
styleSlots: ['content'],
@ -44,7 +47,7 @@ export default implementRuntimeComponent({
customStyle,
slotsElements,
childrenMap,
component
component,
}) => {
const [invalidArray, setInvalidArray] = useState<boolean[]>([]);
const [isFormInvalid, setIsFormInvalid] = useState<boolean>(false);

View File

@ -40,6 +40,9 @@ export default implementRuntimeComponent({
helperText: '',
},
exampleSize: [4, 2],
annotations: {
category: 'Layout',
},
},
spec: {
properties: PropsSchema,
@ -70,7 +73,10 @@ export default implementRuntimeComponent({
const [inputValue, setInputValue] = useState('');
// don't show Invalid state on component mount
const [hideInvalid, setHideInvalid] = useState(true);
const inputId = useMemo(() => first(childrenMap[component.id]?.content)?.id || '', [component.id, childrenMap]);
const inputId = useMemo(
() => first(childrenMap[component.id]?.content)?.id || '',
[component.id, childrenMap]
);
const [validResult, setValidResult] = useState({
isInvalid: false,
errorMsg: '',

View File

@ -30,6 +30,9 @@ export default implementRuntimeComponent({
exampleSize: [6, 6],
isDraggable: true,
isResizable: true,
annotations: {
category: 'Layout',
},
},
spec: {
properties: PropsSchema,

View File

@ -103,6 +103,9 @@ export default implementRuntimeComponent({
fallbackSrc: 'https://via.placeholder.com/150',
},
exampleSize: [6, 6],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,
@ -151,7 +154,7 @@ export default implementRuntimeComponent({
ignoreFallback={ignoreFallback}
borderRadius={borderRadius}
fallbackSrc={fallbackSrc}
></BaseImage>
/>
);
}
);

View File

@ -71,6 +71,9 @@ export default implementRuntimeComponent({
defaultValue: '',
},
exampleSize: [4, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,
@ -79,7 +82,7 @@ export default implementRuntimeComponent({
setInputValue: Type.Object({
value: Type.String(),
}),
resetInputValue: void 0,
resetInputValue: undefined,
},
slots: [],
styleSlots: ['content'],

View File

@ -27,6 +27,9 @@ export default implementRuntimeComponent({
},
},
exampleSize: [2, 1],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,

View File

@ -25,6 +25,9 @@ export default implementRuntimeComponent({
href: 'https://www.google.com',
},
exampleSize: [2, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,

View File

@ -44,6 +44,9 @@ export default implementRuntimeComponent({
isResizable: true,
exampleProperties,
exampleSize: [6, 6],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,

View File

@ -64,25 +64,29 @@ const exampleProperties = {
};
export default implementRuntimeComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'multiSelect',
displayName: 'MultiSelect',
description: 'chakra-ui MultiSelect',
isResizable: true,
isDraggable: true,
exampleProperties,
exampleSize: [4, 1],
version: 'chakra_ui/v1',
metadata: {
name: 'multiSelect',
displayName: 'MultiSelect',
description: 'chakra-ui MultiSelect',
isResizable: true,
isDraggable: true,
exampleProperties,
exampleSize: [4, 1],
annotations: {
category: 'Input',
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(({
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(
({
options,
placeholder,
defaultValue,
@ -97,12 +101,12 @@ export default implementRuntimeComponent({
const newValue = (defaultValue || []).map(o => o.value);
mergeState({ value: newValue });
}, [defaultValue, mergeState]);
const onChange = (options: Static<typeof OptionsSchema>) => {
const newValue = options.map(o => o.value);
mergeState({ value: newValue });
};
return (
<Box
width="full"
@ -123,4 +127,5 @@ export default implementRuntimeComponent({
/>
</Box>
);
})
}
);

View File

@ -54,6 +54,9 @@ export default implementRuntimeComponent({
defaultValue: 0,
},
exampleSize: [4, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,
@ -62,7 +65,7 @@ export default implementRuntimeComponent({
setInputValue: Type.Object({
value: Type.Number(),
}),
resetInputValue: void 0,
resetInputValue: undefined,
},
slots: [],
styleSlots: ['content'],

View File

@ -30,33 +30,37 @@ const PropsSchema = Type.Object({
});
export default implementRuntimeComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'radio',
displayName: 'Radio',
description: 'chakra-ui radio',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'Radio',
format: 'plain',
},
value: 'Radio 1',
isDisabled: false,
size: 'md',
version: 'chakra_ui/v1',
metadata: {
name: 'radio',
displayName: 'Radio',
description: 'chakra-ui radio',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'Radio',
format: 'plain',
},
exampleSize: [3, 1],
value: 'Radio 1',
isDisabled: false,
size: 'md',
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
exampleSize: [3, 1],
annotations: {
category: 'Input',
},
})(({
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(
({
text,
value,
isDisabled,
@ -74,11 +78,11 @@ export default implementRuntimeComponent({
useEffect(() => {
mergeState({ value: text.raw });
}, [mergeState, text.raw]);
useEffect(() => {
mergeState({ value });
}, [mergeState, value]);
return (
<BaseRadio
height="10"
@ -99,4 +103,5 @@ export default implementRuntimeComponent({
<Text value={text} />
</BaseRadio>
);
})
}
);

View File

@ -26,6 +26,9 @@ export default implementRuntimeComponent({
isNumerical: true,
},
exampleSize: [3, 3],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,

View File

@ -12,6 +12,9 @@ export default implementRuntimeComponent({
isResizable: true,
exampleProperties: {},
exampleSize: [6, 6],
annotations: {
category: 'Advance',
},
},
spec: {
properties: Type.Object({}),

View File

@ -68,6 +68,9 @@ export default implementRuntimeComponent({
isDraggable: true,
exampleProperties,
exampleSize: [4, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,

View File

@ -58,6 +58,9 @@ export default implementRuntimeComponent({
spacing: 10,
},
exampleSize: [6, 6],
annotations: {
category: 'Layout',
},
},
spec: {
properties: PropsSchema,

View File

@ -1,3 +1,3 @@
import { TableImpl } from './Table';
export default TableImpl
export default TableImpl;

View File

@ -49,6 +49,9 @@ export const implementTable = implementRuntimeComponent({
isResizable: true,
exampleProperties,
exampleSize: [8, 6],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,
@ -58,4 +61,4 @@ export const implementTable = implementRuntimeComponent({
styleSlots: [],
events: [],
},
})
});

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { css } from '@emotion/css';
import {
Tabs as BaseTabs,
@ -33,6 +33,9 @@ export default implementRuntimeComponent({
initialSelectedTabIndex: 0,
},
exampleSize: [6, 6],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,
@ -74,7 +77,9 @@ export default implementRuntimeComponent({
</TabList>
<TabPanels>
{tabNames.map((_, idx) => {
const ele = slotsElements.content ? ([] as React.ReactElement[]).concat(slotsElements.content)[idx] : placeholder;
const ele = slotsElements.content
? ([] as React.ReactElement[]).concat(slotsElements.content)[idx]
: placeholder;
return (
<TabPanel
key={idx}

View File

@ -46,6 +46,9 @@ export default implementRuntimeComponent({
text: 'tooltip',
},
exampleSize: [2, 1],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,

View File

@ -10,7 +10,7 @@ const ToastPosition = Type.Union([
Type.Literal('bottom-right'),
Type.Literal('bottom-left'),
]);
export const ToastOpenParamterSchema = Type.Object({
export const ToastOpenParameterSchema = Type.Object({
position: Type.Optional(ToastPosition),
duration: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
title: Type.Optional(Type.String()),
@ -40,7 +40,7 @@ export const ToastCloseParameterSchema = Type.Object({
positions: Type.Optional(Type.Array(ToastPosition)),
});
export type ToastOpenParameter = Static<typeof ToastOpenParamterSchema>;
export type ToastOpenParameter = Static<typeof ToastOpenParameterSchema>;
export type ToastCloseParameter = Static<typeof ToastCloseParameterSchema>;
const pickProperty = <T, U extends Record<string, any>>(
@ -58,14 +58,15 @@ export default function ToastUtilMethodFactory() {
let toast: ReturnType<typeof createStandaloneToast> | undefined;
const toastOpen: UtilMethod = {
name: 'toast.open',
method(parameters: Static<typeof ToastOpenParamterSchema>) {
method(parameters: Static<typeof ToastOpenParameterSchema>) {
if (!toast) {
toast = createStandaloneToast();
}
if (parameters) {
toast(pickProperty(ToastOpenParamterSchema, parameters));
toast(pickProperty(ToastOpenParameterSchema, parameters));
}
},
parameters: ToastOpenParameterSchema,
};
const toastClose: UtilMethod = {
@ -85,6 +86,7 @@ export default function ToastUtilMethodFactory() {
}
}
},
parameters: ToastCloseParameterSchema,
};
return [toastOpen, toastClose];
}

View File

@ -30,6 +30,9 @@ export default implementRuntimeComponent({
exampleSize: [6, 6],
isDraggable: true,
isResizable: true,
annotations: {
category: 'Layout',
},
},
spec: {
properties: PropsSchema,

View File

@ -1,10 +1,7 @@
import { JSONSchema7 } from 'json-schema';
import { parseVersion } from './version';
import { parseVersion, Version } from './version';
import { ComponentMetadata } from './metadata';
import { MethodSchema } from './method';
import { Version } from './version';
// TODO: (type-safe), rename version 2 to normal version
type ComponentSpec<
KMethodName extends string,

View File

@ -1,9 +1,19 @@
export type Metadata = {
export type Metadata<TAnnotations = Record<string, unknown>> = {
name: string;
description?: string;
annotations?: Record<string, any> & TAnnotations;
};
export type ComponentMetadata = Metadata & {
type ComponentCategory =
| (string & {})
| 'Layout'
| 'Input'
| 'Display'
| 'Advance'
| undefined;
export type ComponentMetadata = Metadata<{ category?: ComponentCategory }> & {
// TODO:(yanzhen): move to annotations
isDraggable: boolean;
isResizable: boolean;
displayName: string;

View File

@ -22,7 +22,12 @@ function installTern(cm: CodeMirror.Editor) {
const t = new CodeMirror.TernServer({ defs: [ecma as unknown as Def] });
cm.on('cursorActivity', cm => t.updateArgHints(cm));
cm.on('change', (_instance, change) => {
if (change.text.length === 1 && change.text[0] === '.') {
if (
// change happened
change.text.length + (change.removed?.length || 0) > 0 &&
// not changed by auto-complete
change.origin !== 'complete'
) {
t.complete(cm);
}
});
@ -69,6 +74,9 @@ export const ExpressionEditor: React.FC<{
},
theme: 'ayu-mirage',
viewportMargin: Infinity,
hintOptions: {
completeSingle: false,
},
});
const t = installTern(cm.current);
tServer.current = t.server;
@ -93,5 +101,14 @@ export const ExpressionEditor: React.FC<{
}
}, [defs]);
return <Box css={style} ref={wrapperEl} height="100%" width="100%" />;
return (
<Box
css={style}
ref={wrapperEl}
height="100%"
width="100%"
borderRadius="2"
overflow="hidden"
/>
);
};

View File

@ -6,24 +6,22 @@ import { pickBy } from 'lodash-es';
import ErrorBoundary from '../ErrorBoundary';
const theme = {
scheme: 'monokai',
author: 'wimer hazenberg (http://www.monokai.nl)',
base00: '#272822',
base01: '#383830',
base02: '#49483e',
base03: '#75715e',
base04: '#a59f85',
base05: '#f8f8f2',
base06: '#f5f4f1',
base07: '#f9f8f5',
base08: '#f92672',
base09: '#fd971f',
base0A: '#f4bf75',
base0B: '#a6e22e',
base0C: '#a1efe4',
base0D: '#66d9ef',
base0E: '#ae81ff',
base0F: '#cc6633',
base0A: '#fded02',
base0B: '#01a252',
base0C: '#b5e4f4',
base0D: '#01a0e4',
base0E: '#a16a94',
base0F: '#cdab53',
base00: '#090300',
base01: '#3a3432',
base02: '#4a4543',
base03: '#5c5855',
base04: '#807d7c',
base05: '#a5a2a2',
base06: '#d6d5d4',
base07: '#f7f7f7',
base08: '#db2d20',
base09: '#e8bbd0',
};
const style = css`
@ -31,6 +29,7 @@ const style = css`
padding-left: 0.25em !important;
flex: 1;
margin: 0 !important;
background-color: white !important;
}
`;

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useMemo } from 'react';
import {
Box,
FormControl,
@ -19,6 +19,7 @@ import { KeyValueEditor } from '../../KeyValueEditor';
import { EditorServices } from '../../../types';
import { ComponentModel } from '../../../AppModel/ComponentModel';
import { AppModel } from '../../../AppModel/AppModel';
import { ComponentId } from '../../../AppModel/IAppModel';
type Props = {
eventTypes: readonly string[];
@ -31,7 +32,7 @@ type Props = {
export const EventHandlerForm: React.FC<Props> = observer(props => {
const { handler, eventTypes, onChange, onRemove, hideEventType, services } = props;
const { registry, editorStore } = services;
const { registry, editorStore, appModelManager } = services;
const { utilMethods } = registry;
const { components } = editorStore;
const [methods, setMethods] = useState<string[]>([]);
@ -43,6 +44,41 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
},
});
const hasParams = useMemo(
() => Object.keys(formik.values.method.parameters ?? {}).length,
[formik.values.method.parameters]
);
const params = useMemo(() => {
const params: Record<string, string> = {};
const { values } = formik;
const methodName = values.method.name;
if (values.method.name) {
let parameters = {};
if (handler.componentId === GLOBAL_UTILS_ID) {
const targetMethod = utilMethods.get(methodName);
parameters = targetMethod?.parameters?.properties ?? {};
} else {
const targetComponent = appModelManager.appModel.getComponentById(
handler.componentId as ComponentId
);
const targetMethod = (targetComponent?.methods ?? []).find(
({ name }) => name === formik.values.method.name
);
parameters = targetMethod?.parameters?.properties ?? {};
}
for (const key in parameters) {
params[key] = values.method.parameters?.[key] ?? '';
}
}
return params;
}, [formik.values.method.name]);
const updateMethods = useCallback(
(componentId: string) => {
if (componentId === GLOBAL_UTILS_ID) {
@ -68,6 +104,10 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
formik.setValues(handler);
}, [handler]);
useEffect(() => {
formik.setFieldValue('method.parameters', params);
}, [params]);
useEffect(() => {
if (handler.componentId) {
updateMethods(handler.componentId);
@ -76,6 +116,8 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
const onTargetComponentChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
updateMethods(e.target.value);
formik.handleChange(e);
formik.setFieldValue('method', { name: '', parameters: {} });
};
const typeField = (
@ -102,10 +144,7 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
<Select
name="componentId"
onBlur={() => formik.submitForm()}
onChange={e => {
onTargetComponentChange(e);
formik.handleChange(e);
}}
onChange={onTargetComponentChange}
placeholder="Select Target Component"
value={formik.values.componentId}
>
@ -145,6 +184,7 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
formik.setFieldValue('method.parameters', json);
formik.submitForm();
}}
onlySetValue={true}
/>
</FormControl>
);
@ -195,7 +235,7 @@ export const EventHandlerForm: React.FC<Props> = observer(props => {
{hideEventType ? null : typeField}
{targetField}
{methodField}
{parametersField}
{hasParams ? parametersField : null}
{waitTypeField}
{waitTimeField}
{disabledField}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { FieldProps } from './fields';
import { Switch } from '@chakra-ui/react';
@ -7,6 +7,12 @@ type Props = FieldProps;
const BooleanField: React.FC<Props> = props => {
const { formData, onChange } = props;
useEffect(() => {
if (typeof formData !== 'boolean') {
onChange(false);
}
}, [formData, onChange]);
return (
<Switch isChecked={formData} onChange={evt => onChange(evt.currentTarget.checked)} />
);

View File

@ -18,6 +18,12 @@ const NumberField: React.FC<Props> = props => {
setValue(String(formData));
}, [formData]);
useEffect(() => {
if (typeof formData !== 'number') {
onChange(1);
}
}, [formData, onChange]);
return (
<NumberInput
value={value}

View File

@ -88,10 +88,7 @@ type Props = FieldProps & {
const SchemaField: React.FC<Props> = props => {
const { schema, label, formData, onChange, registry, stateManager } = props;
const [isExpression, setIsExpression] = useState(
// FIXME: regexp copied from FieldModel.ts, is this a stable way to check expression?
() => _isExpression(formData)
);
const [isExpression, setIsExpression] = useState(() => _isExpression(formData));
if (isEmpty(schema)) {
return null;
@ -134,6 +131,7 @@ const SchemaField: React.FC<Props> = props => {
return (
<DefaultTemplate
label={label}
description={schema.description}
displayLabel={displayLabel}
codeMode={codeMode}
isExpression={isExpression}

View File

@ -4,6 +4,26 @@ import { Input, Select } from '@chakra-ui/react';
type Props = FieldProps;
const EnumField: React.FC<FieldProps> = props => {
const { schema, formData, onChange } = props;
const options = (schema.enum || []).map(item => item?.toString() || '');
useEffect(() => {
// reset to valid enum
if (options.length && !options.includes(formData)) {
onChange(options[0]);
}
}, [options, formData, onChange]);
return (
<Select value={formData} onChange={evt => onChange(evt.currentTarget.value)}>
{options.map((value, idx) => {
return <option key={idx}>{value}</option>;
})}
</Select>
);
};
const StringField: React.FC<Props> = props => {
const { schema, formData, onChange } = props;
const [value, setValue] = useState(formData);
@ -14,14 +34,7 @@ const StringField: React.FC<Props> = props => {
// enum
if (Array.isArray(schema.enum)) {
return (
<Select value={formData} onChange={evt => onChange(evt.currentTarget.value)}>
{schema.enum.map((item, idx) => {
const value = item?.toString() || '';
return <option key={idx}>{value}</option>;
})}
</Select>
);
return <EnumField {...props} />;
}
return (

View File

@ -8,6 +8,7 @@ import { HEADER_HEIGHT } from '../constants/layout';
import { genOperation } from '../operations';
import { EditorServices } from '../types';
import { ExplorerMenuTabs } from '../services/enum';
type ComponentEditorState = 'drag' | 'select' | 'hover' | 'idle';
@ -135,6 +136,7 @@ export function useComponentWrapper(services: EditorServices): ComponentWrapperT
setHoverComponentId,
dragOverComponentId,
setDragOverComponentId,
setExplorerMenuTab,
} = editorStore;
const wrapperRef = useRef<HTMLDivElement>(null);
@ -251,6 +253,7 @@ export function useComponentWrapper(services: EditorServices): ComponentWrapperT
e.preventDefault();
setDragOverComponentId('');
setCurrentSlot(undefined);
setExplorerMenuTab(ExplorerMenuTabs.UI_TREE);
const creatingComponent = e.dataTransfer?.getData('component') || '';
eventBus.send(
'operation',

View File

@ -1,101 +1,143 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import {
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
SimpleGrid,
Flex,
Box,
Accordion,
AccordionItem,
AccordionPanel,
AccordionButton,
AccordionIcon,
Input,
Tag,
} from '@chakra-ui/react';
import { encodeDragDataTransfer, DROP_EXAMPLE_SIZE_PREFIX } from '@sunmao-ui/runtime';
import { Registry } from '@sunmao-ui/runtime';
import {
encodeDragDataTransfer,
DROP_EXAMPLE_SIZE_PREFIX,
Registry,
} from '@sunmao-ui/runtime';
import { groupBy, sortBy } from 'lodash-es';
type Props = {
registry: Registry;
};
type Category = {
name: string;
components: ReturnType<Registry['getAllComponents']>;
};
const PRESET_CATEGORY_ORDER = {
Layout: 5,
Input: 4,
Display: 3,
Advance: -Infinity,
};
function getCategoryOrder(name: string): number {
return name in PRESET_CATEGORY_ORDER
? PRESET_CATEGORY_ORDER[name as keyof typeof PRESET_CATEGORY_ORDER]
: 0;
}
function getTagColor(version: string): string {
if (version.startsWith('chakra_ui/')) {
return 'teal';
} else if (version.startsWith('core/v1')) {
return 'yellow';
} else {
return 'blackAlpha';
}
}
export const ComponentList: React.FC<Props> = ({ registry }) => {
const [filterText, setFilterText] = useState('');
const categories = useMemo<Category[]>(() => {
const grouped = groupBy(
registry.getAllComponents().filter(c => {
if (!filterText) {
return true;
}
return new RegExp(filterText, 'i').test(c.metadata.displayName);
}),
c => c.metadata.annotations?.category || 'Advance'
);
return sortBy(
Object.keys(grouped).map(name => ({
name,
components: sortBy(grouped[name], 'metadata.name'),
})),
c => -getCategoryOrder(c.name)
);
}, [filterText, registry]);
return (
<Tabs>
<TabList overflow="auto">
{Array.from(registry.components.keys()).map(version => (
<Tab key={version}>{version}</Tab>
))}
</TabList>
<TabPanels>
{Array.from(registry.components.keys()).map(version => (
<TabPanel key={version} overflow="auto">
<SimpleGrid columns={4} spacing={1}>
{Array.from(registry.components.get(version)!.values()).map(c => {
const onDragStart = (e: any) => {
e.dataTransfer.setData('component', `${c.version}/${c.metadata.name}`);
// pass the exampleSize to gridlayout to render placeholder
e.dataTransfer.setData(
encodeDragDataTransfer(
`${DROP_EXAMPLE_SIZE_PREFIX}${JSON.stringify(
c.metadata.exampleSize
)}`
),
''
);
};
const cEle = (
<Flex
key={c.metadata.name}
flexDirection="column"
align="center"
justify="start"
>
<>
<Input
placeholder="filter the components"
value={filterText}
onChange={evt => setFilterText(evt.currentTarget.value)}
/>
<Accordion allowMultiple defaultIndex={categories.map((_, idx) => idx)}>
{categories.map(category => {
return (
<AccordionItem key={category.name}>
<AccordionButton>
<Box flex="1" textAlign="left">
{category.name}
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
{category.components.map(c => {
const onDragStart = (e: any) => {
e.dataTransfer.setData(
'component',
`${c.version}/${c.metadata.name}`
);
// pass the exampleSize to gridlayout to render placeholder
e.dataTransfer.setData(
encodeDragDataTransfer(
`${DROP_EXAMPLE_SIZE_PREFIX}${JSON.stringify(
c.metadata.exampleSize
)}`
),
''
);
};
const cEle = (
<Flex
key={`${c.version}/${c.metadata.name}`}
className="droppable-element"
cursor="move"
background="gray.100"
width="60px"
height="60px"
width="100%"
borderRadius="md"
align="center"
justify="center"
justify="space-between"
transition="ease 0.2s"
_hover={{
transform: 'scale(1.05)',
background: 'gray.200',
}}
p={2}
mb={1}
draggable
unselectable="on"
onDragStart={onDragStart}
>
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2693"
width="30"
height="30"
>
<path
d="M972.09863 1016.986301H259.506849c-19.638356 0-35.068493-15.430137-35.068493-35.068493V768.70137c-8.416438 5.610959-18.235616 9.819178-28.054794 12.624657h-1.40274c-12.624658 4.208219-26.652055 5.610959-39.276712 5.610959-77.150685 0-138.871233-63.123288-138.871233-138.871233 0-77.150685 63.123288-138.871233 138.871233-138.871232 14.027397 0 28.054795 2.805479 42.082191 7.013698 1.40274 0 2.805479 1.40274 4.20822 1.40274 7.013699 2.805479 14.027397 5.610959 19.638356 8.416438l2.805479 1.40274V269.326027c0-19.638356 15.430137-35.068493 35.068493-35.068493h228.646576l-4.20822-8.416438c-7.013699-11.221918-12.624658-22.443836-15.430137-35.068493v-1.40274c-4.208219-12.624658-5.610959-26.652055-5.610958-39.276712C462.90411 72.942466 526.027397 11.221918 601.775342 11.221918c77.150685 0 138.871233 63.123288 138.871233 138.871233 0 14.027397-2.805479 28.054795-7.013698 42.082191 0 1.40274-1.40274 2.805479-1.40274 4.20822-2.805479 7.013699-5.610959 14.027397-8.416438 19.638356l-7.013699 16.832877h255.29863c19.638356 0 35.068493 15.430137 35.068493 35.068493v314.213698c0 11.221918-5.610959 22.443836-15.430137 29.457535-9.819178 7.013699-22.443836 7.013699-33.665753 2.805479L897.753425 587.747945c-1.40274 0-1.40274-1.40274-2.80548-1.40274-2.805479-1.40274-7.013699-4.208219-11.221918-5.610958h-1.402739c-7.013699-2.805479-14.027397-2.805479-21.041096-2.80548-37.873973 0-68.734247 30.860274-68.734247 68.734247s30.860274 68.734247 68.734247 68.734246c7.013699 0 14.027397-1.40274 19.638356-2.805479 7.013699-1.40274 12.624658-5.610959 18.235616-8.416439 1.40274-1.40274 2.805479-1.40274 4.20822-2.805479l53.304109-25.249315c11.221918-5.610959 23.846575-4.208219 33.665754 1.40274s16.832877 18.235616 16.832876 29.457534V981.917808c0 19.638356-15.430137 35.068493-35.068493 35.068493z m-677.523288-70.136986h642.454795v-182.356164h-1.40274c-11.221918 7.013699-22.443836 12.624658-35.068493 16.832876h-1.40274c-12.624658 4.208219-26.652055 5.610959-39.276712 5.610959-77.150685 0-138.871233-63.123288-138.871233-138.871233 0-77.150685 63.123288-138.871233 138.871233-138.871232 14.027397 0 28.054795 2.805479 42.082192 7.013698 1.40274 0 2.805479 1.40274 4.208219 1.40274 7.013699 2.805479 14.027397 5.610959 19.638356 8.416438l9.819178 4.208219v-224.438356H662.093151c-11.221918 0-22.443836-5.610959-29.457535-15.430137-7.013699-9.819178-7.013699-22.443836-2.805479-33.665753l29.457534-67.331507c0-1.40274 1.40274-1.40274 1.40274-2.805479 1.40274-2.805479 4.208219-7.013699 5.610959-11.221918v-1.40274c2.805479-7.013699 2.805479-14.027397 2.805479-21.041096 0-37.873973-30.860274-68.734247-68.734246-68.734246s-68.734247 30.860274-68.734247 68.734246c0 7.013699 1.40274 14.027397 2.80548 19.638356 1.40274 7.013699 5.610959 12.624658 8.416438 18.235617 1.40274 1.40274 1.40274 2.805479 2.805479 4.208219l28.054795 60.317808c5.610959 11.221918 4.208219 23.846575-1.40274 33.665754s-18.235616 16.832877-29.457534 16.832876H294.575342v274.936987c0 11.221918-5.610959 22.443836-15.430137 29.457534-9.819178 7.013699-22.443836 7.013699-33.665753 2.805479L192.175342 589.150685c-1.40274 0-1.40274-1.40274-2.805479-1.40274-2.805479-1.40274-7.013699-4.208219-11.221918-5.610959h-1.40274c-7.013699-2.805479-14.027397-2.805479-21.041095-2.805479-37.873973 0-68.734247 30.860274-68.734247 68.734246s30.860274 68.734247 68.734247 68.734247c7.013699 0 14.027397-1.40274 19.638356-2.805479 7.013699-1.40274 12.624658-5.610959 18.235616-8.416439 1.40274-1.40274 2.805479-1.40274 4.208219-2.805479l46.290411-22.443836c11.221918-5.610959 23.846575-4.208219 33.665754 1.40274 9.819178 7.013699 16.832877 18.235616 16.832876 29.457534v235.660274z"
p-id="2694"
></path>
</svg>
</Flex>
<Box
p={2}
whiteSpace="pre-wrap"
textAlign="center"
fontSize="x-small"
>
{c.metadata.displayName}
</Box>
</Flex>
);
return cEle;
})}
</SimpleGrid>
</TabPanel>
))}
</TabPanels>
</Tabs>
<Tag colorScheme={getTagColor(c.version)} size="sm">
{c.version}
</Tag>
</Flex>
);
return cEle;
})}
</AccordionPanel>
</AccordionItem>
);
})}
</Accordion>
</>
);
};

View File

@ -53,6 +53,10 @@ export const Editor: React.FC<Props> = observer(
modules,
activeDataSource,
activeDataSourceType,
toolMenuTab,
explorerMenuTab,
setToolMenuTab,
setExplorerMenuTab,
} = editorStore;
const [scale, setScale] = useState(100);
@ -182,7 +186,18 @@ export const Editor: React.FC<Props> = observer(
position="relative"
zIndex="2"
>
<Tabs height="100%" display="flex" flexDirection="column" isLazy>
<Tabs
align="center"
height="100%"
display="flex"
flexDirection="column"
textAlign="left"
isLazy
index={explorerMenuTab}
onChange={activatedTab => {
setExplorerMenuTab(activatedTab);
}}
>
<TabList background="gray.50" overflow="auto" whiteSpace="nowrap">
<Tab>Explorer</Tab>
<Tab>UI Tree</Tab>
@ -233,6 +248,10 @@ export const Editor: React.FC<Props> = observer(
height="100%"
display="flex"
flexDirection="column"
index={toolMenuTab}
onChange={activatedTab => {
setToolMenuTab(activatedTab);
}}
>
<TabList background="gray.50">
<Tab>Inspect</Tab>

View File

@ -9,10 +9,11 @@ type Props = {
value?: Record<string, string>;
isShowHeader?: boolean;
minNum?: number;
onlySetValue?: boolean;
};
export const KeyValueEditor: React.FC<Props> = props => {
const { minNum = 0 } = props;
const { minNum = 0, onlySetValue } = props;
const generateRows = (currentRows: Array<[string, string]> = []) => {
let newRows = toPairs(props.value);
@ -67,6 +68,7 @@ export const KeyValueEditor: React.FC<Props> = props => {
placeholder="key"
onChange={onInputChange}
onBlur={onBlur}
isDisabled={onlySetValue}
/>
<Input
flex={1}
@ -76,14 +78,16 @@ export const KeyValueEditor: React.FC<Props> = props => {
onChange={onInputChange}
onBlur={onBlur}
/>
<IconButton
aria-label="remove row"
icon={<CloseIcon />}
size="xs"
onClick={() => onRemoveRow(i)}
variant="ghost"
isDisabled={minNum >= rows.length}
/>
{onlySetValue ? null : (
<IconButton
aria-label="remove row"
icon={<CloseIcon />}
size="xs"
onClick={() => onRemoveRow(i)}
variant="ghost"
isDisabled={minNum >= rows.length}
/>
)}
</HStack>
);
});
@ -97,9 +101,11 @@ export const KeyValueEditor: React.FC<Props> = props => {
</HStack>
) : null}
{rowItems}
<Button onClick={onAddRow} size="xs" alignSelf="start">
+ Add
</Button>
{onlySetValue ? null : (
<Button onClick={onAddRow} size="xs" alignSelf="start">
+ Add
</Button>
)}
</VStack>
);
};

View File

@ -9,6 +9,7 @@ import { removeModuleId } from '../utils/addModuleId';
import { DataSourceType } from '../components/DataSource';
import { TSchema } from '@sinclair/typebox';
import { genOperation } from '../operations';
import { ExplorerMenuTabs, ToolMenuTabs } from './enum';
type EditingTarget = {
kind: 'app' | 'module';
@ -22,6 +23,8 @@ export class EditorStore {
_selectedComponentId = '';
_hoverComponentId = '';
_dragOverComponentId = '';
explorerMenuTab = ExplorerMenuTabs.EXPLORER;
toolMenuTab = ToolMenuTabs.INSERT;
// current editor editing target(app or module)
currentEditingTarget: EditingTarget = {
kind: 'app',
@ -78,6 +81,12 @@ export class EditorStore {
}
);
reaction(
() => this.selectedComponentId,
() => {
this.setToolMenuTab(ToolMenuTabs.INSPECT);
});
this.updateCurrentEditingTarget('app', this.app.version, this.app.metadata.name);
}
@ -279,4 +288,12 @@ export class EditorStore {
this.setActiveDataSource(component!);
};
setExplorerMenuTab = (val: ExplorerMenuTabs) => {
this.explorerMenuTab = val;
}
setToolMenuTab = (val: ToolMenuTabs) => {
this.toolMenuTab = val;
}
}

View File

@ -0,0 +1,12 @@
enum ExplorerMenuTabs {
EXPLORER = 0,
UI_TREE = 1,
STATE = 2
}
enum ToolMenuTabs {
INSPECT = 0,
INSERT = 1
}
export { ExplorerMenuTabs, ToolMenuTabs };

View File

@ -12,6 +12,9 @@ export default implementRuntimeComponent({
isResizable: false,
exampleProperties: {},
exampleSize: [1, 1],
annotations: {
category: 'Advance',
},
},
spec: {
properties: Type.Object({}),

View File

@ -31,6 +31,9 @@ export default implementRuntimeComponent({
layout: [],
},
exampleSize: [6, 6],
annotations: {
category: 'Layout',
},
},
spec: {
properties: PropsSchema,

View File

@ -3,50 +3,46 @@ import { ModuleSchema } from '../../types';
import { ModuleRenderer } from '../_internal/ModuleRenderer';
export default implementRuntimeComponent({
version: 'core/v1',
metadata: {
name: 'moduleContainer',
displayName: 'ModuleContainer',
description: 'ModuleContainer component',
isDraggable: true,
isResizable: true,
exampleProperties: {
id: 'myModule',
type: 'custom/v1/module',
},
exampleSize: [6, 6],
version: 'core/v1',
metadata: {
name: 'moduleContainer',
displayName: 'ModuleContainer',
description: 'ModuleContainer component',
isDraggable: true,
isResizable: true,
exampleProperties: {
id: 'myModule',
type: 'custom/v1/module',
},
spec: {
properties: ModuleSchema,
state: {},
methods: {},
slots: [],
styleSlots: [],
events: [],
exampleSize: [6, 6],
annotations: {
category: 'Advance',
},
})(({
id,
type,
properties,
handlers,
services,
app
}) => {
if (!type) {
return <span>Please choose a module to render.</span>
}
if (!id) {
return <span>Please set a id for module.</span>
}
return (
<ModuleRenderer
id={id}
type={type}
properties={properties}
handlers={handlers}
services={services}
app={app}
/>
);
})
},
spec: {
properties: ModuleSchema,
state: {},
methods: {},
slots: [],
styleSlots: [],
events: [],
},
})(({ id, type, properties, handlers, services, app }) => {
if (!type) {
return <span>Please choose a module to render.</span>;
}
if (!id) {
return <span>Please set a id for module.</span>;
}
return (
<ModuleRenderer
id={id}
type={type}
properties={properties}
handlers={handlers}
services={services}
app={app}
/>
);
});

View File

@ -25,6 +25,9 @@ export default implementRuntimeComponent({
},
},
exampleSize: [4, 1],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSchema,

View File

@ -28,6 +28,7 @@ import { ApiService } from './apiService';
export type UtilMethod = {
name: string;
method: (parameters?: any) => void;
parameters?: any;
};
export type UtilMethodFactory = () => UtilMethod[];
@ -50,7 +51,7 @@ export class Registry {
components = new Map<string, Map<string, AnyImplementedRuntimeComponent>>();
traits = new Map<string, Map<string, ImplementedRuntimeTrait>>();
modules = new Map<string, Map<string, ImplementedRuntimeModule>>();
utilMethods = new Map<string, UtilMethod['method']>();
utilMethods = new Map<string, UtilMethod>();
private apiService: ApiService;
constructor(apiService: ApiService) {
@ -166,7 +167,7 @@ export class Registry {
if (this.utilMethods.get(m.name)) {
throw new Error(`Already has utilMethod ${m.name} in this registry.`);
}
this.utilMethods.set(m.name, m.method);
this.utilMethods.set(m.name, m);
}
installLib(lib: SunmaoLib) {
@ -185,9 +186,19 @@ export class Registry {
private mountUtilMethods() {
this.apiService.on('uiMethod', ({ componentId, name, parameters }) => {
if (componentId === GLOBAL_UTILS_ID) {
const utilMethod = this.utilMethods.get(name);
const utilMethod = this.utilMethods.get(name)?.method;
if (utilMethod) {
utilMethod(parameters);
const params: Record<string, unknown> = {};
for (const key in parameters) {
const value = parameters[key];
if (value !== undefined && value !== '') {
params[key] = value;
}
}
utilMethod(params);
}
}
});