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

This commit is contained in:
xzdry 2022-06-09 10:39:31 +08:00
commit 08376c41ae
39 changed files with 1037 additions and 216 deletions

View File

@ -0,0 +1,145 @@
{
"modules": [],
"app": {
"version": "example/v1",
"metadata": {
"name": "list_component",
"description": "list component example"
},
"spec": {
"components": [
{
"id": "root",
"type": "chakra_ui/v1/root",
"properties": {},
"traits": [
{
"type": "core/v1/state",
"properties": {
"key": "listTitle",
"initialValue": "Customers List"
}
},
{
"type": "core/v1/arrayState",
"properties": {
"key": "listData",
"initialValue": [
{
"id": 1,
"name": "Tom",
"email": "tom@deck.com"
},
{
"id": 2,
"name": "Jack",
"email": "jack@conversation.com"
},
{
"id": 3,
"name": "Pony",
"email": "pony@response.com"
},
{
"id": 4,
"name": "Peter",
"email": "peter@example.com"
},
{
"id": 5,
"name": "John",
"email": "john@widget.org"
}
]
}
}
]
},
{
"id": "list",
"type": "core/v1/list",
"properties": {
"listData": "{{ root.listData }}"
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "root",
"slot": "root"
}
}
}
]
},
{
"id": "listSlotText",
"type": "core/v1/text",
"properties": {
"value": {
"raw": "{{$slot.$listItem.name}}",
"format": "md"
}
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "list",
"slot": "content"
}
}
}
]
},
{
"id": "listSlotButton",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
"raw": "Click Me",
"format": "md"
}
},
"traits": [
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onClick",
"componentId": "$utils",
"method": {
"name": "chakra_ui/v1/openToast",
"parameters": {
"id": "createSuccessToast",
"title": "Tost",
"description": "Hello, {{$slot.$listItem.name}}!",
"position": "top",
"duration": null,
"isClosable": true
}
},
"wait": {},
"disabled": false
}
]
}
},
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "list",
"slot": "content"
}
}
}
]
}
]
}
}
}

View File

@ -1,133 +1,135 @@
{
"modules": [{
"version": "core/v1",
"kind": "Module",
"parsedVersion": {
"category": "core/v1",
"value": "littleItem"
},
"metadata": {
"name": "littleItem"
},
"spec": {
"properties": {},
"events": ["onEdit"],
"stateMap": {
"value": "{{ $moduleId }}__input.value"
}
},
"impl": [
{
"id": "{{ $moduleId }}__hstack",
"type": "chakra_ui/v1/hstack",
"modules": [
{
"version": "custom/v1",
"kind": "Module",
"parsedVersion": {
"category": "custom/v1",
"value": "listItem"
},
"metadata": {
"name": "listItem"
},
"spec": {
"properties": {},
"traits": []
"events": ["onEdit"],
"stateMap": {
"value": "{{ $moduleId }}__input.value"
}
},
{
"id": "{{ $moduleId }}__text",
"type": "core/v1/text",
"properties": {
"value": {
"raw": "**{{value}}**",
"format": "md"
}
"impl": [
{
"id": "{{ $moduleId }}__hstack",
"type": "chakra_ui/v1/hstack",
"properties": {},
"traits": []
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
]
},
{
"id": "{{ $moduleId }}__inputValueText",
"type": "core/v1/text",
"properties": {
"value": {
"raw": "**{{ {{ $moduleId }}__input.value }}**",
"format": "md"
}
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
]
},
{
"id": "{{ $moduleId }}__input",
"type": "chakra_ui/v1/input",
"properties": {},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
]
},
{
"id": "{{ $moduleId }}__button",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
"raw": "click{{value}}",
"format": "md"
}
},
"traits": [
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onClick",
"componentId": "$module",
"method": {
"name": "onEdit",
"parameters": {
"moduleId": "{{$moduleId}}"
}
},
"wait": {},
"disabled": false
}
]
{
"id": "{{ $moduleId }}__text",
"type": "core/v1/text",
"properties": {
"value": {
"raw": "**{{value}}**",
"format": "md"
}
},
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
}
]
}
]
}],
]
},
{
"id": "{{ $moduleId }}__inputValueText",
"type": "core/v1/text",
"properties": {
"value": {
"raw": "**{{ {{ $moduleId }}__input.value }}**",
"format": "md"
}
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
]
},
{
"id": "{{ $moduleId }}__input",
"type": "chakra_ui/v1/input",
"properties": {},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
]
},
{
"id": "{{ $moduleId }}__button",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
"raw": "click{{value}}",
"format": "md"
}
},
"traits": [
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onClick",
"componentId": "$module",
"method": {
"name": "onEdit",
"parameters": {
"moduleId": "{{$moduleId}}"
}
},
"wait": {},
"disabled": false
}
]
}
},
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
}
]
}
]
}
],
"app": {
"version": "example/v1",
"version": "sunmao/v1",
"kind": "Application",
"metadata": {
"name": "list_component",
"description": "list component example"
"name": "some App"
},
"spec": {
"components": [
@ -140,7 +142,7 @@
"type": "core/v1/state",
"properties": {
"key": "listTitle",
"initialValue": "客户列表"
"initialValue": "Customers List"
}
},
{
@ -150,28 +152,28 @@
"initialValue": [
{
"id": 1,
"name": "马云",
"email": "jack.ma@deck.com"
"name": "Tom",
"email": "tom@deck.com"
},
{
"id": 2,
"name": "马化腾",
"email": "pony.ma@conversation.com"
"name": "Jack",
"email": "jack@conversation.com"
},
{
"id": 3,
"name": "李彦宏",
"email": "robin.li@response.com"
"name": "Pony",
"email": "pony@response.com"
},
{
"id": 4,
"name": "张一鸣",
"email": "yiming.zhang@example.com"
"name": "Peter",
"email": "peter@example.com"
},
{
"id": 5,
"name": "王兴",
"email": "xing.wang@widget.org"
"name": "John",
"email": "john@widget.org"
}
]
}
@ -180,12 +182,12 @@
},
{
"id": "list",
"type": "chakra_ui/v1/list",
"type": "core/v1/list",
"properties": {
"listData": "{{ root.listData }}",
"template": {
"id": "littleItem{{$listItem.id}}",
"type": "core/v1/littleItem",
"id": "listItem{{$listItem.id}}",
"type": "custom/v1/listItem",
"properties": {
"value": "{{$listItem.name}}"
},
@ -214,6 +216,47 @@
}
}
]
},
{
"id": "moduleContainer2",
"type": "core/v1/moduleContainer",
"properties": {
"id": "listItem{{$slot.$listItem.id}}",
"type": "custom/v1/listItem",
"properties": {
"value": "{{$slot.$listItem.name}}"
},
"handlers": [
{
"type": "onEdit",
"componentId": "$utils",
"method": {
"name": "chakra_ui/v1/openToast",
"parameters": {
"position": "top",
"duration": 1000,
"title": "Hello,{{$slot.$listItem.name}}!",
"description": "",
"isClosable": false,
"variant": "subtle",
"status": "info",
"id": ""
}
}
}
]
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "list",
"slot": "content"
}
}
}
]
}
]
}

View File

@ -34,6 +34,7 @@
"@sunmao-ui/core": "^0.6.0",
"@sunmao-ui/editor-sdk": "^0.2.0",
"@sunmao-ui/runtime": "^0.6.0",
"@sunmao-ui/shared": "^0.1.0",
"eslint-plugin-react-hooks": "^4.3.0",
"lodash-es": "^4.17.21",
"react": "^17.0.2",

View File

@ -4,7 +4,7 @@ import { css, cx } from '@emotion/css';
import { Type, Static } from '@sinclair/typebox';
import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { PaginationPropsSpec as BasePaginationPropsSpec } from '../generated/types/Pagination';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const PaginationPropsSpec = Type.Object(BasePaginationPropsSpec);
const PaginationStateSpec = Type.Object({

View File

@ -5,7 +5,7 @@ import { Type, Static } from '@sinclair/typebox';
import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { RadioPropsSpec as BaseRadioPropsSpec } from '../generated/types/Radio';
import { useEffect } from 'react';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const RadioPropsSpec = Type.Object({
...BaseRadioPropsSpec,

View File

@ -6,7 +6,7 @@ import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { SelectPropsSpec as BaseSelectPropsSpec } from '../generated/types/Select';
import { useEffect, useRef } from 'react';
import { SelectHandle } from '@arco-design/web-react/es/Select/interface';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const SelectPropsSpec = Type.Object({
...BaseSelectPropsSpec,

View File

@ -4,7 +4,7 @@ import { css } from '@emotion/css';
import { Type, Static } from '@sinclair/typebox';
import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { SwitchPropsSpec as BaseSwitchPropsSpec } from '../generated/types/Switch';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const SwitchPropsSpec = Type.Object({
...BaseSwitchPropsSpec,

View File

@ -1,4 +1,11 @@
/* eslint-disable @typescript-eslint/ban-types */
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { css } from '@emotion/css';
import { sortBy } from 'lodash-es';
import { ResizeCallbackData } from 'react-resizable';
import { TableInstance } from '@arco-design/web-react/es/Table/table';
import { ColumnProps } from '@arco-design/web-react/es/Table';
import { RefInputType } from '@arco-design/web-react/es/Input/interface';
import {
Button,
Link,
@ -11,19 +18,14 @@ import {
LIST_ITEM_INDEX_EXP,
ModuleRenderer,
implementRuntimeComponent,
ImplWrapper,
} from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
import { sortBy } from 'lodash-es';
import { Type, Static } from '@sinclair/typebox';
import { ResizableTitle } from './ResizableTitle';
import { FALLBACK_METADATA, getComponentProps } from '../../sunmao-helper';
import { TablePropsSpec, ColumnSpec } from '../../generated/types/Table';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { TableInstance } from '@arco-design/web-react/es/Table/table';
import { ColumnProps } from '@arco-design/web-react/es/Table';
import { useStateValue } from 'src/hooks/useStateValue';
import { ResizableTitle } from './ResizableTitle';
import { RefInputType } from '@arco-design/web-react/es/Input/interface';
import { ResizeCallbackData } from 'react-resizable';
import { useStateValue } from '../../hooks/useStateValue';
const TableStateSpec = Type.Object({
clickedRow: Type.Optional(Type.Any()),
@ -80,6 +82,7 @@ export const exampleProperties: Static<typeof TablePropsSpec> = {
type: 'text',
filter: true,
displayValue: '',
componentSlotIndex: 0,
},
{
title: 'Salary',
@ -89,6 +92,7 @@ export const exampleProperties: Static<typeof TablePropsSpec> = {
filter: false,
type: 'text',
displayValue: '',
componentSlotIndex: 0,
},
{
title: 'Link',
@ -97,19 +101,7 @@ export const exampleProperties: Static<typeof TablePropsSpec> = {
filter: true,
sortDirections: ['ascend', 'descend'],
displayValue: '',
},
{
title: 'CustomComponent',
dataIndex: 'customComponent',
type: 'module',
filter: false,
module: {
id: 'clistItemName-{{$listItem.id}}',
handlers: [],
properties: [],
type: 'core/v1/text',
},
displayValue: '',
componentSlotIndex: 0,
},
],
data: Array(13)
@ -156,7 +148,14 @@ export const Table = implementRuntimeComponent({
properties: TablePropsSpec,
state: TableStateSpec,
methods: {},
slots: {},
slots: {
content: {
slotProps: Type.Object({
[LIST_ITEM_EXP]: Type.Any(),
[LIST_ITEM_INDEX_EXP]: Type.Number(),
}),
},
},
styleSlots: ['content'],
events: ['onRowClick', 'onSearch', 'onPageChange', 'onFilter', 'onSort', 'onChange'],
},
@ -355,6 +354,46 @@ export const Table = implementRuntimeComponent({
/>
);
break;
case 'component':
const childrenSchema = app.spec.components.filter(c => {
return c.traits.find(
t =>
t.type === 'core/v1/slot' &&
(t.properties.container as any).id === component.id
);
});
const childSchema = childrenSchema[evaledColumn.componentSlotIndex || 0];
if (!childSchema) {
return (
<div>
Cannot find child with index {column.componentSlotIndex} in slot.
</div>
);
}
const _childrenSchema = {
...childSchema,
id: `${component.id}_${childSchema.id}_${index}`,
};
colItem = (
<ImplWrapper
key={_childrenSchema.id}
component={_childrenSchema}
app={app}
services={services}
childrenMap={{}}
isInModule
evalListItem
slotProps={{
[LIST_ITEM_EXP]: record,
[LIST_ITEM_INDEX_EXP]: index,
}}
/>
);
break;
default:
const text = evaledColumn.displayValue || value;
colItem = <span title={column.ellipsis ? text : ''}>{text}</span>;
@ -365,7 +404,15 @@ export const Table = implementRuntimeComponent({
return newColumn;
})
);
}, [cProps.columns]);
}, [
app,
cProps.columns,
callbackMap,
component.id,
component.properties.columns,
services,
useDefaultFilter,
]);
const handleChange = (
pagination: PaginationProps,

View File

@ -5,7 +5,7 @@ import { Type, Static } from '@sinclair/typebox';
import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { TabsPropsSpec as BaseTabsPropsSpec } from '../generated/types/Tabs';
import { useEffect, useRef } from 'react';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const TabsPropsSpec = Type.Object(BaseTabsPropsSpec);
const TabsStateSpec = Type.Object({

View File

@ -6,7 +6,7 @@ import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { TextAreaPropsSpec as BaseTextAreaPropsSpec } from '../generated/types/TextArea';
import { useEffect, useRef } from 'react';
import { RefInputType } from '@arco-design/web-react/es/Input/interface';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const TextAreaPropsSpec = Type.Object({
...BaseTextAreaPropsSpec,

View File

@ -6,7 +6,7 @@ import { FALLBACK_METADATA, getComponentProps } from '../sunmao-helper';
import { TreeSelectPropsSpec as BaseTreeSelectPropsSpec } from '../generated/types/TreeSelect';
import { useEffect, useRef } from 'react';
import { RefTreeSelectType } from '@arco-design/web-react/es/TreeSelect';
import { useStateValue } from 'src/hooks/useStateValue';
import { useStateValue } from '../hooks/useStateValue';
const TreeSelectPropsSpec = Type.Object(BaseTreeSelectPropsSpec);
const TreeSelectStateSpec = Type.Object({

View File

@ -0,0 +1,209 @@
import { Application } from '@sunmao-ui/core';
export const customComponent: Application = {
version: 'sunmao/v1',
kind: 'Application',
metadata: {
name: 'some App',
},
spec: {
components: [
{
id: 'table',
type: 'arco/v1/table',
properties: {
columns: [
{
title: 'Name',
dataIndex: 'name',
sorter: true,
sortDirections: ['ascend', 'descend'],
type: 'text',
filter: true,
displayValue: '',
componentSlotIndex: 0,
},
{
title: 'Salary',
dataIndex: 'salary',
sorter: true,
sortDirections: ['ascend', 'descend'],
filter: false,
type: 'text',
displayValue: '',
componentSlotIndex: 0,
},
{
title: 'Link',
dataIndex: 'link',
type: 'link',
filter: true,
sortDirections: ['ascend', 'descend'],
displayValue: '',
componentSlotIndex: 0,
},
{
title: 'Hello',
type: 'component',
componentSlotIndex: 0,
dataIndex: '',
displayValue: '',
filter: false,
},
],
data: [
{
key: 'key 0',
name: 'Naomi Cook0',
link: 'link-A',
salary: 272,
},
{
key: 'key 1',
name: 'Kevin Sandra1',
link: 'link-B',
salary: 911,
},
{
key: 'key 2',
name: 'Kevin Sandra2',
link: 'link-B',
salary: 527,
},
{
key: 'key 3',
name: 'Kevin Sandra3',
link: 'link-B',
salary: 906,
},
{
key: 'key 4',
name: 'Naomi Cook4',
link: 'link-A',
salary: 261,
},
{
key: 'key 5',
name: 'Naomi Cook5',
link: 'link-A',
salary: 134,
},
{
key: 'key 6',
name: 'Kevin Sandra6',
link: 'link-A',
salary: 877,
},
{
key: 'key 7',
name: 'Kevin Sandra7',
link: 'link-A',
salary: 287,
},
{
key: 'key 8',
name: 'Naomi Cook8',
link: 'link-B',
salary: 319,
},
{
key: 'key 9',
name: 'Kevin Sandra9',
link: 'link-B',
salary: 105,
},
{
key: 'key 10',
name: 'Naomi Cook10',
link: 'link-B',
salary: 468,
},
{
key: 'key 11',
name: 'Naomi Cook11',
link: 'link-A',
salary: 53,
},
{
key: 'key 12',
name: 'Naomi Cook12',
link: 'link-A',
salary: 195,
},
],
checkCrossPage: true,
pagination: {
enablePagination: true,
pageSize: 6,
defaultCurrent: 1,
updateWhenDefaultPageChanges: false,
useCustomPagination: false,
},
rowClick: false,
tableLayoutFixed: false,
borderCell: false,
stripe: false,
size: 'default',
useDefaultFilter: true,
useDefaultSort: true,
pagePosition: 'bottomCenter',
rowSelectionType: 'single',
border: true,
loading: false,
},
traits: [],
},
{
id: 'button4',
type: 'arco/v1/button',
properties: {
type: 'primary',
status: 'default',
long: false,
size: 'small',
disabled: false,
loading: false,
shape: 'round',
text: 'Click',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'table',
slot: 'content',
},
},
},
{
type: 'core/v1/event',
properties: {
handlers: [
{
type: 'onClick',
componentId: '$utils',
method: {
name: 'arco/v1/message',
parameters: {
type: 'info',
content: 'Hello{{$slot.$listItem.name}}',
position: 'top',
closable: false,
duration: 1000,
},
},
disabled: false,
wait: {
type: 'delay',
time: 0,
},
},
],
},
},
],
},
],
},
};

View File

@ -4,6 +4,7 @@ import { basicUsage } from './basicUsage';
import { selection } from './selection';
import { attributes } from './attributes';
import { sortAndFilter } from './sortAndFilter';
import { customComponent } from './customComponent';
const { Title, Text, Paragraph } = Typography;
@ -24,6 +25,12 @@ export const TableDemoPage: React.FC = () => {
<Paragraph>You can easily open or close the properties of the table</Paragraph>
<DemoWrapper application={attributes} />
<Divider />
<Title heading={3}>Custom Column Component</Title>
<Paragraph>
You can use any Sunmao component as column element instead of plain text.
</Paragraph>
<DemoWrapper application={customComponent} />
<Divider />
<Title heading={3}>Sort and filter</Title>
<Paragraph>
Configure the <Text code>sortable</Text> or <Text code>filterable</Text> of{' '}

View File

@ -1,6 +1,6 @@
import { Type } from '@sinclair/typebox';
import { StringUnion } from '../../sunmao-helper';
import { EventHandlerSpec } from '@sunmao-ui/runtime';
import { EventHandlerSpec } from '@sunmao-ui/shared';
import { Category } from '../../constants/category';
const PaginationSpec = Type.Object(
@ -182,12 +182,22 @@ export const ColumnSpec = Type.Object({
link: Type.String(),
button: Type.String(),
module: Type.String(),
component: Type.String(),
}),
{
title: 'Type',
category: Category.Basic,
}
),
componentSlotIndex: Type.Number({
title: 'Component Slot Index',
conditions: [
{
key: 'type',
value: 'component',
},
],
}),
dataIndex: Type.String({
title: 'Key',
category: Category.Basic,
@ -198,6 +208,20 @@ export const ColumnSpec = Type.Object({
title: 'Display Value',
category: Category.Basic,
description: 'The text you want to display instead of raw text',
conditions: [
{
or: [
{
key: 'type',
value: 'link',
},
{
key: 'type',
value: 'text',
},
],
},
],
}),
width: Type.Optional(
Type.Number({

View File

@ -36,6 +36,7 @@
"@sunmao-ui/core": "^0.6.0",
"@sunmao-ui/editor-sdk": "^0.2.0",
"@sunmao-ui/runtime": "^0.6.0",
"@sunmao-ui/shared": "^0.1.0",
"chakra-react-select": "^1.3.2",
"framer-motion": "^4",
"lodash-es": "^4.17.21",

View File

@ -4,9 +4,9 @@ import {
implementRuntimeComponent,
LIST_ITEM_EXP,
LIST_ITEM_INDEX_EXP,
ModuleSpec,
ModuleRenderer,
} from '@sunmao-ui/runtime';
import { ModuleRenderSpec } from '@sunmao-ui/shared';
import { css } from '@emotion/css';
import { BASIC } from './constants/category';
import { CORE_VERSION, CoreWidgetName } from '@sunmao-ui/editor-sdk';
@ -17,7 +17,7 @@ const PropsSpec = Type.Object({
category: BASIC,
widget: `${CORE_VERSION}/${CoreWidgetName.Expression}`,
}),
template: ModuleSpec,
template: ModuleRenderSpec,
});
const exampleProperties = {

View File

@ -190,10 +190,17 @@ export const TableImpl = implementTable(
{columns.map((column, j) => (
<TableTd
index={i}
component={component}
key={column.key}
item={item}
onClickItem={() => selectItem(item)}
rawColumn={(component.properties.columns as Static<typeof ColumnsPropertySpec>)[j]}
rawColumn={
(
component.properties.columns as Static<
typeof ColumnsPropertySpec
>
)[j]
}
column={column}
services={services}
app={app}

View File

@ -1,4 +1,5 @@
import { RuntimeApplication } from '@sunmao-ui/core';
import React from 'react';
import { RuntimeApplication, RuntimeComponentSchema } from '@sunmao-ui/core';
import { Static } from '@sinclair/typebox';
import { ColumnSpec, ColumnsPropertySpec } from './TableTypes';
import { Button, Link, Td, Text } from '@chakra-ui/react';
@ -8,6 +9,7 @@ import {
ModuleRenderer,
UIServices,
ExpressionError,
ImplWrapper,
} from '@sunmao-ui/runtime';
export const TableTd: React.FC<{
@ -17,9 +19,10 @@ export const TableTd: React.FC<{
rawColumn: Static<typeof ColumnsPropertySpec>[0];
onClickItem: () => void;
services: UIServices;
app?: RuntimeApplication;
component: RuntimeComponentSchema;
app: RuntimeApplication;
}> = props => {
const { item, index, column, rawColumn, onClickItem, services, app } = props;
const { item, index, component, column, rawColumn, onClickItem, services, app } = props;
const evalOptions = {
evalListItem: true,
scopeObject: {
@ -70,10 +73,6 @@ export const TableTd: React.FC<{
content = <Button onClick={onClick}>{buttonConfig.text}</Button>;
break;
case 'module':
const evalScope = {
[LIST_ITEM_EXP]: item,
[LIST_ITEM_INDEX_EXP]: index,
};
content = (
<ModuleRenderer
id={column.module.id}
@ -81,11 +80,51 @@ export const TableTd: React.FC<{
properties={column.module.properties}
handlers={column.module.handlers}
services={services}
evalScope={evalScope}
evalScope={{
[LIST_ITEM_EXP]: item,
[LIST_ITEM_INDEX_EXP]: index,
}}
app={app}
/>
);
break;
case 'component':
const childrenSchema = app.spec.components.filter(c => {
return c.traits.find(
t =>
t.type === 'core/v1/slot' &&
(t.properties.container as any).id === component.id
);
});
const childSchema = childrenSchema[column.componentSlotIndex];
if (!childSchema) {
return (
<div>Cannot find child with index {column.componentSlotIndex} in slot.</div>
);
}
const _childrenSchema = {
...childSchema,
id: `${component.id}_${childSchema.id}_${index}`,
};
content = (
<ImplWrapper
key={_childrenSchema.id}
component={_childrenSchema}
app={app}
services={services}
childrenMap={{}}
isInModule
evalListItem
slotProps={{
[LIST_ITEM_EXP]: item,
[LIST_ITEM_INDEX_EXP]: index,
}}
/>
);
break;
}
return (

View File

@ -1,5 +1,5 @@
import { Type } from '@sinclair/typebox';
import { ModuleSpec, EventCallBackHandlerSpec } from '@sunmao-ui/runtime';
import { ModuleRenderSpec, EventCallBackHandlerSpec } from '@sunmao-ui/shared';
import { BASIC, APPEARANCE, BEHAVIOR } from '../constants/category';
export const MajorKeyPropertySpec = Type.String({
@ -30,6 +30,7 @@ export const TableSizePropertySpec = Type.KeyOf(
export const TdTypeSpec = Type.KeyOf(
Type.Object({
text: Type.String(),
component: Type.String(),
image: Type.String(),
link: Type.String(),
button: Type.String(),
@ -52,6 +53,15 @@ export const ColumnSpec = Type.Object(
title: 'Display value',
}),
type: TdTypeSpec,
componentSlotIndex: Type.Number({
title: 'Component Slot Index',
conditions: [
{
key: 'type',
value: 'component',
},
],
}),
buttonConfig: Type.Object(
{
text: Type.String({
@ -71,7 +81,7 @@ export const ColumnSpec = Type.Object(
],
}
),
module: { ...ModuleSpec, conditions: [{ key: 'type', value: 'module' }] },
module: { ...ModuleRenderSpec, conditions: [{ key: 'type', value: 'module' }] },
},
{
title: 'Column',

View File

@ -1,5 +1,9 @@
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent } from '@sunmao-ui/runtime';
import {
implementRuntimeComponent,
LIST_ITEM_EXP,
LIST_ITEM_INDEX_EXP,
} from '@sunmao-ui/runtime';
import {
ColumnsPropertySpec,
DataPropertySpec,
@ -62,7 +66,14 @@ export const implementTable = implementRuntimeComponent({
properties: PropsSpec,
state: TableStateSpec,
methods: {},
slots: {},
slots: {
content: {
slotProps: Type.Object({
[LIST_ITEM_EXP]: Type.Any(),
[LIST_ITEM_INDEX_EXP]: Type.Number(),
}),
},
},
styleSlots: [],
events: [],
},

View File

@ -13,7 +13,7 @@ import {
import { Static } from '@sinclair/typebox';
import { JSONSchema7 } from 'json-schema';
import { CloseIcon, ArrowUpIcon, ArrowDownIcon } from '@chakra-ui/icons';
import { EventHandlerSpec, EventCallBackHandlerSpec } from '@sunmao-ui/runtime';
import { EventHandlerSpec, EventCallBackHandlerSpec } from '@sunmao-ui/shared';
import { ComponentSchema } from '@sunmao-ui/core';
import { formWrapperCSS } from '../style';
import { EditorServices } from '../../../types';

View File

@ -4,8 +4,7 @@ import { Button, VStack } from '@chakra-ui/react';
import { Static } from '@sinclair/typebox';
import produce from 'immer';
import { ComponentSchema } from '@sunmao-ui/core';
import { EventHandlerSpec } from '@sunmao-ui/runtime';
import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
import { CORE_VERSION, CoreTraitName, EventHandlerSpec } from '@sunmao-ui/shared';
import { genOperation } from '../../../operations';
import { EditorServices } from '../../../types';
import { EventHandlerForm } from './EventHandlerForm';

View File

@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
import { EditorServices } from '../../types';
import { observer } from 'mobx-react-lite';
import { Box } from '@chakra-ui/react';
@ -66,6 +66,26 @@ export const EditorMaskWrapper: React.FC<Props> = observer(props => {
);
};
const onClickIframe = useCallback(() => {
if (document.activeElement?.tagName === 'IFRAME') {
setSelectedComponentId(document.activeElement?.getAttribute('title') || '');
setTimeout(() => {
window.focus();
});
}
}, [setSelectedComponentId]);
// can't capture the iframe click event
// use window's blur event to detect whether clicking the iframes
useEffect(() => {
window.focus();
window.addEventListener('blur', onClickIframe);
return () => {
window.removeEventListener('blur', onClickIframe);
};
}, [onClickIframe]);
const mousePositionWithOffset: [number, number] = [
mousePosition[0] + scrollOffset[0],
mousePosition[1] + scrollOffset[1],

View File

@ -4,10 +4,10 @@ import {
TraitValidateContext,
ValidateErrorResult,
} from '../interfaces';
import { EventHandlerSpec, GLOBAL_UTIL_METHOD_ID } from '@sunmao-ui/runtime';
import { GLOBAL_UTIL_METHOD_ID } from '@sunmao-ui/runtime';
import { isExpression } from '../utils';
import { ComponentId, EventName } from '../../AppModel/IAppModel';
import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
import { CORE_VERSION, CoreTraitName, EventHandlerSpec } from '@sunmao-ui/shared';
class EventHandlerValidatorRule implements TraitValidatorRule {
kind: 'trait' = 'trait';

View File

@ -29,8 +29,11 @@
const rootEl = document.querySelector('#root');
const render = example => {
ReactDOM.unmountComponentAtNode(rootEl);
const { App, registry } = initSunmaoUI({libs: [sunmaoChakraUILib]});
const { App, registry } = initSunmaoUI({ libs: [sunmaoChakraUILib] });
const { app, modules = [] } = example.value;
console.log('example', example);
console.log('modules', modules);
window.registry = registry;
modules.forEach(m => {
registry.registerModule(m);
});

View File

@ -8,16 +8,23 @@ import { getSlotElements } from './hooks/useSlotChildren';
import { useGlobalHandlerMap } from './hooks/useGlobalHandlerMap';
import { useEleRef } from './hooks/useEleMap';
import { useGridLayout } from './hooks/useGridLayout';
import { initStateAndMethod } from '../../../utils/initStateAndMethod';
export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
function ImplWrapperMain(props, ref) {
const { component: c, children } = props;
const { component: c, children, slotProps, evalListItem } = props;
const { registry, stateManager } = props.services;
const Impl = registry.getComponent(c.parsedType.version, c.parsedType.name).impl;
useGlobalHandlerMap(props);
// This code is to init dynamic generated components
// because they have not be initialized before
if (!stateManager.store[c.id]) {
initStateAndMethod(registry, stateManager, [c]);
}
const { eleRef, onRef } = useEleRef(props);
const { mergeState, subscribeMethods, executeTrait } = useRuntimeFunctions(props);
@ -28,7 +35,8 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
executeTrait(
t,
stateManager.deepEval(t.properties, {
scopeObject: { $slot: props.slotProps },
evalListItem,
scopeObject: { $slot: slotProps },
fallbackWhenError: () => undefined,
})
)
@ -59,7 +67,8 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
});
},
{
scopeObject: { $slot: props.slotProps },
evalListItem,
scopeObject: { $slot: slotProps },
fallbackWhenError: () => undefined,
}
);
@ -70,7 +79,7 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
// because mergeState will be called during the first render of component, and state will change
setTraitResults(c.traits.map((trait, i) => executeTrait(trait, properties[i])));
return () => stops.forEach(s => s());
}, [c.id, c.traits, executeTrait, stateManager, props.slotProps]);
}, [c.id, c.traits, executeTrait, stateManager, slotProps, evalListItem]);
// reduce traitResults
const propsFromTraits: TraitResult<string, string>['props'] = useMemo(() => {
@ -97,7 +106,8 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
return merge(
stateManager.deepEval(c.properties, {
fallbackWhenError: () => undefined,
scopeObject: { $slot: props.slotProps },
evalListItem,
scopeObject: { $slot: slotProps },
}),
propsFromTraits
);
@ -109,13 +119,17 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
({ result: newResult }: any) => {
setEvaledComponentProperties({ ...newResult });
},
{ fallbackWhenError: () => undefined, scopeObject: { $slot: props.slotProps } }
{
evalListItem,
fallbackWhenError: () => undefined,
scopeObject: { $slot: slotProps },
}
);
// must keep this line, reason is the same as above
setEvaledComponentProperties({ ...result });
return stop;
}, [c.properties, stateManager, props.slotProps]);
}, [c.properties, stateManager, slotProps]);
useEffect(() => {
const clearFunctions = propsFromTraits?.componentDidMount?.map(e => e());

View File

@ -5,7 +5,7 @@ import { merge } from 'lodash';
import { HandlerMap } from '../../../../services/handler';
export function useRuntimeFunctions(props: ImplWrapperProps) {
const { component: c, services } = props;
const { component: c, services, slotProps, evalListItem } = props;
const { stateManager, registry, globalHandlerMap } = services;
const mergeState = useCallback(
@ -35,9 +35,11 @@ export function useRuntimeFunctions(props: ImplWrapperProps) {
mergeState,
subscribeMethods,
services,
slotProps,
evalListItem,
});
},
[c.id, mergeState, registry, services, subscribeMethods]
[c.id, evalListItem, mergeState, registry, services, slotProps, subscribeMethods]
);
return {
mergeState,

View File

@ -11,15 +11,15 @@ import {
import { ImplWrapper } from './ImplWrapper';
import { watch } from '../../utils/watchReactivity';
import { ImplementedRuntimeModule, UIServices } from '../../types';
import { EventHandlerSpec, ModuleSpec } from '@sunmao-ui/shared';
import { EventHandlerSpec, ModuleRenderSpec } from '@sunmao-ui/shared';
import { resolveChildrenMap } from '../../utils/resolveChildrenMap';
import { initStateAndMethod } from '../../utils/initStateAndMethod';
import { ExpressionError } from '../../services/StateManager';
type Props = Static<typeof ModuleSpec> & {
type Props = Static<typeof ModuleRenderSpec> & {
evalScope?: Record<string, any>;
services: UIServices;
app?: RuntimeApplication;
app: RuntimeApplication;
};
export const ModuleRenderer = React.forwardRef<HTMLDivElement, Props>((props, ref) => {

View File

@ -0,0 +1,118 @@
import { implementRuntimeComponent } from '../../utils/buildKit';
import { Type } from '@sinclair/typebox';
import { CORE_VERSION, CoreComponentName, StringUnion } from '@sunmao-ui/shared';
import { css } from '@emotion/css';
import { useEffect } from 'react';
export default implementRuntimeComponent({
version: CORE_VERSION,
metadata: {
name: CoreComponentName.Iframe,
displayName: 'Iframe',
description: '',
isDraggable: false,
isResizable: false,
exampleProperties: {
src: 'https://www.openstreetmap.org/export/embed.html?bbox=-0.004017949104309083%2C51.47612752641776%2C0.00030577182769775396%2C51.478569861898606&layer=mapnik',
referrerpolicy: 'unset',
sandbox: 'unset',
fetchpriority: 'auto',
},
exampleSize: [1, 1],
annotations: {
category: 'Advance',
},
},
spec: {
properties: Type.Object({
src: Type.String({ title: 'Src' }),
referrerpolicy: StringUnion(
[
'unset',
'no-referrer',
'no-referrer-when-downgrade',
'origin',
'origin-when-cross-origin',
'same-origin',
'strict-origin',
'strict-origin-when-cross-origin',
'unsafe-url',
],
{
title: 'Referrer Policy',
description:
"Indicates which referrer to send when fetching the frame's resource.",
}
),
sandbox: StringUnion(
[
'unset',
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation',
'allow-top-navigation-by-user-activation',
],
{
title: 'Sandbox',
description: 'Applies extra restrictions to the content in the frame.',
}
),
fetchpriority: StringUnion(['auto', 'low', 'high'], {
title: 'Fetch Priority',
description:
'Provides a hint of the relative priority to use when fetching the iframe document',
}),
}),
state: Type.Object({}),
methods: {
requestFullscreen: Type.Object({}),
},
slots: {},
styleSlots: ['content'],
events: [],
},
})(
({
component,
src,
referrerpolicy,
sandbox,
fetchpriority,
elementRef,
customStyle,
subscribeMethods,
}) => {
const iframeProps: Record<string, unknown> = {
title: component.id,
src,
fetchpriority,
};
if (referrerpolicy !== 'unset') {
iframeProps.referrerpolicy = referrerpolicy;
}
if (sandbox !== 'unset') {
iframeProps.sandbox = sandbox;
}
useEffect(() => {
subscribeMethods({
requestFullscreen() {
elementRef?.current.requestFullscreen();
},
});
}, [elementRef, subscribeMethods]);
return (
<iframe ref={elementRef} {...iframeProps} className={css(customStyle?.content)} />
);
}
);

View File

@ -0,0 +1,112 @@
import { Type } from '@sinclair/typebox';
import { css } from '@emotion/css';
import { LIST_ITEM_EXP, LIST_ITEM_INDEX_EXP } from '../../constants';
import { implementRuntimeComponent } from '../../utils/buildKit';
import { ImplWrapper } from '../_internal/ImplWrapper';
const PropsSpec = Type.Object({
listData: Type.Array(Type.Record(Type.String(), Type.String()), {
title: 'Data',
category: 'Basic',
widget: `core/v1/expression`,
}),
});
const exampleProperties = {
listData: [{ id: '1' }, { id: '2' }, { id: '3' }],
};
export default implementRuntimeComponent({
version: 'core/v1',
metadata: {
name: 'list',
description: 'core list',
displayName: 'List',
isDraggable: true,
isResizable: true,
exampleProperties,
exampleSize: [6, 6],
annotations: {
category: 'Display',
},
},
spec: {
properties: PropsSpec,
methods: {},
state: Type.Object({}),
slots: {
content: {
slotProps: Type.Object({
[LIST_ITEM_EXP]: Type.Any(),
[LIST_ITEM_INDEX_EXP]: Type.Number(),
}),
},
},
styleSlots: ['content'],
events: [],
},
})(({ listData, component, app, services, customStyle, elementRef }) => {
if (!listData) {
return null;
}
const childrenSchema = app.spec.components.filter(c => {
return c.traits.find(
t =>
t.type === 'core/v1/slot' && (t.properties.container as any).id === component.id
);
});
if (childrenSchema.length === 0) {
return (
<div
className={css`
${customStyle?.content}
`}
ref={elementRef}
>
List has no children. Drag components into here to add listItems.
</div>
);
}
const listItems = listData.map((listItem, i) => {
const _childrenSchema = childrenSchema.map(child => {
return {
...child,
id: `${component.id}_${child.id}_${i}`,
};
});
const childrenEles = _childrenSchema.map(child => {
return (
<ImplWrapper
key={child.id}
component={child}
app={app}
services={services}
childrenMap={{}}
isInModule
evalListItem
slotProps={{
[LIST_ITEM_EXP]: listItem,
[LIST_ITEM_INDEX_EXP]: i,
}}
/>
);
});
return <li key={i}>{childrenEles}</li>;
});
return (
<ul
className={css`
list-style: none;
${customStyle?.content}
`}
ref={elementRef}
>
{listItems}
</ul>
);
});

View File

@ -1,5 +1,5 @@
import { implementRuntimeComponent } from '../../utils/buildKit';
import { ModuleSpec, CORE_VERSION, CoreComponentName } from '@sunmao-ui/shared';
import { ModuleRenderSpec, CORE_VERSION, CoreComponentName } from '@sunmao-ui/shared';
import { ModuleRenderer } from '../_internal/ModuleRenderer';
export default implementRuntimeComponent({
@ -20,7 +20,7 @@ export default implementRuntimeComponent({
},
},
spec: {
properties: ModuleSpec,
properties: ModuleRenderSpec,
state: {},
methods: {},
slots: {},

View File

@ -67,12 +67,8 @@ export type { RegistryInterface, SunmaoLib } from './services/Registry';
export { ExpressionError } from './services/StateManager';
export type { StateManagerInterface } from './services/StateManager';
export { ModuleRenderer } from './components/_internal/ModuleRenderer';
export { ImplWrapper } from './components/_internal/ImplWrapper';
export { default as Text, TextPropertySpec } from './components/_internal/Text';
export {
EventHandlerSpec,
EventCallBackHandlerSpec,
ModuleSpec,
} from '@sunmao-ui/shared';
// TODO: check this export
export { watch } from './utils/watchReactivity';

View File

@ -8,6 +8,8 @@ import CoreDummy from '../components/core/Dummy';
import CoreModuleContainer from '../components/core/ModuleContainer';
import CoreStack from '../components/core/Stack';
import CoreFileInput from '../components/core/FileInput';
import CoreList from '../components/core/List';
import CoreIframe from '../components/core/Iframe';
// traits
import CoreArrayState from '../traits/core/ArrayState';
@ -242,6 +244,8 @@ export function initRegistry(
registry.registerComponent(CoreModuleContainer);
registry.registerComponent(CoreStack);
registry.registerComponent(CoreFileInput);
registry.registerComponent(CoreList);
registry.registerComponent(CoreIframe);
registry.registerTrait(CoreState);
registry.registerTrait(CoreArrayState);

View File

@ -11,11 +11,16 @@ export const EventTraitPropertiesSpec = Type.Object({
export const generateCallback = (
handler: Omit<Static<typeof EventHandlerSpec>, 'type'>,
rawHandler: Static<typeof EventHandlerSpec>,
services: UIServices
services: UIServices,
slotProps?: unknown,
evalListItem?: boolean
) => {
const send = () => {
// Eval before sending event to assure the handler object is evaled from the latest state.
const evaledHandler = services.stateManager.deepEval(rawHandler);
const evaledHandler = services.stateManager.deepEval(rawHandler, {
scopeObject: { $slot: slotProps },
evalListItem,
});
if (evaledHandler.disabled && typeof evaledHandler.disabled === 'boolean') {
return;
@ -54,7 +59,7 @@ export default implementRuntimeTrait({
state: {},
},
})(() => {
return ({ trait, handlers, services }) => {
return ({ trait, handlers, services, slotProps, evalListItem }) => {
const callbackQueueMap: Record<string, Array<() => void>> = {};
const rawHandlers = trait.properties.handlers as Static<typeof EventHandlerSpec>[];
// setup current handlers
@ -64,7 +69,7 @@ export default implementRuntimeTrait({
callbackQueueMap[handler.type] = [];
}
callbackQueueMap[handler.type].push(
generateCallback(handler, rawHandlers[i], services)
generateCallback(handler, rawHandlers[i], services, slotProps, evalListItem)
);
}

View File

@ -15,7 +15,8 @@ export type ImplWrapperProps<KSlot extends string = string> = {
childrenMap: ChildrenMap<KSlot>;
services: UIServices;
isInModule: boolean;
app?: RuntimeApplication;
app: RuntimeApplication;
evalListItem?: boolean;
slotProps?: unknown;
} & ComponentParamsFromApp;
@ -28,7 +29,7 @@ export type ComponentImplProps<
> = ImplWrapperProps &
TraitResult<KStyleSlot, KEvent>['props'] &
RuntimeFunctions<TState, TMethods, TSlots> & {
elementRef?: React.Ref<any>;
elementRef?: React.MutableRefObject<any>;
getElement?: (ele: HTMLElement) => void;
};

View File

@ -20,6 +20,8 @@ export type TraitImpl<T = any> = (
trait: RuntimeTraitSchema;
componentId: string;
services: UIServices;
evalListItem?: boolean;
slotProps?: unknown;
}
) => TraitResult<string, string>;

View File

@ -3,7 +3,7 @@ import { Static, TSchema } from '@sinclair/typebox';
import { RegistryInterface } from '../services/Registry';
import { StateManagerInterface } from '../services/StateManager';
import {
ModuleSpec,
ModuleRenderSpec,
parseTypeBox,
CORE_VERSION,
CoreComponentName,
@ -36,7 +36,7 @@ export function initSingleComponentState(
stateManager.store[c.id] = state;
if (c.type === `${CORE_VERSION}/${CoreComponentName.ModuleContainer}`) {
const moduleSchema = c.properties as Static<typeof ModuleSpec>;
const moduleSchema = c.properties as Static<typeof ModuleRenderSpec>;
try {
const mSpec = registry.getModuleByType(moduleSchema.type).spec;
const moduleInitState: Record<string, unknown> = {};

View File

@ -6,6 +6,7 @@ export enum CoreComponentName {
ModuleContainer = 'moduleContainer',
GridLayout = 'grid_layout',
Text = 'text',
Iframe = 'iframe',
}
// core traits
export enum CoreTraitName {

View File

@ -2,7 +2,7 @@ import { EventHandlerSpec } from './event';
import { Type } from '@sinclair/typebox';
import { CORE_VERSION, CoreWidgetName } from '../constants/core';
export const ModuleSpec = Type.Object(
export const ModuleRenderSpec = Type.Object(
{
id: Type.String({
title: 'Module ID',