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" }