From 29ad679e87fcfc1aa7c4a1248eb3e20df6024d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=AF=85?= Date: Thu, 24 Sep 2020 01:04:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20utils=20?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/demo/src/editor/components.ts | 4 +- packages/demo/src/editor/config.ts | 10 + packages/plugin-utils-pane/CHANGELOG.md | 1 + packages/plugin-utils-pane/README.md | 3 + packages/plugin-utils-pane/build.json | 9 + packages/plugin-utils-pane/package.json | 43 +++++ packages/plugin-utils-pane/src/index.scss | 12 ++ packages/plugin-utils-pane/src/index.tsx | 213 ++++++++++++++++++++++ packages/plugin-utils-pane/src/utils.ts | 0 packages/plugin-utils-pane/tsconfig.json | 10 + 10 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-utils-pane/CHANGELOG.md create mode 100644 packages/plugin-utils-pane/README.md create mode 100644 packages/plugin-utils-pane/build.json create mode 100644 packages/plugin-utils-pane/package.json create mode 100644 packages/plugin-utils-pane/src/index.scss create mode 100644 packages/plugin-utils-pane/src/index.tsx create mode 100644 packages/plugin-utils-pane/src/utils.ts create mode 100644 packages/plugin-utils-pane/tsconfig.json diff --git a/packages/demo/src/editor/components.ts b/packages/demo/src/editor/components.ts index caa52a36f..9ef78211d 100644 --- a/packages/demo/src/editor/components.ts +++ b/packages/demo/src/editor/components.ts @@ -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, diff --git a/packages/demo/src/editor/config.ts b/packages/demo/src/editor/config.ts index bda055b0b..288472b21 100644 --- a/packages/demo/src/editor/config.ts +++ b/packages/demo/src/editor/config.ts @@ -83,6 +83,16 @@ export default { }, pluginProps: {}, }, + { + pluginKey: 'utilsPane', + type: 'PanelIcon', + props: { + align: 'top', + icon: 'util', + description: '工具类', + }, + pluginProps: {}, + }, { pluginKey: 'sourceEditor', type: 'PanelIcon', diff --git a/packages/plugin-utils-pane/CHANGELOG.md b/packages/plugin-utils-pane/CHANGELOG.md new file mode 100644 index 000000000..420e6f23d --- /dev/null +++ b/packages/plugin-utils-pane/CHANGELOG.md @@ -0,0 +1 @@ +# Change Log diff --git a/packages/plugin-utils-pane/README.md b/packages/plugin-utils-pane/README.md new file mode 100644 index 000000000..fcb19d90f --- /dev/null +++ b/packages/plugin-utils-pane/README.md @@ -0,0 +1,3 @@ +## 关于 @ali/lowcode-plugin-utils-pane + +这是低代码引擎的 utils 面板。 diff --git a/packages/plugin-utils-pane/build.json b/packages/plugin-utils-pane/build.json new file mode 100644 index 000000000..77627cdf9 --- /dev/null +++ b/packages/plugin-utils-pane/build.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + "build-plugin-component", + "build-plugin-fusion", + ["build-plugin-moment-locales", { + "locales": ["zh-cn"] + }] + ] +} \ No newline at end of file diff --git a/packages/plugin-utils-pane/package.json b/packages/plugin-utils-pane/package.json new file mode 100644 index 000000000..d5b55841e --- /dev/null +++ b/packages/plugin-utils-pane/package.json @@ -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" + } +} diff --git a/packages/plugin-utils-pane/src/index.scss b/packages/plugin-utils-pane/src/index.scss new file mode 100644 index 000000000..7b433fcea --- /dev/null +++ b/packages/plugin-utils-pane/src/index.scss @@ -0,0 +1,12 @@ +.lowcode-plugin-utils-pane { + position: relative; + font-size: 100%; + width: 100%; + transform: scale(1); // 这是为了让下面的弹层能正确地计算宽度 + + .lc-popup-placeholder { + position: fixed; + width: 100%; + transform: translateY(-100px); + } +} diff --git a/packages/plugin-utils-pane/src/index.tsx b/packages/plugin-utils-pane/src/index.tsx new file mode 100644 index 000000000..a04bc6401 --- /dev/null +++ b/packages/plugin-utils-pane/src/index.tsx @@ -0,0 +1,213 @@ +import './index.scss'; + +import React, { isValidElement, PureComponent } from 'react'; + +import { Designer, SettingEntry, SettingField, SettingPropEntry } from '@ali/lowcode-designer'; +import { Editor, getSetter } from '@ali/lowcode-editor-core'; +import { PopupService } from '@ali/lowcode-editor-skeleton'; + +import type { PluginProps, SetterType, FieldConfig, UtilsMap } from '@ali/lowcode-types'; + +// 插件自定义props +export interface UtilsPaneProps {} + +// 插件自定义state +interface State { + utils: UtilsMap; +} + +export class UtilsPane extends PureComponent { + static displayName = 'UtilsPane'; + + // 插件初始化处理函数 + static init = function (editor: Editor): void {}; + + state: State = { + utils: this.props.editor.get('designer')?.project?.get('utils') || [], + }; + + private _itemSetter: SetterType = { + componentName: 'ObjectSetter', + props: { + config: { + items: [ + { + name: 'name', + title: '名称', + setter: { + componentName: 'StringSetter', + }, + }, + { + name: 'type', + title: '类型', + initialValue: 'npm', + setter: { + componentName: 'RadioGroupSetter', + props: { + dataSource: [ + { label: 'NPM 包', value: 'npm' }, + // { label: '自定义函数', value: 'function' }, + ], + }, + }, + }, + { + name: 'content', + title: '内容', + setter: { + componentName: 'ObjectSetter', + props: { + config: { + items: [ + { + name: 'componentName', + title: '组件名称', + setter: { + componentName: 'StringSetter', + }, + }, + { + name: 'package', + title: '包名', + setter: { + componentName: 'StringSetter', + }, + }, + { + name: 'version', + title: '版本号', + setter: { + componentName: 'StringSetter', + }, + }, + { + name: 'destructuring', + title: '解构', + setter: { + componentName: 'BoolSetter', + }, + }, + { + name: 'exportName', + title: '导出名', + setter: { + componentName: 'StringSetter', + }, + }, + { + name: 'subName', + title: '子导出名', + setter: { + componentName: 'StringSetter', + }, + }, + { + name: 'main', + title: '主入口', + setter: { + componentName: 'StringSetter', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }; + + private _field = new SettingField(this._getSettingEntry(), this._getFieldConfig()); + private _cleanups: Array<() => void> = []; + + // 打开或激活插件前的切片处理函数 + open(): void | boolean | Promise {} + + // 关闭或挂起插件前的切片处理函数 + close(): void | boolean | Promise {} + + componentDidMount() { + this._cleanups.push( + this._field.onValueChange(() => { + this.setState({ + utils: this._field.getHotValue(), + }); + }), + ); + } + + componentWillUnmount() { + this._cleanups.forEach((clean) => { + clean(); + }); + } + + render(): React.ReactNode { + const ArraySetter = this._getArraySetter(); + + return ( +
+

I am a lowcode engine demo

+ + + +
+ ); + } + + private _handleValueChange = (value: UtilsMap) => { + this.setState({ utils: value }); + this._updateProjectUtils(value); + }; + + private _getSettingEntry(): SettingEntry { + const editor = this.props.editor; + const designer = editor.get('designer') as Designer; + const document = designer.currentDocument!; + const rootNode = document.rootNode!; + + // TODO: remove debug code : + Object.assign(window, { editor, designer, utilsPane: this }); + + return new SettingPropEntry(rootNode.settingEntry, '__internal', 'field'); + } + + private _getFieldConfig(): FieldConfig { + return { + name: '__utils', + setter: this._itemSetter, + }; + } + + private _getArraySetter(): React.ComponentType { + const arraySetter = getSetter('ArraySetter'); + if (!arraySetter) { + return () => Error: ArraySetter is missing!; + } + + const { component: ArraySetter } = arraySetter; + if (isValidElement(ArraySetter)) { + return (props: unknown) => React.cloneElement(ArraySetter); + } + + return ArraySetter; + } + + private get _designer(): Designer { + return this.props.editor.get('designer')!; + } + + private _updateProjectUtils(utils: UtilsMap) { + this._designer.project.set('utils', utils); + } +} + +export default UtilsPane; diff --git a/packages/plugin-utils-pane/src/utils.ts b/packages/plugin-utils-pane/src/utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-utils-pane/tsconfig.json b/packages/plugin-utils-pane/tsconfig.json new file mode 100644 index 000000000..4a965ec62 --- /dev/null +++ b/packages/plugin-utils-pane/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "./src/" + ] +} + From 425c24d03b30ffdacbbdbfb94c12ba66c51bc72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=AF=85?= Date: Wed, 2 Dec 2020 03:48:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20utils=20?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E7=9A=84=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/demo/src/editor/config.ts | 14 +- packages/designer/src/project/project.ts | 12 +- .../src/form-components/index.ts | 1 + .../src/form-components/js-function.tsx | 55 ++++ packages/plugin-utils-pane/src/index.scss | 122 ++++++- packages/plugin-utils-pane/src/index.tsx | 249 ++++---------- packages/plugin-utils-pane/src/list.tsx | 191 +++++++++++ .../plugin-utils-pane/src/locale/en-US.json | 3 + .../plugin-utils-pane/src/locale/index.ts | 10 + .../plugin-utils-pane/src/locale/zh-CN.json | 3 + packages/plugin-utils-pane/src/pane.tsx | 308 ++++++++++++++++++ .../plugin-utils-pane/src/utils-defaults.tsx | 31 ++ packages/plugin-utils-pane/src/utils-form.tsx | 247 ++++++++++++++ .../plugin-utils-pane/src/utils-types.tsx | 16 + packages/plugin-utils-pane/src/utils.ts | 0 packages/react-renderer/package.json | 2 +- 16 files changed, 1061 insertions(+), 203 deletions(-) create mode 100644 packages/plugin-utils-pane/src/form-components/index.ts create mode 100644 packages/plugin-utils-pane/src/form-components/js-function.tsx create mode 100644 packages/plugin-utils-pane/src/list.tsx create mode 100644 packages/plugin-utils-pane/src/locale/en-US.json create mode 100644 packages/plugin-utils-pane/src/locale/index.ts create mode 100644 packages/plugin-utils-pane/src/locale/zh-CN.json create mode 100644 packages/plugin-utils-pane/src/pane.tsx create mode 100644 packages/plugin-utils-pane/src/utils-defaults.tsx create mode 100644 packages/plugin-utils-pane/src/utils-form.tsx create mode 100644 packages/plugin-utils-pane/src/utils-types.tsx delete mode 100644 packages/plugin-utils-pane/src/utils.ts diff --git a/packages/demo/src/editor/config.ts b/packages/demo/src/editor/config.ts index 288472b21..3d00233c0 100644 --- a/packages/demo/src/editor/config.ts +++ b/packages/demo/src/editor/config.ts @@ -90,6 +90,16 @@ export default { align: 'top', icon: 'util', description: '工具类', + panelProps: { + floatable: true, + height: 300, + help: undefined, + hideTitleBar: false, + maxHeight: 800, + maxWidth: 1200, + title: '工具类扩展面板', + width: 430, + }, }, pluginProps: {}, }, @@ -193,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'); diff --git a/packages/designer/src/project/project.ts b/packages/designer/src/project/project.ts index f15227f04..619854399 100644 --- a/packages/designer/src/project/project.ts +++ b/packages/designer/src/project/project.ts @@ -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(); } diff --git a/packages/plugin-utils-pane/src/form-components/index.ts b/packages/plugin-utils-pane/src/form-components/index.ts new file mode 100644 index 000000000..6c3302dab --- /dev/null +++ b/packages/plugin-utils-pane/src/form-components/index.ts @@ -0,0 +1 @@ +export * from './js-function'; diff --git a/packages/plugin-utils-pane/src/form-components/js-function.tsx b/packages/plugin-utils-pane/src/form-components/js-function.tsx new file mode 100644 index 000000000..51651020b --- /dev/null +++ b/packages/plugin-utils-pane/src/form-components/js-function.tsx @@ -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 extends (arg0: infer U) => any ? U : never; +type MonacoRef = Arg0TypeOf; + +class InternalJSFunction extends PureComponent { + 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 ( + + ); + } +} + +export const JSFunction = connect()(InternalJSFunction); diff --git a/packages/plugin-utils-pane/src/index.scss b/packages/plugin-utils-pane/src/index.scss index 7b433fcea..13e59fbeb 100644 --- a/packages/plugin-utils-pane/src/index.scss +++ b/packages/plugin-utils-pane/src/index.scss @@ -1,12 +1,116 @@ .lowcode-plugin-utils-pane { - position: relative; - font-size: 100%; - width: 100%; - transform: scale(1); // 这是为了让下面的弹层能正确地计算宽度 - - .lc-popup-placeholder { - position: fixed; - width: 100%; - transform: translateY(-100px); + 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; } } diff --git a/packages/plugin-utils-pane/src/index.tsx b/packages/plugin-utils-pane/src/index.tsx index a04bc6401..c6bcb2903 100644 --- a/packages/plugin-utils-pane/src/index.tsx +++ b/packages/plugin-utils-pane/src/index.tsx @@ -1,213 +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'; -import React, { isValidElement, PureComponent } from 'react'; +const PLUGIN_NAME = 'utilsPane'; -import { Designer, SettingEntry, SettingField, SettingPropEntry } from '@ali/lowcode-designer'; -import { Editor, getSetter } from '@ali/lowcode-editor-core'; -import { PopupService } from '@ali/lowcode-editor-skeleton'; +export interface UtilsPaneProps extends PluginProps { + /** + * 支持的 Util 的类型 + */ + utilsTypes: UtilTypeInfo[]; -import type { PluginProps, SetterType, FieldConfig, UtilsMap } from '@ali/lowcode-types'; - -// 插件自定义props -export interface UtilsPaneProps {} - -// 插件自定义state -interface State { - utils: UtilsMap; + /** + * 初始的 Utils (若 schema 中尚未定义 utils) + */ + initialUtils?: UtilItem[]; } -export class UtilsPane extends PureComponent { - static displayName = 'UtilsPane'; +interface State { + active: boolean; +} - // 插件初始化处理函数 - static init = function (editor: Editor): void {}; +export default class UtilsPanePlugin extends PureComponent { + static displayName = 'UtilsPanePlugin'; - state: State = { - utils: this.props.editor.get('designer')?.project?.get('utils') || [], + static defaultProps = { + initialUtils: DEFAULT_UTILS, }; - private _itemSetter: SetterType = { - componentName: 'ObjectSetter', - props: { - config: { - items: [ - { - name: 'name', - title: '名称', - setter: { - componentName: 'StringSetter', - }, - }, - { - name: 'type', - title: '类型', - initialValue: 'npm', - setter: { - componentName: 'RadioGroupSetter', - props: { - dataSource: [ - { label: 'NPM 包', value: 'npm' }, - // { label: '自定义函数', value: 'function' }, - ], - }, - }, - }, - { - name: 'content', - title: '内容', - setter: { - componentName: 'ObjectSetter', - props: { - config: { - items: [ - { - name: 'componentName', - title: '组件名称', - setter: { - componentName: 'StringSetter', - }, - }, - { - name: 'package', - title: '包名', - setter: { - componentName: 'StringSetter', - }, - }, - { - name: 'version', - title: '版本号', - setter: { - componentName: 'StringSetter', - }, - }, - { - name: 'destructuring', - title: '解构', - setter: { - componentName: 'BoolSetter', - }, - }, - { - name: 'exportName', - title: '导出名', - setter: { - componentName: 'StringSetter', - }, - }, - { - name: 'subName', - title: '子导出名', - setter: { - componentName: 'StringSetter', - }, - }, - { - name: 'main', - title: '主入口', - setter: { - componentName: 'StringSetter', - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, + state = { + active: false, }; - private _field = new SettingField(this._getSettingEntry(), this._getFieldConfig()); - private _cleanups: Array<() => void> = []; + constructor(props: UtilsPaneProps) { + super(props); + this.state.active = true; - // 打开或激活插件前的切片处理函数 - open(): void | boolean | Promise {} + const { editor } = this.props; - // 关闭或挂起插件前的切片处理函数 - close(): void | boolean | Promise {} + // @todo pluginName, to unsubscribe + // 第一次 active 事件不会触发监听器 + editor.on('skeleton.panel-dock.active', (pluginName) => { + if (pluginName === PLUGIN_NAME) { + this.setState({ active: true }); + } + }); - componentDidMount() { - this._cleanups.push( - this._field.onValueChange(() => { - this.setState({ - utils: this._field.getHotValue(), - }); - }), - ); - } - - componentWillUnmount() { - this._cleanups.forEach((clean) => { - clean(); + editor.on('skeleton.panel-dock.unactive', (pluginName) => { + if (pluginName === PLUGIN_NAME) { + this.setState({ active: false }); + } }); } - render(): React.ReactNode { - const ArraySetter = this._getArraySetter(); + 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 ( -
-

I am a lowcode engine demo

- - - -
+ ); } - private _handleValueChange = (value: UtilsMap) => { - this.setState({ utils: value }); - this._updateProjectUtils(value); + private handleSchemaChange = (utilsMap: UtilsMap) => { + const { editor } = this.props; + + // @TODO 姿势是否最优? + if (editor.get('designer')) { + editor.get('designer').project.set('utils', utilsMap); + } }; - - private _getSettingEntry(): SettingEntry { - const editor = this.props.editor; - const designer = editor.get('designer') as Designer; - const document = designer.currentDocument!; - const rootNode = document.rootNode!; - - // TODO: remove debug code : - Object.assign(window, { editor, designer, utilsPane: this }); - - return new SettingPropEntry(rootNode.settingEntry, '__internal', 'field'); - } - - private _getFieldConfig(): FieldConfig { - return { - name: '__utils', - setter: this._itemSetter, - }; - } - - private _getArraySetter(): React.ComponentType { - const arraySetter = getSetter('ArraySetter'); - if (!arraySetter) { - return () => Error: ArraySetter is missing!; - } - - const { component: ArraySetter } = arraySetter; - if (isValidElement(ArraySetter)) { - return (props: unknown) => React.cloneElement(ArraySetter); - } - - return ArraySetter; - } - - private get _designer(): Designer { - return this.props.editor.get('designer')!; - } - - private _updateProjectUtils(utils: UtilsMap) { - this._designer.project.set('utils', utils); - } } - -export default UtilsPane; diff --git a/packages/plugin-utils-pane/src/list.tsx b/packages/plugin-utils-pane/src/list.tsx new file mode 100644 index 000000000..64dbc1650 --- /dev/null +++ b/packages/plugin-utils-pane/src/list.tsx @@ -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 { + 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) => ( +
  • +
    +
    +
    + {item.name} + + {(item.type === 'npm' || item.type === 'tnpm') && ( + + {' '} + 源自{' '} + + "{item.content?.package} + {item.content?.main ? `/${item.content?.main}` : ''}" + + + {item.content?.exportName && item.content?.exportName !== item.name + ? `中的 ${item.content?.exportName}${ + item.content?.subName ? `.${item.content?.subName}` : '' + }` + : ''} + + + )} +
    + {!!utilTypes.some((t) => t.type === item.type) && + this.renderItemDetailBalloon(item)} + {!!utilTypes.some((t) => t.type === item.type) && ( + + )} + {!!utilTypes.some((t) => t.type === item.type) && ( + + )} + +
    +
    + {item.type} + {(item.type === 'npm' || item.type === 'tnpm') && item.content?.destructuring && ( + 解构 + )} +
    +
    +
  • + )) || [] + ); + }; + + private renderItemDetailBalloon(item: UtilItem): React.ReactNode { + return ( + 详情} + align="b" + alignEdge + triggerType="hover" + style={{ width: 300 }} + > + {item.type === 'function' ? ( +
    +
    +              {item.content?.value || ''}
    +            
    +
    + ) : ( + (([key, value]) => ({ + label: key, + value: `${value}`, + })), + console.log, + )} + > + +
    {typeof val === 'string' ? `"${val}"` : `${val}`}
    } + /> +
    + )} +
    + ); + } + + render() { + const { utilTypes } = this.props; + const { filteredType } = this.state; + + return ( +
    + ({ + label: utilType.label, + value: utilType.type, + })), + ]} + onFilterChange={this.handleSearchFilterChange} + /> +
    + {this.renderVirtualUtilsList()} +
    +
    + ); + } +} diff --git a/packages/plugin-utils-pane/src/locale/en-US.json b/packages/plugin-utils-pane/src/locale/en-US.json new file mode 100644 index 000000000..24c351193 --- /dev/null +++ b/packages/plugin-utils-pane/src/locale/en-US.json @@ -0,0 +1,3 @@ +{ + "UtilsPane": "Utils Pane" +} diff --git a/packages/plugin-utils-pane/src/locale/index.ts b/packages/plugin-utils-pane/src/locale/index.ts new file mode 100644 index 000000000..49de985ed --- /dev/null +++ b/packages/plugin-utils-pane/src/locale/index.ts @@ -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 }; diff --git a/packages/plugin-utils-pane/src/locale/zh-CN.json b/packages/plugin-utils-pane/src/locale/zh-CN.json new file mode 100644 index 000000000..89fb4feb8 --- /dev/null +++ b/packages/plugin-utils-pane/src/locale/zh-CN.json @@ -0,0 +1,3 @@ +{ + "UtilsPane": "工具类扩展面板" +} diff --git a/packages/plugin-utils-pane/src/pane.tsx b/packages/plugin-utils-pane/src/pane.tsx new file mode 100644 index 000000000..2d02f0fef --- /dev/null +++ b/packages/plugin-utils-pane/src/pane.tsx @@ -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; +} + +interface State { + utilItems: UtilsMap; + tabItems: TabItem[]; + activeTabKey: PaneTabKey; +} + +export class UtilsPane extends PureComponent { + 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) => { + 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 [ + + {utilTypes.map((type) => ( + {type.label} + ))} + , + ]; + } else if (utilTypes.length === 1) { + return [ + , + ]; + } else { + return []; + } + } + + return []; + }; + + // 更通用的处理 + private renderTabItemContent = ( + tabItemKey: PaneTabKey, + data: Partial | undefined | null, + ) => { + const { utilItems } = this.state; + const { utilTypes = [] } = this.props; + + if (tabItemKey === PaneTabKey.List) { + if (utilItems.length <= 0) { + return ( + + 您可以点击右上角的【添加】按钮来添加一个。 + + ); + } + + return ( + + ); + } else if (tabItemKey === PaneTabKey.Edit) { + return ( + + ); + } else if (tabItemKey === PaneTabKey.Create) { + return ( + + ); + } else { + console.warn('Unknown tab type: ', tabItemKey); + return null; + } + }; + + render() { + const { activeTabKey, tabItems } = this.state; + + return ( +
    + + {tabItems.map((item: TabItem) => ( + {this.renderTabItemContent(item.key, item.data)} + ))} + +
    + ); + } +} + +function isValidTabKey(tabKey: unknown): tabKey is PaneTabKey { + return typeof tabKey === 'string' && (Object.values(PaneTabKey) as string[]).includes(tabKey); +} diff --git a/packages/plugin-utils-pane/src/utils-defaults.tsx b/packages/plugin-utils-pane/src/utils-defaults.tsx new file mode 100644 index 000000000..729268e9c --- /dev/null +++ b/packages/plugin-utils-pane/src/utils-defaults.tsx @@ -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); +} +`, + }, + }, +]; diff --git a/packages/plugin-utils-pane/src/utils-form.tsx b/packages/plugin-utils-pane/src/utils-form.tsx new file mode 100644 index 000000000..49f8c66b8 --- /dev/null +++ b/packages/plugin-utils-pane/src/utils-form.tsx @@ -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 | null; + onComplete?: (item: UtilItem) => void; + onCancel?: () => void; +} + +/** + * 通过是否存在 ID 来决定读写状态 + */ +export class UtilsForm extends PureComponent { + 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 | 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 ( +
    + + + 保存 + + + +
    + ); + } +} + +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): 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, + }, + }; +} diff --git a/packages/plugin-utils-pane/src/utils-types.tsx b/packages/plugin-utils-pane/src/utils-types.tsx new file mode 100644 index 000000000..00121aba3 --- /dev/null +++ b/packages/plugin-utils-pane/src/utils-types.tsx @@ -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: '自定义函数', + }, +]; diff --git a/packages/plugin-utils-pane/src/utils.ts b/packages/plugin-utils-pane/src/utils.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index 0a344692a..bed80f399 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -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" }