mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 01:21:58 +00:00
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:
commit
47a32edcd9
@ -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,
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
1
packages/plugin-utils-pane/CHANGELOG.md
Normal file
1
packages/plugin-utils-pane/CHANGELOG.md
Normal file
@ -0,0 +1 @@
|
||||
# Change Log
|
||||
3
packages/plugin-utils-pane/README.md
Normal file
3
packages/plugin-utils-pane/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
## 关于 @ali/lowcode-plugin-utils-pane
|
||||
|
||||
这是低代码引擎的 utils 面板。
|
||||
9
packages/plugin-utils-pane/build.json
Normal file
9
packages/plugin-utils-pane/build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": [
|
||||
"build-plugin-component",
|
||||
"build-plugin-fusion",
|
||||
["build-plugin-moment-locales", {
|
||||
"locales": ["zh-cn"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
43
packages/plugin-utils-pane/package.json
Normal file
43
packages/plugin-utils-pane/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
packages/plugin-utils-pane/src/form-components/index.ts
Normal file
1
packages/plugin-utils-pane/src/form-components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './js-function';
|
||||
@ -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);
|
||||
116
packages/plugin-utils-pane/src/index.scss
Normal file
116
packages/plugin-utils-pane/src/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
86
packages/plugin-utils-pane/src/index.tsx
Normal file
86
packages/plugin-utils-pane/src/index.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
191
packages/plugin-utils-pane/src/list.tsx
Normal file
191
packages/plugin-utils-pane/src/list.tsx
Normal 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">
|
||||
"{item.content?.package}
|
||||
{item.content?.main ? `/${item.content?.main}` : ''}"
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
3
packages/plugin-utils-pane/src/locale/en-US.json
Normal file
3
packages/plugin-utils-pane/src/locale/en-US.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"UtilsPane": "Utils Pane"
|
||||
}
|
||||
10
packages/plugin-utils-pane/src/locale/index.ts
Normal file
10
packages/plugin-utils-pane/src/locale/index.ts
Normal 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 };
|
||||
3
packages/plugin-utils-pane/src/locale/zh-CN.json
Normal file
3
packages/plugin-utils-pane/src/locale/zh-CN.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"UtilsPane": "工具类扩展面板"
|
||||
}
|
||||
308
packages/plugin-utils-pane/src/pane.tsx
Normal file
308
packages/plugin-utils-pane/src/pane.tsx
Normal 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);
|
||||
}
|
||||
31
packages/plugin-utils-pane/src/utils-defaults.tsx
Normal file
31
packages/plugin-utils-pane/src/utils-defaults.tsx
Normal 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);
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
];
|
||||
247
packages/plugin-utils-pane/src/utils-form.tsx
Normal file
247
packages/plugin-utils-pane/src/utils-form.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
16
packages/plugin-utils-pane/src/utils-types.tsx
Normal file
16
packages/plugin-utils-pane/src/utils-types.tsx
Normal 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: '自定义函数',
|
||||
},
|
||||
];
|
||||
10
packages/plugin-utils-pane/tsconfig.json
Normal file
10
packages/plugin-utils-pane/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"./src/"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user