Merge branch feat/plugin-utils-pane into release/1.0.0

Title: feat: 完成 utils 面板的基本功能 

Link: https://code.aone.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/codereview/4227732
This commit is contained in:
rongbin.arb 2020-12-02 16:04:26 +08:00
commit 47a32edcd9
21 changed files with 1168 additions and 7 deletions

View File

@ -2,8 +2,9 @@ import logo from '@ali/lowcode-plugin-sample-logo';
import samplePreview from '@ali/lowcode-plugin-sample-preview';
import undoRedo from '@ali/lowcode-plugin-undo-redo';
import componentsPane from '@ali/lowcode-plugin-components-pane';
import outline, { OutlinePane } from '@ali/lowcode-plugin-outline-pane';
import outline from '@ali/lowcode-plugin-outline-pane';
import dataSourcePane from '@ali/lowcode-plugin-datasource-pane';
import utilsPane from '@ali/lowcode-plugin-utils-pane';
import zhEn from '@ali/lowcode-plugin-zh-en';
import eventBindDialog from '@ali/lowcode-plugin-event-bind-dialog';
import variableBindDialog from '@ali/lowcode-plugin-variable-bind-dialog';
@ -18,6 +19,7 @@ export default {
undoRedo,
componentsPane,
outline,
utilsPane,
zhEn,
eventBindDialog,
variableBindDialog,

View File

@ -83,6 +83,26 @@ export default {
},
pluginProps: {},
},
{
pluginKey: 'utilsPane',
type: 'PanelIcon',
props: {
align: 'top',
icon: 'util',
description: '工具类',
panelProps: {
floatable: true,
height: 300,
help: undefined,
hideTitleBar: false,
maxHeight: 800,
maxWidth: 1200,
title: '工具类扩展面板',
width: 430,
},
},
pluginProps: {},
},
{
pluginKey: 'sourceEditor',
type: 'PanelIcon',
@ -183,14 +203,14 @@ export default {
'https://dev.g.alicdn.com/ali-lowcode/ali-lowcode-engine/1.0.0/react-simulator-renderer.css',
//'https://dev.g.alicdn.com/ali-lowcode/ali-lowcode-engine/1.0.0/react-simulator-renderer.js',
// for debug
'http://localhost:3333/js/react-simulator-renderer.js',
'http://localhost:3333/js/react-simulator-renderer.js',
// 'http://localhost:3333/js/react-simulator-renderer.css',
];
editor.set('simulatorUrl', simulatorUrl);
editor.set('requestHandlersMap', {
mtop: createMtopHandler(),
fetch: createFetchHandler(),
jsonp: createJsonpHandler()
jsonp: createJsonpHandler(),
});
// editor.set('renderEnv', 'rax');

View File

@ -104,7 +104,9 @@ export class Project {
| string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
value: any,
): void {}
): void {
Object.assign(this.data, { [key]: value });
}
/**
*
@ -121,7 +123,9 @@ export class Project {
| 'css'
| 'dataSource'
| string,
): any {}
): any {
return Reflect.get(this.data, key);
}
open(doc?: string | DocumentModel | RootSchema): DocumentModel {
if (!doc) {
@ -152,7 +156,9 @@ export class Project {
if (isDocumentModel(doc)) {
return doc.open();
} else if (isPageSchema(doc)) {
const foundDoc = this.documents.find(curDoc => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id);
const foundDoc = this.documents.find(
(curDoc) => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id,
);
if (foundDoc) {
foundDoc.remove();
}

View File

@ -0,0 +1 @@
# Change Log

View File

@ -0,0 +1,3 @@
## 关于 @ali/lowcode-plugin-utils-pane
这是低代码引擎的 utils 面板。

View File

@ -0,0 +1,9 @@
{
"plugins": [
"build-plugin-component",
"build-plugin-fusion",
["build-plugin-moment-locales", {
"locales": ["zh-cn"]
}]
]
}

View File

@ -0,0 +1,43 @@
{
"name": "@ali/lowcode-plugin-utils-pane",
"version": "1.0.8-0",
"description": "alibaba lowcode editor utils pane plugin",
"files": [
"es/",
"lib/"
],
"main": "lib/index.js",
"module": "es/index.js",
"stylePath": "style.js",
"scripts": {
"build": "build-scripts build --skip-demo",
"test": "ava",
"test:snapshot": "ava --update-snapshots"
},
"keywords": [
"lowcode",
"editor"
],
"author": "changyun.pcy",
"dependencies": {
"@alifd/next": "1.x"
},
"peerDependencies": {
"@ali/lowcode-editor-core": "^1.0.8-0",
"prop-types": "^15.5.8",
"react": "^16.8.1",
"react-dom": "^16.8.1",
"react-router-dom": "^5.1.2"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.3",
"@types/react": "^16.9.13",
"@types/react-dom": "^16.9.4",
"build-plugin-component": "^0.2.7-1",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"
}
}

View File

@ -0,0 +1 @@
export * from './js-function';

View File

@ -0,0 +1,55 @@
import React, { PureComponent } from 'react';
import { connect } from '@formily/react-schema-renderer';
import MonacoEditor, { EditorWillMount } from 'react-monaco-editor';
import noop from 'lodash/noop';
export interface JSFunctionProps {
className: string;
value: string;
onChange?: (val: string) => void;
}
type Arg0TypeOf<T> = T extends (arg0: infer U) => any ? U : never;
type MonacoRef = Arg0TypeOf<EditorWillMount>;
class InternalJSFunction extends PureComponent<JSFunctionProps, unknown> {
static isFieldComponent = true;
static defaultProps = {
onChange: noop,
};
private monacoRef: MonacoRef | null = null;
private handleEditorChange = () => {
if (
this.monacoRef &&
this.monacoRef.editor &&
!this.monacoRef.editor.getModelMarkers({}).find((marker) => marker.owner === 'json') &&
this.props.onChange
) {
this.props.onChange(this.monacoRef.editor.getModels()?.[0]?.getValue());
}
};
private handleEditorWillMount: EditorWillMount = (monaco) => {
this.monacoRef = monaco;
};
render() {
const { value } = this.props;
return (
<MonacoEditor
theme="vs-dark"
width={400}
height={150}
defaultValue={value}
language="js"
onChange={this.handleEditorChange}
editorWillMount={this.handleEditorWillMount}
/>
);
}
}
export const JSFunction = connect()(InternalJSFunction);

View File

@ -0,0 +1,116 @@
.lowcode-plugin-utils-pane {
margin: 0 8px;
> .next-tabs {
> .next-tabs-bar {
.next-tabs-nav-extra {
.next-btn {
margin-left: 4px;
}
}
}
}
}
.lowcode-plugin-utils-pane-list {
margin: 8px;
.next-search {
width: 100%;
.next-search-left {
height: 28px !important;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
.next-before {
height: 28px !important;
.next-select {
height: 28px !important;
}
}
.next-search-input {
height: 28px !important;
input {
height: 28px !important;
}
}
.next-input {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.next-after {
// height: 28px !important;
.next-btn {
height: 30px !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
.next-icon:before {
font-size: 12px;
}
}
}
}
.utils-list {
margin-top: 8px;
height: unquote('calc(100vh - 48px - 48px - 42px - 28px - 8px - 8px)');
overflow: auto;
.next-virtual-list-wrapper > div > ul > li {
border-top: 1px solid #ddd;
&:first-child {
border-top: none;
}
}
}
.utils-item {
margin: 8px;
.utils-item-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: 28px;
.next-btn {
margin-left: 4px;
}
}
.utils-item-name-wrap {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-family: monospace;
}
.utils-item-from {
color: rgba(0, 0, 0, 0.65);
font-size: 12px;
}
.utils-item-import-from {
color: rgba(0, 0, 0, 0.45);
}
.utils-item-desc {
.next-tag {
margin-right: 4px;
}
}
}
}
.lowcode-plugin-utils-form {
height: unquote('calc(100vh - 48px - 48px - 42px)');
overflow: auto;
.next-form-item {
.next-form-item-control {
font-size: 100%;
}
}
}
.utils-item-detail-func-expr {
pre {
max-width: 100%;
overflow: auto;
}
}

View File

@ -0,0 +1,86 @@
import React, { PureComponent } from 'react';
import { PluginProps, UtilItem, UtilsMap } from '@ali/lowcode-types';
import get from 'lodash/get';
import { UtilsPane, UtilTypeInfo } from './pane';
import './index.scss';
import { DEFAULT_UTILS_TYPES } from './utils-types';
import { DEFAULT_UTILS } from './utils-defaults';
const PLUGIN_NAME = 'utilsPane';
export interface UtilsPaneProps extends PluginProps {
/**
* Util
*/
utilsTypes: UtilTypeInfo[];
/**
* Utils ( schema utils)
*/
initialUtils?: UtilItem[];
}
interface State {
active: boolean;
}
export default class UtilsPanePlugin extends PureComponent<UtilsPaneProps, State> {
static displayName = 'UtilsPanePlugin';
static defaultProps = {
initialUtils: DEFAULT_UTILS,
};
state = {
active: false,
};
constructor(props: UtilsPaneProps) {
super(props);
this.state.active = true;
const { editor } = this.props;
// @todo pluginName, to unsubscribe
// 第一次 active 事件不会触发监听器
editor.on('skeleton.panel-dock.active', (pluginName) => {
if (pluginName === PLUGIN_NAME) {
this.setState({ active: true });
}
});
editor.on('skeleton.panel-dock.unactive', (pluginName) => {
if (pluginName === PLUGIN_NAME) {
this.setState({ active: false });
}
});
}
render() {
const { initialUtils = DEFAULT_UTILS, utilsTypes = DEFAULT_UTILS_TYPES, editor } = this.props;
const { active } = this.state;
if (!active) return null;
const projectSchema = editor.get('designer').project.getSchema() ?? {};
return (
<UtilsPane
initialUtils={initialUtils}
utilTypes={utilsTypes}
schema={get(projectSchema, 'utils')}
onSchemaChange={this.handleSchemaChange}
/>
);
}
private handleSchemaChange = (utilsMap: UtilsMap) => {
const { editor } = this.props;
// @TODO 姿势是否最优?
if (editor.get('designer')) {
editor.get('designer').project.set('utils', utilsMap);
}
};
}

View File

@ -0,0 +1,191 @@
import { UtilItem } from '@ali/lowcode-types';
import { Balloon, Button, Search, Table, Tag, VirtualList } from '@alifd/next';
import tap from 'lodash/tap';
import React, { PureComponent } from 'react';
import type { UtilTypeInfo } from './pane';
const { Column: TableCol } = Table;
export interface UtilsListProps {
utilTypes: UtilTypeInfo[];
utilItems: UtilItem[] | null | undefined;
onEditUtil?: (utilName: string) => void;
onDuplicateUtil?: (utilName: string) => void;
onRemoveUtil?: (utilName: string) => void;
}
interface State {
filteredType: string;
keyword: string;
}
type TableRow = {
label: string;
value: any;
};
export class UtilList extends PureComponent<UtilsListProps, State> {
state = {
filteredType: '',
keyword: '',
};
private handleSearchFilterChange = (filteredType: any) => {
this.setState({ filteredType });
};
private handleSearch = (keyword: any) => {
this.setState({ keyword });
};
private handleEditUtilItem = (id: any) => {
if (this.props.onEditUtil) {
this.props.onEditUtil(id);
}
};
private handleDuplicateUtilItem = (id: any) => {
if (this.props.onDuplicateUtil) {
this.props.onDuplicateUtil(id);
}
};
private handleRemoveDataSource = (id: any) => {
if (this.props.onRemoveUtil) {
this.props.onRemoveUtil(id);
}
};
private renderVirtualUtilsList = () => {
const { filteredType, keyword } = this.state;
const utilsMap = this.props.utilItems || [];
const { utilTypes } = this.props;
return (
utilsMap
.filter((item) => !filteredType || item.type === filteredType)
.filter((item) => !keyword || item.name.indexOf(keyword) >= 0)
.map((item) => (
<li key={item.name}>
<div className="utils-item">
<div className="utils-item-title">
<div className="utils-item-name-wrap" title={item.name}>
<span className="utils-item-name">{item.name}</span>
{(item.type === 'npm' || item.type === 'tnpm') && (
<span className="utils-item-from">
{' '}
<span className="utils-item-import-from"></span>{' '}
<span className="utils-item-package-name">
&quot;{item.content?.package}
{item.content?.main ? `/${item.content?.main}` : ''}&quot;
</span>
<span className="utils-item-export-name">
{item.content?.exportName && item.content?.exportName !== item.name
? `中的 ${item.content?.exportName}${
item.content?.subName ? `.${item.content?.subName}` : ''
}`
: ''}
</span>
</span>
)}
</div>
{!!utilTypes.some((t) => t.type === item.type) &&
this.renderItemDetailBalloon(item)}
{!!utilTypes.some((t) => t.type === item.type) && (
<Button size="small" onClick={this.handleEditUtilItem.bind(this, item.name)}>
</Button>
)}
{!!utilTypes.some((t) => t.type === item.type) && (
<Button size="small" onClick={this.handleDuplicateUtilItem.bind(this, item.name)}>
</Button>
)}
<Button size="small" onClick={this.handleRemoveDataSource.bind(this, item.name)}>
</Button>
</div>
<div className="utils-item-desc">
<Tag size="small">{item.type}</Tag>
{(item.type === 'npm' || item.type === 'tnpm') && item.content?.destructuring && (
<Tag size="small"></Tag>
)}
</div>
</div>
</li>
)) || []
);
};
private renderItemDetailBalloon(item: UtilItem): React.ReactNode {
return (
<Balloon
trigger={<Button size="small"></Button>}
align="b"
alignEdge
triggerType="hover"
style={{ width: 300 }}
>
{item.type === 'function' ? (
<div className="utils-item-detail-func-expr">
<pre>
<code>{item.content?.value || ''}</code>
</pre>
</div>
) : (
<Table
dataSource={tap(
Object.entries({
type: item.type,
...item.content,
}).map<TableRow>(([key, value]) => ({
label: key,
value: `${value}`,
})),
console.log,
)}
>
<TableCol title="" dataIndex="label" />
<TableCol
title=""
dataIndex="value"
cell={(val: any) => <div>{typeof val === 'string' ? `"${val}"` : `${val}`}</div>}
/>
</Table>
)}
</Balloon>
);
}
render() {
const { utilTypes } = this.props;
const { filteredType } = this.state;
return (
<div className="lowcode-plugin-utils-pane-list">
<Search
hasClear
onSearch={this.handleSearch}
filterProps={{}}
defaultFilterValue={filteredType}
filter={[
{
label: '全部',
value: '',
},
...(utilTypes || []).map((utilType) => ({
label: utilType.label,
value: utilType.type,
})),
]}
onFilterChange={this.handleSearchFilterChange}
/>
<div className="utils-list">
<VirtualList>{this.renderVirtualUtilsList()}</VirtualList>
</div>
</div>
);
}
}

View File

@ -0,0 +1,3 @@
{
"UtilsPane": "Utils Pane"
}

View File

@ -0,0 +1,10 @@
import { createIntl } from '@ali/lowcode-editor-core';
import enUS from './en-US.json';
import zhCN from './zh-CN.json';
const { intl, intlNode, getLocale, setLocale } = createIntl({
'en-US': enUS,
'zh-CN': zhCN,
});
export { intl, intlNode, getLocale, setLocale };

View File

@ -0,0 +1,3 @@
{
"UtilsPane": "工具类扩展面板"
}

View File

@ -0,0 +1,308 @@
/**
* Dialog
*/
import { UtilItem, UtilsMap } from '@ali/lowcode-types';
import { Button, Dialog, MenuButton, Message, Tab } from '@alifd/next';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
import React, { PureComponent } from 'react';
import { UtilList } from './list';
import { UtilsForm } from './utils-form';
const { Item: TabItem } = Tab;
const { Item: MenuButtonItem } = MenuButton;
enum PaneTabKey {
List = 'list',
Create = 'create',
Edit = 'edit',
}
export type UtilTypeInfo = {
type: UtilItem['type'];
label: string;
};
export interface UtilsPaneProps {
initialUtils?: UtilsMap | null;
schema?: UtilsMap | null;
utilTypes: UtilTypeInfo[];
onSchemaChange?: (schema: UtilsMap) => void;
}
export interface TabItem {
key: PaneTabKey;
title: string;
closeable: boolean;
data?: Partial<UtilItem>;
}
interface State {
utilItems: UtilsMap;
tabItems: TabItem[];
activeTabKey: PaneTabKey;
}
export class UtilsPane extends PureComponent<UtilsPaneProps, State> {
state: State = {
utilItems: this.props.schema || this.props.initialUtils || [],
tabItems: [
{
key: PaneTabKey.List,
title: '工具类扩展列表',
closeable: false,
},
],
activeTabKey: PaneTabKey.List,
};
private notifyItemsChanged = () => {
this.setState({}, () => {
if (this.props.onSchemaChange) {
this.props.onSchemaChange(this.state.utilItems);
}
});
};
private handleCreateItem = (newItem: UtilItem) => {
const doSaveNewItem = () => {
this.closeTab(PaneTabKey.Create);
this.setState(({ utilItems }) => ({
utilItems: [{ ...newItem }, ...utilItems.filter((x) => x.name !== newItem.name)],
}));
this.notifyItemsChanged();
};
if (this.state.utilItems.some((util) => util.name === newItem.name)) {
Dialog.confirm({
content: `工具类扩展 "${newItem.name}" 已存在,如果导入会替换已存在的扩展,是否继续?`,
onOk: () => {
doSaveNewItem();
},
});
return;
}
doSaveNewItem();
};
private handleUpdateItem = (changedItem: UtilItem) => {
this.closeTab(PaneTabKey.Edit);
this.setState(({ utilItems }) => ({
utilItems: utilItems.map((x) => [x.name === changedItem.name ? changedItem : x][0]),
}));
this.notifyItemsChanged();
};
private handleRemoveItem = (toBeRemovedUtilName: string) => {
const doRemove = () => {
this.setState(
({ utilItems }) => ({
utilItems: utilItems.filter((item) => item.name !== toBeRemovedUtilName),
}),
() => {
this.notifyItemsChanged();
},
);
};
Dialog.confirm({
content: '确定要删除吗?',
onOk: () => {
doRemove();
},
});
};
private handleDuplicateItem = (utilName: string) => {
const targetUtil = this.state.utilItems.find((item) => item.name === utilName);
if (!targetUtil) {
return;
}
this.openCreateItemTab({
...cloneDeep(targetUtil),
name: `${targetUtil.name}Copy`,
});
};
private handleEditItem = (utilName: string) => {
const targetUtil = this.state.utilItems.find((item) => item.name === utilName);
if (!targetUtil) {
return;
}
this.openEditDataSourceTab(cloneDeep(targetUtil));
};
private handleTabChange = (activeTabKey: string | number) => {
if (isValidTabKey(activeTabKey)) {
this.setState({ activeTabKey });
}
};
private openCreateItemTab = (initialItem: Partial<UtilItem>) => {
const { tabItems } = this.state;
if (!tabItems.find((item) => item.key === PaneTabKey.Create)) {
this.setState(({ tabItems: latestTabItems }) => ({
tabItems: latestTabItems.concat({
key: PaneTabKey.Create,
title: '添加工具类扩展',
closeable: true,
data: {
...initialItem,
},
}),
}));
this.setState({ activeTabKey: PaneTabKey.Create });
} else {
Message.notice('当前已经有一个添加工具类扩展的标签页了');
}
};
private handleCreateItemBtnClick = (dataSourceType: string) => {
this.openCreateItemTab({
type: dataSourceType as UtilItem['type'],
});
};
private handleCreateItemMenuBtnClick = (dataSourceType: string) => {
this.openCreateItemTab({
type: dataSourceType as UtilItem['type'],
});
};
private openEditDataSourceTab = (utilItem: UtilItem) => {
const { tabItems } = this.state;
if (!tabItems.find((item) => item.key === PaneTabKey.Edit)) {
this.setState(({ tabItems: latestTabItems }) => ({
tabItems: latestTabItems.concat({
key: PaneTabKey.Edit,
title: '修改工具类扩展',
closeable: true,
data: {
...utilItem,
},
}),
}));
}
this.setState({ activeTabKey: PaneTabKey.Edit });
};
private closeTab = (tabKey: any) => {
this.setState(
({ tabItems }) => ({
tabItems: tabItems.filter((item) => item.key !== tabKey),
}),
() => {
this.setState(({ tabItems }) => ({
activeTabKey: get(tabItems, '[0].key'),
}));
},
);
};
renderTabExtraContent = () => {
const { utilTypes } = this.props;
if (isArray(utilTypes)) {
if (utilTypes.length > 1) {
return [
<MenuButton label="添加" onItemClick={this.handleCreateItemMenuBtnClick}>
{utilTypes.map((type) => (
<MenuButtonItem key={type.type}>{type.label}</MenuButtonItem>
))}
</MenuButton>,
];
} else if (utilTypes.length === 1) {
return [
<Button onClick={this.handleCreateItemBtnClick.bind(this, utilTypes[0].type)}>
</Button>,
];
} else {
return [];
}
}
return [];
};
// 更通用的处理
private renderTabItemContent = (
tabItemKey: PaneTabKey,
data: Partial<UtilItem> | undefined | null,
) => {
const { utilItems } = this.state;
const { utilTypes = [] } = this.props;
if (tabItemKey === PaneTabKey.List) {
if (utilItems.length <= 0) {
return (
<Message type="help" title="暂无工具类扩展">
</Message>
);
}
return (
<UtilList
utilTypes={utilTypes}
utilItems={utilItems}
onEditUtil={this.handleEditItem}
onDuplicateUtil={this.handleDuplicateItem}
onRemoveUtil={this.handleRemoveItem}
/>
);
} else if (tabItemKey === PaneTabKey.Edit) {
return (
<UtilsForm
item={data}
onComplete={this.handleUpdateItem}
onCancel={this.closeTab.bind(this, PaneTabKey.Edit)}
/>
);
} else if (tabItemKey === PaneTabKey.Create) {
return (
<UtilsForm
item={data}
onComplete={this.handleCreateItem}
onCancel={this.closeTab.bind(this, tabItemKey)}
/>
);
} else {
console.warn('Unknown tab type: ', tabItemKey);
return null;
}
};
render() {
const { activeTabKey, tabItems } = this.state;
return (
<div className="lowcode-plugin-datasource-pane">
<Tab
activeKey={activeTabKey}
extra={this.renderTabExtraContent()}
onChange={this.handleTabChange}
onClose={this.closeTab}
>
{tabItems.map((item: TabItem) => (
<TabItem {...item}>{this.renderTabItemContent(item.key, item.data)}</TabItem>
))}
</Tab>
</div>
);
}
}
function isValidTabKey(tabKey: unknown): tabKey is PaneTabKey {
return typeof tabKey === 'string' && (Object.values(PaneTabKey) as string[]).includes(tabKey);
}

View File

@ -0,0 +1,31 @@
import { UtilItem } from '@ali/lowcode-types';
export const DEFAULT_UTILS: UtilItem[] = [
{
type: 'npm',
name: 'clone',
content: {
package: 'lodash',
destructuring: true,
},
},
{
type: 'npm',
name: 'moment',
content: {
package: 'moment',
destructuring: false,
},
},
{
type: 'function',
name: 'record',
content: {
type: 'JSFunction',
value: `function(logkey, gmkey, gokey, reqMethod) {
goldlog.record('/demo.event.' + logkey, gmkey, gokey, reqMethod);
}
`,
},
},
];

View File

@ -0,0 +1,247 @@
// @todo schema default
import { UtilItem } from '@ali/lowcode-types';
import { Button } from '@alifd/next';
import { FormButtonGroup, registerValidationFormats, SchemaForm, Submit } from '@formily/next';
import { ArrayTable, Input, NumberPicker, Switch } from '@formily/next-components';
import memorize from 'lodash/memoize';
import React, { PureComponent } from 'react';
import { JSFunction } from './form-components';
registerValidationFormats({
util_npm_version_format: /^\d+\.\d+\.\d+(-[a-z0-9-]+(\.[a-z0-9]+))?$/i,
util_name_js_identifier: /[a-z$_][a-z$_0-9]+/i,
});
type FlatUtilItem = {
name: string;
type: UtilItem['type'];
// NPM/TNPM util:
componentName?: string;
package?: string;
version?: string;
destructuring?: boolean;
exportName?: string;
subName?: string;
main?: string;
// function util
functionExpr?: string;
};
const FORM_SCHEMA_NPM = {
type: 'object',
properties: {
type: TYPE_FIELD(),
name: NAME_FIELD(),
componentName: {
type: 'string',
title: 'componentName',
display: false,
},
package: {
type: 'string',
title: '包名',
required: true,
},
version: {
type: 'string',
title: '版本号',
required: false,
'x-rules': {
format: 'util_npm_version_format',
},
},
destructuring: {
type: 'boolean',
title: '需解构',
required: false,
},
exportName: {
type: 'string',
title: '导出名',
// hide: '{{!destructuring}}', // TODO: 这联动一直报错
required: false,
},
subName: {
type: 'string',
title: '子导出名',
// hide: '{{!destructuring}}',
required: false,
},
main: {
type: 'string',
title: '入口文件',
required: false,
},
},
};
const FORM_SCHEMA_FUNCTION = {
type: 'object',
properties: {
type: TYPE_FIELD(),
name: NAME_FIELD(),
functionExpr: {
type: 'string',
title: '函数定义',
required: true,
'x-component': 'JSFunction',
'x-component-props': {
defaultValue: `/**
* util
* this.xxx 访 API
**/
function () {
console.log("Hello world! (Context: %o)", this);
// TODO: 完善这个 util 函数
}
`,
},
},
},
};
export interface UtilsFormProps {
item?: Partial<UtilItem> | null;
onComplete?: (item: UtilItem) => void;
onCancel?: () => void;
}
/**
* ID
*/
export class UtilsForm extends PureComponent<UtilsFormProps, unknown> {
state = {};
private handleFormSubmit = (formData: any) => {
const utilItem = parseFlatUtilItem(formData);
if (this.props.onComplete) {
this.props.onComplete(utilItem);
}
};
private handleCancel = () => {
if (this.props.onCancel) {
this.props.onCancel();
}
};
private getInitialValues = memorize((utilItem: Partial<UtilItem> | undefined | null) => {
return flattenUtilItem(utilItem || {});
});
private readonly formComponents = {
string: Input,
boolean: Switch,
number: NumberPicker,
ArrayTable,
Input,
NumberPicker,
Switch,
JSFunction,
};
private readonly formLabelCol = {
span: 6,
};
private readonly formWrapperCol = {
span: 16,
};
private get schema() {
return this.props.item?.type === 'function' ? FORM_SCHEMA_FUNCTION : FORM_SCHEMA_NPM;
}
render() {
const { item } = this.props;
return (
<div className="lowcode-plugin-datasource-form">
<SchemaForm
onSubmit={this.handleFormSubmit}
components={this.formComponents}
labelCol={this.formLabelCol}
wrapperCol={this.formWrapperCol}
schema={this.schema}
initialValues={this.getInitialValues(item)}
>
<FormButtonGroup offset={this.formLabelCol.span}>
<Submit></Submit>
<Button onClick={this.handleCancel}></Button>
</FormButtonGroup>
</SchemaForm>
</div>
);
}
}
function TYPE_FIELD() {
return {
title: '类型',
type: 'string',
editable: false,
'x-component': 'Input',
'x-component-props': {
readOnly: true,
},
};
}
function NAME_FIELD() {
return {
type: 'string',
title: '引用名',
required: true,
'x-component-props': {
placeholder: '请输入引用名(工具类扩展在引用时的名称)',
autoFocus: true,
},
'x-rules': {
format: 'util_name_js_identifier',
},
};
}
function flattenUtilItem(utilItem: Partial<UtilItem>): FlatUtilItem {
return {
...(utilItem.type === 'function'
? {
functionExpr: utilItem.content?.value,
}
: utilItem.content),
name: utilItem.name || '',
type: utilItem.type || 'npm',
};
}
function parseFlatUtilItem(flatUtil: FlatUtilItem): UtilItem {
if (flatUtil.type === 'function') {
return {
name: flatUtil.name,
type: flatUtil.type,
content: {
type: 'JSFunction',
value: flatUtil.functionExpr || '',
},
};
}
return {
name: flatUtil.name,
type: flatUtil.type,
content: {
componentName: flatUtil.componentName || flatUtil.name,
package: flatUtil.package || '',
version: flatUtil.version,
destructuring: flatUtil.destructuring ?? false,
exportName: flatUtil.exportName,
subName: flatUtil.subName,
main: flatUtil.main,
},
};
}

View File

@ -0,0 +1,16 @@
import { UtilTypeInfo } from './pane';
export const DEFAULT_UTILS_TYPES: UtilTypeInfo[] = [
{
type: 'npm',
label: 'NPM 包',
},
{
type: 'tnpm',
label: 'TNPM 包',
},
{
type: 'function',
label: '自定义函数',
},
];

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"include": [
"./src/"
]
}

View File

@ -54,5 +54,5 @@
"publishConfig": {
"registry": "http://registry.npm.alibaba-inc.com"
},
"homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@1.0.20/build/index.html"
"homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@1.0.21/build/index.html"
}