diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c644ba5..281c877 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,13 +2,19 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence -* @leoyuan @alvarto +* @liujuping @JackLian @alvarto /packages/plugin-manual/ @alvarto -/packages/base-monaco-editor/ @alvarto @wangshihao111 -/packages/plugin-code-editor/ @alvarto +/packages/base-monaco-editor/ @alvarto @wangshihao111 @SuSunSam +/packages/plugin-code-editor/ @alvarto @SuSunSam /packages/plugin-schema/ @alvarto -/packages/plugin-components-pane/ @mark-ck -/packages/plugin-datasource-pane/ @xingmolu -/packages/plugin-zh-en/ @leoyuan -/packages/plugin-undo-redo/ @leoyuan +/packages/plugin-components-pane/ @mark-ck @love999262 +/packages/plugin-datasource-pane/ @YSMJ1994 +/packages/plugin-zh-en/ @JackLian @liujuping +/packages/plugin-undo-redo/ @JackLian @liujuping +/packages/plugin-resource-tabs/ @JackLian @liujuping +/packages/plugin-set-ref-prop/ @JackLian @liujuping +/packages/plugin-view-manager-pane/ @JackLian @liujuping +/packages/base-monaco-editor/ @hzd822 +/packages/plugin-multiple-editor/ @hzd822 +/packages/action-block @liujuping \ No newline at end of file diff --git a/.github/workflows/deprecate npm.yml b/.github/workflows/deprecate npm.yml new file mode 100644 index 0000000..ea096ec --- /dev/null +++ b/.github/workflows/deprecate npm.yml @@ -0,0 +1,32 @@ +name: deprecate Package Version + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to be deleted package@version' + required: true + +jobs: + delete-package-version: + runs-on: ubuntu-latest + if: >- + github.ref == 'refs/heads/main' && + (github.actor == 'JackLian' || github.actor == 'liujuping') + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: deprecate Package Version + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + npm deprecate ${{ github.event.inputs.version }} "This version is deprecated. Please consider upgrading to a newer version." diff --git a/.github/workflows/publish beta npm.yml b/.github/workflows/publish beta npm.yml index ff4108b..01ab685 100644 --- a/.github/workflows/publish beta npm.yml +++ b/.github/workflows/publish beta npm.yml @@ -28,6 +28,7 @@ jobs: run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" + npm install --legacy-peer-deps echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc cd packages/${{ github.event.inputs.packagePath }} npm install --legacy-peer-deps diff --git a/.github/workflows/publish npm.yml b/.github/workflows/publish npm.yml index 7b686d9..5ada959 100644 --- a/.github/workflows/publish npm.yml +++ b/.github/workflows/publish npm.yml @@ -32,9 +32,10 @@ jobs: git config --local user.name "GitHub Action" echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + npm install --legacy-peer-deps cd packages/${{ github.event.inputs.packagePath }} npm install --legacy-peer-deps - npm version patch + npm version ${{ github.event.inputs.versionType }} npm run build echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc @@ -43,5 +44,8 @@ jobs: echo "PACKAGE_NAME=$(node -p "require('./package.json').name")" >> $GITHUB_ENV echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + git add package.json + git commit -m "Bump version to ${PACKAGE_VERSION}" + git tag -a "${PACKAGE_NAME}@${PACKAGE_VERSION}" -m "Release ${PACKAGE_NAME} version ${PACKAGE_VERSION}" git push origin "${PACKAGE_NAME}@${PACKAGE_VERSION}" \ No newline at end of file diff --git a/packages/plugin-block/src/index.tsx b/packages/plugin-block/src/index.tsx index afd3b29..dfbbb71 100644 --- a/packages/plugin-block/src/index.tsx +++ b/packages/plugin-block/src/index.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { ILowCodePluginContext } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; import { default as BlockPane } from './pane'; -const LowcodePluginCusPlugin = (ctx: ILowCodePluginContext) => { +const LowcodePluginCusPlugin = (ctx: IPublicModelPluginContext) => { return { // 插件名,注册环境下唯一 name: 'LowcodePluginCusPlugin', diff --git a/packages/plugin-code-editor/src/index.tsx b/packages/plugin-code-editor/src/index.tsx index d7caf4c..c40e74e 100644 --- a/packages/plugin-code-editor/src/index.tsx +++ b/packages/plugin-code-editor/src/index.tsx @@ -1,8 +1,9 @@ import { CodeEditorPane } from './pane'; -import { project, ILowCodePluginContext } from '@alilc/lowcode-engine'; +import { project } from '@alilc/lowcode-engine'; import icon from './icon'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; -const plugin = (ctx: ILowCodePluginContext) => { +const plugin = (ctx: IPublicModelPluginContext) => { return { name: 'codeEditor', width: 600, diff --git a/packages/plugin-datasource-pane/src/index.tsx b/packages/plugin-datasource-pane/src/index.tsx index a8098a9..838ada5 100644 --- a/packages/plugin-datasource-pane/src/index.tsx +++ b/packages/plugin-datasource-pane/src/index.tsx @@ -1,10 +1,10 @@ -import { ILowCodePluginContext } from '@alilc/lowcode-engine'; import DataSourcePanePlugin from './pane'; import { DataSourcePaneImportPlugin, DataSourceType, } from './types'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; export interface Options { importPlugins?: DataSourcePaneImportPlugin[]; @@ -13,7 +13,7 @@ export interface Options { } // TODO: 2.0插件传参修改,不支持直接options: Options -const plugin = (ctx: ILowCodePluginContext, options: Options) => { +const plugin = (ctx: IPublicModelPluginContext, options: Options) => { return { name: 'com.alibaba.lowcode.datasource.pane', width: 300, diff --git a/packages/plugin-manual/src/index.tsx b/packages/plugin-manual/src/index.tsx index bb57695..cf85189 100644 --- a/packages/plugin-manual/src/index.tsx +++ b/packages/plugin-manual/src/index.tsx @@ -1,7 +1,7 @@ -import { ILowCodePluginContext } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; import { IconQuestion } from './icon'; -const PluginManual = (ctx: ILowCodePluginContext) => { +const PluginManual = (ctx: IPublicModelPluginContext) => { return { init() { // 往引擎增加面板 diff --git a/packages/plugin-multiple-editor/README.md b/packages/plugin-multiple-editor/README.md index 3bc74da..dab124a 100644 --- a/packages/plugin-multiple-editor/README.md +++ b/packages/plugin-multiple-editor/README.md @@ -6,6 +6,7 @@ ```ts import multipleFileCodeEditorFactory from '@alilc/lowcode-plugin-multiple-editor'; + import { PrettierPlugin } from '@alilc/lowcode-plugin-multiple-editor/es/plugins/prettier-plugin'; @@ -57,6 +58,7 @@ await plugins.register(plugin); 4. 如需在 setter 内适用类型定义,请开启 base-editor 的单例模式,仅需在应用入口处调用如下方法即可: 5. 如果低码项目有使用出码,则需对出码进行定制,将 _sourceCodeMap 中的文件生成到项目中,并对文件的引用进行处理,具体处理方式可自行组织 +__使用单例模式__ ```ts import { configure } from '@alilc/lowcode-plugin-base-monaco-editor'; configure({ singleton: true }); diff --git a/packages/plugin-multiple-editor/package.json b/packages/plugin-multiple-editor/package.json index 289f34c..38e07c7 100644 --- a/packages/plugin-multiple-editor/package.json +++ b/packages/plugin-multiple-editor/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-multiple-editor", - "version": "0.1.0", + "version": "0.2.0", "main": "es/index.js", "license": "MIT", "private": false, @@ -24,7 +24,8 @@ "release": "fast-publish" }, "dependencies": { - "@alilc/lowcode-types": "^1.0.3" + "@alilc/lowcode-types": "^1.0.3", + "@babel/types": "^7.16.0" }, "devDependencies": { "@alifd/next": "^1.25.29", @@ -122,6 +123,9 @@ "webpackbar": "^5.0.2", "workbox-webpack-plugin": "^6.4.1" }, + "resolutions": { + "@types/react": "^17.0.2" + }, "browserslist": { "production": [ ">0.2%", @@ -175,5 +179,6 @@ ], "fastPublish": { "npmClient": "npm" - } + }, + "repository": "https://github.com/alibaba/lowcode-plugins.git" } diff --git a/packages/plugin-multiple-editor/scripts/build.js b/packages/plugin-multiple-editor/scripts/build.js index a15ed89..007f85e 100644 --- a/packages/plugin-multiple-editor/scripts/build.js +++ b/packages/plugin-multiple-editor/scripts/build.js @@ -155,26 +155,27 @@ function build(previousFileSizes) { } return reject(new Error(messages.errors.join('\n\n'))); } - if ( - process.env.CI && - (typeof process.env.CI !== 'string' || - process.env.CI.toLowerCase() !== 'false') && - messages.warnings.length - ) { - // Ignore sourcemap warnings in CI builds. See #8227 for more info. - const filteredWarnings = messages.warnings.filter( - w => !/Failed to parse source map/.test(w) - ); - if (filteredWarnings.length) { - console.log( - chalk.yellow( - '\nTreating warnings as errors because process.env.CI = true.\n' + - 'Most CI servers set it automatically.\n' - ) - ); - return reject(new Error(filteredWarnings.join('\n\n'))); - } - } + // process.env.CI=true; + // if ( + // process.env.CI && + // (typeof process.env.CI !== 'string' || + // process.env.CI.toLowerCase() !== 'false') && + // messages.warnings.length + // ) { + // // Ignore sourcemap warnings in CI builds. See #8227 for more info. + // const filteredWarnings = messages.warnings.filter( + // w => !/Failed to parse source map/.test(w) + // ); + // if (filteredWarnings.length) { + // console.log( + // chalk.yellow( + // '\nTreating warnings as errors because process.env.CI = true.\n' + + // 'Most CI servers set it automatically.\n' + // ) + // ); + // return reject(new Error(filteredWarnings.join('\n\n'))); + // } + // } const resolveArgs = { stats, @@ -192,4 +193,4 @@ function build(previousFileSizes) { return resolve(resolveArgs); }); }); -} \ No newline at end of file +} diff --git a/packages/plugin-multiple-editor/src/Context.tsx b/packages/plugin-multiple-editor/src/Context.tsx index 9346c35..b90b61b 100644 --- a/packages/plugin-multiple-editor/src/Context.tsx +++ b/packages/plugin-multiple-editor/src/Context.tsx @@ -21,7 +21,6 @@ import { sortDir, } from './utils/files'; import { editorController } from './Controller'; -import { getDefaultFileList } from './MultipleFileEditor/util'; export type CurrentFile = { file?: File; @@ -39,11 +38,13 @@ export interface EditorContextType { updateFileTreeByPath( path: string[], target: Dir | File, - operation: 'delete' | 'add' + operation: 'delete' | 'add' | 'rename', + newName?: string ): void; selectFile(file: File | string, path: string[]): void; selectFileByName(name: string): void; updateState(state: Partial): void; + updateCurrentFile(content: string): void; initialFileMap: Record; extraLibs: { path: string; content: string }[]; } @@ -86,7 +87,9 @@ export const EditorProvider: FC<{ () => editorController.getCodeTemp()?._sourceCodeMap.files || {}, [] ); - const initFileTree = fileMap ? fileMapToTree(fileMap) : new Dir('/'); + const initFileTree = fileMap + ? fileMapToTree(fileMap) + : new Dir('/', [], [], ''); const initState: StoreState = { declarations: '', extraLibs: [], @@ -103,7 +106,7 @@ export const EditorProvider: FC<{ if (typeof file === 'string') { const { fileTree } = state; const targetFile = getFileByPath(fileTree, file, path); - finalFile = targetFile || new File('index.js', ''); + finalFile = targetFile || new File('index.js', '', 'index.js'); } else { finalFile = file; } @@ -123,12 +126,14 @@ export const EditorProvider: FC<{ updateFileTree(tree: Dir) { dispatch({ type: 'update', payload: { fileTree: sortDir(tree) } }); }, - updateFileTreeByPath(path, target, operation) { + updateFileTreeByPath(path, target, operation, newName) { const { fileTree } = state; if (operation === 'add') { insertNodeToTree(fileTree, path, target); } else if (operation === 'delete') { deleteNodeOnTree(fileTree, path, target); + } else if (operation === 'rename') { + target.name = newName as string; } const payload: Partial = { fileTree: sortDir(fileTree) }; // 新增文件时,选中当前文件 @@ -147,12 +152,27 @@ export const EditorProvider: FC<{ updateState(state: Partial) { dispatch({ type: 'update', payload: state }); }, + updateCurrentFile(content) { + if (this.currentFile.file) { + this.currentFile.file.content = content; + this.updateState({ + currentFile: { + ...this.currentFile, + file: { + ...this.currentFile.file, + content, + }, + }, + }); + // this.updateFileTree({...this.fileTree}); + } + }, }), - [fileMap, selectFile, softSave, state] + [fileMap, mode, selectFile, softSave, state] ); useEffect(() => { const off = editorController.onSourceCodeChange((codeMap) => { - const fileTree = fileMapToTree(codeMap) || new Dir('/'); + const fileTree = fileMapToTree(codeMap) || new Dir('/', [], [], ''); dispatch({ type: 'update', payload: { fileTree } }); const targetFile = getFileByPath(fileTree, 'index.js', []); setTimeout(() => { diff --git a/packages/plugin-multiple-editor/src/Controller.ts b/packages/plugin-multiple-editor/src/Controller.ts index cce1d56..401838a 100644 --- a/packages/plugin-multiple-editor/src/Controller.ts +++ b/packages/plugin-multiple-editor/src/Controller.ts @@ -1,8 +1,10 @@ import type { Project, Event } from '@alilc/lowcode-shell'; -import { skeleton, common } from '@alilc/lowcode-engine'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; +import { skeleton } from '@alilc/lowcode-engine'; import { beautifyCSS, compatGetSourceCodeMap, + fileMapToTree, getConstructorContent, getDefaultDeclaration, getInitFuncContent, @@ -10,7 +12,8 @@ import { treeToMap, } from './utils'; import { FunctionEventParams, Monaco, ObjectType } from './types'; -import type { editor } from 'monaco-editor'; +import { common } from '@alilc/lowcode-engine'; +import { editor } from 'monaco-editor'; import { addFunction, focusByFunctionName, @@ -21,13 +24,14 @@ import { EditorContextType } from './Context'; import { Message } from '@alifd/next'; import { getMethods } from './utils/get-methods'; import { EditorHook, HookKeys } from './EditorHook'; -import { Service } from './Service'; +import { PluginHooks, Service } from './Service'; +import { MonacoSuggestions } from './MonacoSuggestions'; export * from './EditorHook'; export interface EditorControllerState { declarationsMap: Record; - extraLibs: Array<{ path: string; content: string }>; + extraLibs: { path: string; content: string }[]; } export type HookHandleFn = (fn: T) => () => void; @@ -49,7 +53,9 @@ export class EditorController extends EditorHook { defaultFiles: ObjectType; - monaco?: Monaco; + useLess?: boolean; + + public monaco?: Monaco; private codeTemp?: CodeTemp; @@ -59,23 +65,30 @@ export class EditorController extends EditorHook { private state: EditorControllerState; - codeEditor?: editor.IStandaloneCodeEditor; + public codeEditor?: editor.IStandaloneCodeEditor; + + public codeEditorCtx?: EditorContextType; + + public service!: Service; - private codeEditorCtx?: EditorContextType; + private loadMonacoPromise?: Promise; - service!: Service; + private monacoSuggestions: MonacoSuggestions; - onImportSchema: HookHandleFn<(schema: any) => void | Promise> = - this.hookFactory(HookKeys.onImport); + public onImportSchema: HookHandleFn< + (schema: IPublicTypeProjectSchema) => void | Promise + > = this.hookFactory(HookKeys.onImport); - onSourceCodeChange: HookHandleFn<(code: any) => void> = this.hookFactory( - HookKeys.onSourceCodeChange - ); + public onSourceCodeChange: HookHandleFn<(code: any) => void> = + this.hookFactory(HookKeys.onSourceCodeChange); - onEditCodeChange: HookHandleFn< + public onEditCodeChange: HookHandleFn< (code: { content: string; file: string }) => void > = this.hookFactory(HookKeys.onEditCodeChange); + public onMonacoLoaded: HookHandleFn<(monaco: Monaco) => void> = + this.hookFactory(HookKeys.onMonacoLoaded); + constructor() { super(); this.state = { @@ -85,6 +98,21 @@ export class EditorController extends EditorHook { this.listeners = []; this.defaultFiles = {}; this.extraLibMap = new Map(); + this.monacoSuggestions = new MonacoSuggestions(this); + } + + async initMonaco() { + if (!this.monaco) { + if (!this.loadMonacoPromise) { + const { getMonaco } = await import( + '@alilc/lowcode-plugin-base-monaco-editor' + ); + this.loadMonacoPromise = getMonaco(); + } + this.monaco = await this.loadMonacoPromise; + this.triggerHook(HookKeys.onMonacoLoaded, this.monaco); + this.service.triggerHook(PluginHooks.onMonacoLoaded, this.monaco); + } } init(project: Project, editor: Event, service: Service) { @@ -94,6 +122,7 @@ export class EditorController extends EditorHook { this.setupEventListeners(); this.initCodeTempBySchema(this.getSchema(true)); this.triggerHook(HookKeys.onImport, this.getSchema(true)); + this.initMonaco(); } initCodeEditor( @@ -102,6 +131,7 @@ export class EditorController extends EditorHook { ) { this.codeEditor = codeEditor; this.codeEditorCtx = ctx; + this.monacoSuggestions.init(); } setCodeTemp(code: any | ((old: CodeTemp) => CodeTemp)) { @@ -122,7 +152,7 @@ export class EditorController extends EditorHook { this.applyLibs(); } - addComponentDeclarations(list: Array<[string, string]> = []) { + addComponentDeclarations(list: [string, string][] = []) { for (const [key, dec] of list) { this.state.declarationsMap[key] = dec; } @@ -131,7 +161,7 @@ export class EditorController extends EditorHook { } private publishExtraLib() { - const libs: Array<{ path: string; content: string }> = []; + const libs: { path: string; content: string }[] = []; this.extraLibMap.forEach((content, path) => libs.push({ content, path })); this.state.extraLibs = libs; this.publish(); @@ -151,10 +181,7 @@ export class EditorController extends EditorHook { private async applyLibs() { if (!this.monaco) { - const { getMonaco } = await import( - '@alilc/lowcode-plugin-base-monaco-editor' - ); - this.monaco = await getMonaco(undefined) as any; + await this.initMonaco(); } const decStr = Object.keys(this.state.declarationsMap).reduce( (v, k) => `${v}\n${k}: ${this.state.declarationsMap[k]};\n`, @@ -173,7 +200,7 @@ export class EditorController extends EditorHook { }); } - getSchema(pure?: boolean): any { + getSchema(pure?: boolean): IPublicTypeProjectSchema { const schema = this.project.exportSchema( common.designerCabin.TransformStage.Save ); @@ -182,15 +209,21 @@ export class EditorController extends EditorHook { ? treeToMap(this.codeEditorCtx.fileTree) : this.codeTemp?._sourceCodeMap.files; // 获取最新的fileMap if (fileMap && !pure) { - if (!this.compileSourceCode(fileMap)) { - throw new Error('编译失败'); + try { + if (!this.compileSourceCode(fileMap)) { + // 下面会导致整个页面挂掉,先作为弱依赖,给个提示 + throw new Error('编译失败'); + } + Object.assign(schema.componentsTree[0], this.codeTemp); + } catch (error) { + console.error(error); + Message.error('源码编译失败,请返回修改'); } - Object.assign(schema.componentsTree[0], this.codeTemp); } return schema; } - importSchema(schema: any) { + importSchema(schema: IPublicTypeProjectSchema) { this.project.importSchema(schema); this.initCodeTempBySchema(schema); this.triggerHook(HookKeys.onImport, schema); @@ -217,15 +250,15 @@ export class EditorController extends EditorHook { }; } - initCodeTempBySchema(schema: any) { + public initCodeTempBySchema(schema: IPublicTypeProjectSchema) { const componentSchema = schema.componentsTree[0] || {}; - const { css, methods, state, lifeCycles } = componentSchema; + const { css, methods, state, lifeCycles } = componentSchema as any; const codeMap = (componentSchema as any)._sourceCodeMap; const defaultFileMap = { ...this.defaultFiles, - ...getDefaultFileList(schema), + ...getDefaultFileList(schema, this.useLess), }; - const compatMap = compatGetSourceCodeMap(codeMap); + const compatMap = compatGetSourceCodeMap(codeMap, defaultFileMap); this.codeTemp = { css, methods, @@ -233,7 +266,6 @@ export class EditorController extends EditorHook { lifeCycles, _sourceCodeMap: { ...compatMap, - files: defaultFileMap, }, }; } @@ -253,8 +285,8 @@ export class EditorController extends EditorHook { this.editor?.on('common:codeEditor.addFunction', (( params: FunctionEventParams ) => { + this.codeEditorCtx?.selectFile('index.js', []); setTimeout(() => { - this.codeEditorCtx?.selectFile('index.js', []); if (this.monaco && this.codeEditor) { addFunction(this.codeEditor, params, this.monaco); } @@ -306,7 +338,20 @@ export class EditorController extends EditorHook { pageNode.state = state; pageNode.methods = methods; pageNode.lifeCycles = lifeCycles; - pageNode.css = beautifyCSS(fileMap['index.css'] || '', {}); + const lessContent = fileMap['index.less']; + // 没有less文件,走之前的逻辑 + if (!lessContent) { + pageNode.css = beautifyCSS(fileMap['index.css'] || '', {}); + } + if (this.useLess && lessContent) { + window.less?.render(lessContent, {}, (err: any, output: any) => { + if (err) { + Message.error('less 编译失败'); + console.error(err); + } + pageNode.css = output?.css || ''; + }); + } if (lifeCycles.constructor === {}.constructor) { lifeCycles.constructor = { originalCode: 'function constructor() { }', @@ -341,10 +386,25 @@ export class EditorController extends EditorHook { return true; } - resetSaveStatus() { + public resetSaveStatus() { this.codeEditorCtx?.updateState({ modifiedKeys: [] }); } + // 添加一堆文件 + public addFiles(fileMap: ObjectType) { + if (!Object.keys(fileMap || {}).length || !this.codeEditorCtx?.fileTree) { + return; + } + const subTree = fileMapToTree(fileMap); + const { files, dirs } = subTree; + const newTree = { ...this.codeEditorCtx?.fileTree }; + newTree.files?.push(...files); + newTree.dirs?.push(...dirs); + this.codeEditorCtx.updateState({ + fileTree: newTree, + }); + } + triggerHook(key: HookKeys, ...args: any[]): void { super.triggerHook(key, ...args); } diff --git a/packages/plugin-multiple-editor/src/EditorHook.ts b/packages/plugin-multiple-editor/src/EditorHook.ts index 0433d69..8fcf145 100644 --- a/packages/plugin-multiple-editor/src/EditorHook.ts +++ b/packages/plugin-multiple-editor/src/EditorHook.ts @@ -2,6 +2,7 @@ export enum HookKeys { onImport = 'onImport', onSourceCodeChange = 'onSourceCodeChange', onEditCodeChange = 'onEditCodeChange', + onMonacoLoaded = 'onMonacoLoaded', } export class EditorHook { diff --git a/packages/plugin-multiple-editor/src/MonacoSuggestions.ts b/packages/plugin-multiple-editor/src/MonacoSuggestions.ts new file mode 100644 index 0000000..62a35e5 --- /dev/null +++ b/packages/plugin-multiple-editor/src/MonacoSuggestions.ts @@ -0,0 +1,67 @@ +import { EditorController } from './Controller'; +import { getFilesByPath, pathResolve, resolveFilePath } from './utils'; + +export class MonacoSuggestions { + private editorInitialed = false; + constructor(private controller: EditorController) {} + + get monaco() { + return this.controller.monaco!; + } + + get editor() { + return this.controller.codeEditor!; + } + + init() { + if (!this.editorInitialed) { + this.initPathSuggestion(); + } + } + + private initPathSuggestion() { + this.editorInitialed = true; + const monaco = this.monaco; + const getFileMap = () => + this.controller.getCodeTemp()?._sourceCodeMap.files || {}; + this.monaco?.languages.registerCompletionItemProvider( + ['javascript', 'typescript'], + { + triggerCharacters: ['/'], + provideCompletionItems(model, position) { + const textUntilPosition = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column + 1, + }); + const currentFilePath = resolveFilePath(model.uri.path); + const match = textUntilPosition.match(/('.*')|(".*")/gim); + if (!match) return; + const relativePath = match?.[0]?.replace(/'|"/g, '') || ''; + const filesList = getFilesByPath( + getFileMap(), + pathResolve(currentFilePath, relativePath) + ); + return { + suggestions: filesList.map(({ type, path }) => ({ + label: path, + sortText: type === 'dir' ? '1' : '2', + range: { + startColumn: position.column, + endColumn: position.column, + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + }, + kind: + type === 'file' + ? monaco.languages.CompletionItemKind.File + : monaco.languages.CompletionItemKind.Folder, + insertText: path.replace(/\.\w+$/, ''), + })), + }; + }, + } + ); + } +} diff --git a/packages/plugin-multiple-editor/src/MultipleFileEditor/Editor.tsx b/packages/plugin-multiple-editor/src/MultipleFileEditor/Editor.tsx index ee1b2fc..cb1e374 100644 --- a/packages/plugin-multiple-editor/src/MultipleFileEditor/Editor.tsx +++ b/packages/plugin-multiple-editor/src/MultipleFileEditor/Editor.tsx @@ -1,20 +1,14 @@ import FileTree from '@/components/FileTree'; import MonacoEditor from '@/components/MonacoEditor'; +import Outline from '@/components/Outline'; import { useEditorContext } from '../Context'; import { Dialog, Message } from '@alifd/next'; import cls from 'classnames'; -import React, { - FC, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import './index.less'; import { HandleChangeFn } from '../components/FileTree/TreeNode'; import { languageMap } from '../utils/constants'; -import { getKeyByPath, parseKey, treeToMap } from 'utils/files'; +import { parseKey, treeToMap } from 'utils/files'; import { editorController, HookKeys } from '../Controller'; import { initEditorModel, useUnReactiveFn } from './util'; @@ -22,6 +16,10 @@ import type { editor } from 'monaco-editor'; import { Monaco } from '../types'; import { PluginHooks } from '@/Service'; +// 最大最小宽(带工具栏) +const MINWIDTH = 246; +const MAXWIDTH = 446; + const Editor: FC = () => { const editorContext = useEditorContext(); const { @@ -37,34 +35,30 @@ const Editor: FC = () => { const monacoEditor = useRef(); const monacoRef = useRef(); const [fullscreen, setFullscreen] = useState(false); - + const [fileContent, setFileContent] = useState(file?.content); + const [fileTreeWidth, setFileTreeWidth] = useState('200px'); const containerRef = useRef(); - const filePath = useMemo( - () => [...path, file?.name].join('/'), - [file?.name, path] - ); + const filePath = file?.fullPath || ''; const handleChange = useCallback( (file, path) => { selectFile(file, path); + setFileContent(file?.content); }, [selectFile] ); const handleEditorChange = useCallback( (value: string) => { + setFileContent(value); file && (file.content = value); - const curKey = getKeyByPath(path, file?.name || ''); editorController.triggerHook(HookKeys.onEditCodeChange, { - file: path - .join('/') - .concat('/') - .concat(file?.name || ''), + file: file?.fullPath, content: value, }); - if (!modifiedKeys.find((k) => k === curKey)) { - updateState({ modifiedKeys: [...modifiedKeys, curKey] }); + if (!modifiedKeys.find((k) => k === file?.fullPath)) { + updateState({ modifiedKeys: [...modifiedKeys, file?.fullPath as any] }); } }, - [file, modifiedKeys, path, updateState] + [file, modifiedKeys, updateState] ); const handleCompile = useCallback( @@ -87,7 +81,6 @@ const Editor: FC = () => { }, [fileTree] ); - const { handler: handleSave } = useUnReactiveFn(() => { if (handleCompile(true)) { // 全部保存, 标记清空 @@ -109,6 +102,10 @@ const Editor: FC = () => { monacoRef.current = monaco; initEditorModel(initialFileMap, monaco); editorController.initCodeEditor(codeEditor, editorContext); + editorController.service.triggerHook( + PluginHooks.onEditorMount, + codeEditor + ); }, [editorContext, initialFileMap] ); @@ -120,16 +117,12 @@ const Editor: FC = () => { }, [editorContext]); useEffect(() => { - const filepath = path - .join('/') - .concat('/') - .concat(file?.name || ''); editorController.service.triggerHook(PluginHooks.onSelectFileChange, { - filepath, + filepath: file?.fullPath, content: file?.content, }); editorController.triggerHook(HookKeys.onEditCodeChange, { - file: filepath, + file: file?.fullPath, content: file?.content, }); }, [file, path]); @@ -150,6 +143,27 @@ const Editor: FC = () => { const handleFullScreen = useCallback((enable: boolean) => { setFullscreen(enable); }, []); + const handleMoveDrag = () => { + let first = true; + document.onmousemove = function (e) { + if (first) { + // 只拖动一下视为点击误触 防止偶现点击触发拖动问题 + first = false; + return; + } + const clientX = e.clientX; + const maxWidth = + clientX < MINWIDTH ? MINWIDTH : clientX > MAXWIDTH ? MAXWIDTH : clientX; + setFileTreeWidth(`${fullscreen ? maxWidth : maxWidth - 46}px`); + return false; + }; + + document.onmouseup = function (e) { + document.onmousemove = null; + // document.onmouseup = null; + return false; + }; + }; return (
{ )} ref={containerRef as any} > - handleCompile()} - fullscreen={fullscreen} - onFullscreen={handleFullScreen} +
+ handleCompile()} + fullscreen={fullscreen} + onFullscreen={handleFullScreen} + actions={editorController.service.actionMap} + /> +
+ -
+

{file ? file.name : '无文件'}

{file ? ( => { +export const getDefaultFileList = ( + rootSchema: any, + useLess?: boolean +): ObjectType => { const sourceCodeMap = rootSchema?.componentsTree?.[0]?._sourceCodeMap || {}; const { files } = compatGetSourceCodeMap(sourceCodeMap); // 兼容旧格式 if (files['index.js']) { @@ -28,7 +31,8 @@ export const getDefaultFileList = (rootSchema: any): ObjectType => { {} ) ), - 'index.css': rootSchema?.componentsTree?.[0]?.css || '', + [useLess ? 'index.less' : 'index.css']: + rootSchema?.componentsTree?.[0]?.css || '', ...files, }; @@ -101,9 +105,13 @@ export async function addFunction( if (!monacoEditor || !monaco) { return; } - const count = monacoEditor.getModel()?.getLineCount() ?? 0; - const range = new monaco.Range(count, 1, count, 1); + let count = monacoEditor.getModel()?.getLineCount() ?? 0; + // 找到倒数第一个非空行 + while (!monacoEditor.getModel()?.getLineContent(count)) { + count--; + } + const range = new monaco.Range(count, 1, count, 1); const functionCode = params.template ? params.template : `\n\t${params.functionName}(){\n\t}\n`; diff --git a/packages/plugin-multiple-editor/src/Service.ts b/packages/plugin-multiple-editor/src/Service.ts index 2ceb81c..79dd16c 100644 --- a/packages/plugin-multiple-editor/src/Service.ts +++ b/packages/plugin-multiple-editor/src/Service.ts @@ -1,11 +1,15 @@ -import { EditorController } from './Controller'; +import { ReactElement } from 'react'; +import { EditorController, HookHandleFn } from './Controller'; import { EditorHook } from './EditorHook'; -import type { Skeleton } from '@alilc/lowcode-shell'; +import type {IPublicApiSkeleton} from '@alilc/lowcode-types'; +import { Monaco } from './types'; export enum PluginHooks { onActive = 'onActive', onDeActive = 'onDeActive', onSelectFileChange = 'onSelectFileChange', + onEditorMount = 'onEditorMount', + onMonacoLoaded = 'onMonacoLoaded', } export interface EditorPluginInterface { @@ -16,6 +20,14 @@ export interface ServiceInitOptions { plugins?: EditorPluginInterface[]; } +export interface PluginAction { + key: string; + title: string; + icon: ReactElement; + action: () => any; + priority: number; +} + export class Service extends EditorHook { // private options: ServiceInitOptions; public onActive = this.hookFactory(PluginHooks.onActive); @@ -24,12 +36,19 @@ export class Service extends EditorHook { public onSelectFileChange = this.hookFactory(PluginHooks.onSelectFileChange); - constructor(public controller: EditorController, private skeleton: Skeleton) { + public onEditorMount = this.hookFactory(PluginHooks.onEditorMount); + + public onMonacoLoaded: HookHandleFn<(monaco: Monaco) => void> = + this.hookFactory(PluginHooks.onMonacoLoaded); + + actionMap: Array; + + constructor(public controller: EditorController, private skeleton: IPublicApiSkeleton) { super(); + this.actionMap = []; } init(options: ServiceInitOptions) { - // this.options = options; const { plugins } = options; if (plugins) { for (const plugin of plugins) { @@ -41,12 +60,12 @@ export class Service extends EditorHook { } private setupHooks() { - this.skeleton.onShowPanel((pluginName) => { + this.skeleton.onHidePanel((pluginName) => { if (pluginName === 'codeEditor') { this.triggerHook(PluginHooks.onDeActive); } }); - this.skeleton.onHidePanel((pluginName) => { + this.skeleton.onShowPanel((pluginName) => { if (pluginName === 'codeEditor') { this.triggerHook(PluginHooks.onActive); } @@ -56,4 +75,15 @@ export class Service extends EditorHook { public triggerHook(key: PluginHooks, ...args: any[]): void { super.triggerHook(key, ...args); } + + public registerAction(action: PluginAction) { + const index = this.actionMap.findIndex((item) => item.key === action.key); + if (index > -1) { + console.error( + `Action ${action.key}, 已被注册,此 Action 将覆盖原 Action` + ); + this.actionMap.splice(index, 1); + } + this.actionMap.push(action); + } } diff --git a/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/img/rename.png b/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/img/rename.png new file mode 100644 index 0000000..7a82ae2 Binary files /dev/null and b/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/img/rename.png differ diff --git a/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.less b/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.less index fe1f70f..a93b105 100644 --- a/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.less +++ b/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.less @@ -1,5 +1,8 @@ .ilp-tree-node { width: 100%; + overflow-y: auto; + overflow-x: hidden; + height: calc(100% - 24px); line-height: 24px; font-size: 12px; color: #333; @@ -41,14 +44,14 @@ img { width: 14px; height: 14px; + position: relative; + display: block; } span { margin-left: 4px; } - &-icon { - position: absolute; - right: 8px; - top: calc(50% - 7px); + &-icon-delete { + margin-right: 2px; } } &-title { @@ -85,11 +88,18 @@ height: 6px; border-radius: 50%; } - &-icon { + &-icon-wrap{ + align-items: center; + height: 24px; + padding: 0 8px 0 4px; + background-color: rgba(243, 243, 241); + position: absolute; + right: 0; + transform: all 0.15s; display: none; } - &:hover &-icon { - display: block; + &:hover &-icon-wrap { + display: flex; } } } diff --git a/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.tsx b/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.tsx index 3f19078..8386112 100644 --- a/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.tsx +++ b/packages/plugin-multiple-editor/src/components/FileTree/TreeNode/index.tsx @@ -6,6 +6,7 @@ import dirIcon from './img/file-directory.png'; import fileIcon from './img/file.png'; import menuIcon from './img/menu.png'; import deleteIcon from './img/delete.png'; +import renameIcon from './img/rename.png'; import './index.less'; @@ -15,6 +16,12 @@ export type HandleDeleteFn = (path: string[], target: Dir | File) => void; export type HandleChangeFn = (value: File, path: string[]) => void; +export type HandleRenameFn = ( + type: 'file' | 'dir', + path: string[], + target: Dir | File +) => void; + export interface TreeNodeProps { disableAction?: boolean; dir?: Dir; @@ -25,6 +32,7 @@ export interface TreeNodeProps { onChange?: HandleChangeFn; onAdd?: HandleAddFn; onDelete?: HandleDeleteFn; + onRename?: HandleRenameFn; className?: string; } @@ -37,7 +45,7 @@ const helpPopupProps = { placementOffset: 4, }; -const defaultDir = new Dir('root'); +const defaultDir = new Dir('/', [], [], ''); const TreeNode: FC = ({ dir = defaultDir, @@ -48,6 +56,7 @@ const TreeNode: FC = ({ onChange, onDelete, onAdd, + onRename, modifiedKeys, disableAction, }) => { @@ -75,20 +84,39 @@ const TreeNode: FC = ({ ]; // 根目录不能删除 if (parentKey) { - baseActions.push({ - title: '删除目录', - action: () => { - path.pop(); - onDelete?.(path, dir); + baseActions.push( + { + title: '删除目录', + action: () => { + path.pop(); + onDelete?.(path, dir); + }, + id: 'delete', }, - id: 'delete', - }); + { + title: '修改目录名', + action: () => { + path.pop(); + onRename?.('dir', path, dir); + }, + id: 'rename', + } + ); } return baseActions; - }, [dir, onAdd, onDelete, parentKey]); + }, [dir, onAdd, onDelete, onRename, parentKey]); const handleFileClick = (f: File, key: string) => { onChange?.(f, parseKey(key).path); }; + const handleRename = ( + e: MouseEvent, + file: File, + key: string + ) => { + e.stopPropagation(); + e.preventDefault(); + onRename?.('file', parseKey(key).path, file); + }; const handleDelete = ( e: MouseEvent, file: File, @@ -176,6 +204,7 @@ const TreeNode: FC = ({ parentKey={getKey(parentKey, d.name)} onChange={onChange} onAdd={onAdd} + onRename={onRename} onDelete={onDelete} modifiedKeys={modifiedKeys} /> @@ -188,23 +217,29 @@ const TreeNode: FC = ({ style={{ paddingLeft: 8 * (level + 1) }} className={cls( 'ilp-tree-node-file', - selectedKey === key && 'ilp-tree-node-file-selected', - modifiedKeys?.find((k) => k === key) && + selectedKey === f.fullPath && 'ilp-tree-node-file-selected', + modifiedKeys?.find((k) => k === f.fullPath) && 'ilp-tree-node-file-modified' )} onClick={() => handleFileClick(f, key)} > file {f.name} - {f.ext !== 'css' && - !(f.name === 'index.js' && !parentKey) && - !actionDisabled && ( - handleDelete(e, f, key)} - /> + {!(f.name === 'index.js' && !parentKey) && !actionDisabled && ( +
+ handleDelete(e, f, key)} + /> + handleRename(e, f, key)} + /> +
)}
); diff --git a/packages/plugin-multiple-editor/src/components/FileTree/index.less b/packages/plugin-multiple-editor/src/components/FileTree/index.less index 65d9825..e4511a8 100644 --- a/packages/plugin-multiple-editor/src/components/FileTree/index.less +++ b/packages/plugin-multiple-editor/src/components/FileTree/index.less @@ -1,13 +1,21 @@ .ilp-file-bar { - width: 200px; + width: 100%; padding: 8px; &-title { font-size: 14px; color: #333; margin: 0; + height: 24px; display: flex; justify-content: space-between; align-items: center; + .ilp-tree-action-item { + display: inline-block; + margin-left: 8px; + img { + margin-left: 0; + } + } img { width: 16px; height: 16px; diff --git a/packages/plugin-multiple-editor/src/components/FileTree/index.tsx b/packages/plugin-multiple-editor/src/components/FileTree/index.tsx index b3c51bd..979c8e7 100644 --- a/packages/plugin-multiple-editor/src/components/FileTree/index.tsx +++ b/packages/plugin-multiple-editor/src/components/FileTree/index.tsx @@ -1,18 +1,19 @@ import React, { CSSProperties, FC, useCallback, useRef, useState } from 'react'; import { Form, Input, Dialog, Message } from '@alifd/next'; import cls from 'classnames'; -import { Dir, File, getFileOrDirTarget, getKeyByPath } from '../../utils/files'; +import { Dir, File, getFileOrDirTarget } from '../../utils/files'; import TreeNode, { HandleAddFn, HandleChangeFn, HandleDeleteFn, + HandleRenameFn, } from './TreeNode'; import './index.less'; import { useEditorContext } from '../../Context'; -// import saveIcon from './img/save.svg'; import fullscreenIcon from './img/fullscreen.svg'; import fullscreenExitIcon from './img/fullscreen-exit.svg'; import compileIcon from './img/compile.svg'; +import { PluginAction } from '@/Service'; export interface FileTreeProps { dir?: Dir; @@ -22,9 +23,10 @@ export interface FileTreeProps { onSave?: () => any; onFullscreen?: (enable: boolean) => void; fullscreen?: boolean; + actions?: PluginAction[]; } -const defaultDir = new Dir('/'); +const defaultDir = new Dir('/', [], [], ''); function validate( data: { type: string; path: any }, @@ -46,8 +48,13 @@ function validate( return '文件或文件夹已存在'; } } + if (data.type === 'file' && name.endsWith('.less') && name !== 'index.less') { + return 'less 文件仅支持创建 index.less'; + } if (data.type === 'file') { - return name && /\.(js)$/.test(name) ? undefined : '文件名必填且未js后缀'; + return name && /\.(js|less)$/.test(name) + ? undefined + : '文件名必填且未js后缀'; } } @@ -63,14 +70,33 @@ const FileTree: FC = ({ onFullscreen, fullscreen, mode, + actions, }) => { const { updateFileTreeByPath, fileTree, modifiedKeys, currentFile } = useEditorContext(); const [visible, setVisible] = useState(false); const [value, setValue] = useState({ name: '' }); - const tmp = useRef<{ path: string[]; type: string }>({} as any).current; + const tmp = useRef<{ + path: string[]; + type: string; + fullPath: string; + operation?: string; + target?: any; + }>({} as any).current; const handleAdd = useCallback( (type, path) => { + tmp.operation = 'add'; + tmp.path = path; + tmp.type = type; + setValue({ name: '' }); + setVisible(true); + }, + [tmp] + ); + const handleRename = useCallback( + (type, path, target) => { + tmp.target = target; + tmp.operation = 'rename'; tmp.path = path; tmp.type = type; setValue({ name: '' }); @@ -78,7 +104,6 @@ const FileTree: FC = ({ }, [tmp] ); - const handleClose = useCallback(() => { setVisible(false); }, []); @@ -87,7 +112,7 @@ const FileTree: FC = ({ setValue({ name: v }); }, []); - const handleAddFileToTree = useCallback( + const handleEditFileToTree = useCallback( async (e?: React.KeyboardEvent) => { if (e && !(e.key === 'Enter' || e?.keyCode === 13)) { return; @@ -95,8 +120,16 @@ const FileTree: FC = ({ const { name } = value; const validMsg = validate(tmp, name, fileTree); if (!validMsg) { - const target = tmp.type === 'file' ? new File(name, '') : new Dir(name); - updateFileTreeByPath(tmp.path, target, 'add'); + const fullPath = `${tmp.path}/${name}`; + if (tmp.operation === 'rename') { + updateFileTreeByPath(tmp.path, tmp.target, 'rename', name); + } else { + const target = + tmp.type === 'file' + ? new File(name, '', fullPath) + : new Dir(name, [], [], fullPath); + updateFileTreeByPath(tmp.path, target, 'add'); + } setVisible(false); } else { Message.error(validMsg); @@ -116,6 +149,10 @@ const FileTree: FC = ({ }, [updateFileTreeByPath] ); + let title = tmp.type === 'file' ? '新建文件' : '新建文件夹'; + if (tmp.operation === 'rename') { + title = tmp.type === 'file' ? '重命名文件' : '重命名文件夹'; + } return (

@@ -133,6 +170,16 @@ const FileTree: FC = ({ title="编译代码" onClick={onSave} /> + {actions?.map((item) => ( + + {item.icon} + + ))}

@@ -143,20 +190,18 @@ const FileTree: FC = ({ onChange={onChange} onAdd={handleAdd} onDelete={handleDelete} + onRename={handleRename} modifiedKeys={modifiedKeys} - selectedKey={getKeyByPath( - currentFile.path, - currentFile.file?.name || '' - )} + selectedKey={currentFile.file?.fullPath} /> handleAddFileToTree()} + onOk={() => handleEditFileToTree()} >
= ({ autoFocus value={value.name} onChange={handleChange} - onKeyDown={(e) => handleAddFileToTree(e)} + onKeyDown={(e) => handleEditFileToTree(e)} />
diff --git a/packages/plugin-multiple-editor/src/components/MonacoEditor/index.tsx b/packages/plugin-multiple-editor/src/components/MonacoEditor/index.tsx index b514143..285079e 100644 --- a/packages/plugin-multiple-editor/src/components/MonacoEditor/index.tsx +++ b/packages/plugin-multiple-editor/src/components/MonacoEditor/index.tsx @@ -3,6 +3,7 @@ import BaseMonacoEditor from '@alilc/lowcode-plugin-base-monaco-editor'; import type { editor } from 'monaco-editor'; import './index.css'; import { Monaco } from '../../types'; +import { editorController } from '@/Controller'; export interface MonacoEditorProps { theme?: string; @@ -33,6 +34,11 @@ class MonacoEditor extends PureComponent { componentWillUnmount() { window.removeEventListener('resize', this.handleResize); + editorController.onImportSchema(() => { + setTimeout(() => { + this.editor?.getModel()?.setValue(this.props.value || ''); + }, 500); + }); } componentDidUpdate(prevProps: MonacoEditorProps) { @@ -53,13 +59,6 @@ class MonacoEditor extends PureComponent { minimap: { enabled: this.props.isFullscreen }, }); } - // base editor 的bug,value 改变编辑器不会更新,暂时这么解决 - if ( - this.props.value !== prevProps.value && - this.props.filePath === prevProps.filePath - ) { - this.editor?.getModel()?.setValue(this.props.value || ''); - } } initMonaco = () => { @@ -76,13 +75,15 @@ class MonacoEditor extends PureComponent { // compiler options monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES2020, - module: monaco.languages.typescript.ModuleKind.ES2015, + module: monaco.languages.typescript.ModuleKind.CommonJS, allowJs: true, allowNonTsExtensions: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, noEmit: true, esModuleInterop: true, jsx: monaco.languages.typescript.JsxEmit.React, reactNamespace: 'React', + typeRoots: ['node_modules/@types'], }); monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true); @@ -90,15 +91,21 @@ class MonacoEditor extends PureComponent { const openEditorBase = editorService.openCodeEditor.bind(editorService); editorService.openCodeEditor = async (input: any, source: any) => { const result = await openEditorBase(input, source); + const { selection } = input.options; if (result === null) { this.props.onGotoFile?.(input.resource?.path); - // console.log('Open definition for:', input); - // console.log( - // 'Corresponding model:', - // monaco.editor.getModel(input.resource) - // ); + // 定位到对应文件位置 + setTimeout(() => { + const position = { + lineNumber: selection.startLineNumber, + column: selection.startColumn, + }; + this.editor?.revealLineInCenter(position.lineNumber); + this.editor?.setPosition(position); + this.editor?.focus(); + }, 50); } - return result; // always return the base result + return result; }; window.addEventListener('resize', this.handleResize); }; diff --git a/packages/plugin-multiple-editor/src/components/Outline/img/class.png b/packages/plugin-multiple-editor/src/components/Outline/img/class.png new file mode 100644 index 0000000..a61249b Binary files /dev/null and b/packages/plugin-multiple-editor/src/components/Outline/img/class.png differ diff --git a/packages/plugin-multiple-editor/src/components/Outline/img/function.png b/packages/plugin-multiple-editor/src/components/Outline/img/function.png new file mode 100644 index 0000000..a898126 Binary files /dev/null and b/packages/plugin-multiple-editor/src/components/Outline/img/function.png differ diff --git a/packages/plugin-multiple-editor/src/components/Outline/img/props.png b/packages/plugin-multiple-editor/src/components/Outline/img/props.png new file mode 100644 index 0000000..97309a4 Binary files /dev/null and b/packages/plugin-multiple-editor/src/components/Outline/img/props.png differ diff --git a/packages/plugin-multiple-editor/src/components/Outline/index.less b/packages/plugin-multiple-editor/src/components/Outline/index.less new file mode 100644 index 0000000..e9cc659 --- /dev/null +++ b/packages/plugin-multiple-editor/src/components/Outline/index.less @@ -0,0 +1,93 @@ + +@leftWidth: 200px; + +.ilp-multiple-editor { + display: flex; + flex-flow: row nowrap; + height: 100%; + width: 100%; + background-color: #fff; + position: relative; + box-sizing: border-box; + padding-left: @leftWidth; + &-outline { + position: absolute; + left: 0; + bottom: 0; + border-right: 1px solid #efeffe; + } +} + +.ilp-outline-node { + width: 100%; + overflow-y: auto; + line-height: 24px; + font-size: 12px; + color: #333; + &-title { + &-content { + display: flex; + justify-content: flex-start; + align-items: center; + flex: 1; + cursor: pointer; + -webkit-user-select: none; + user-select: none; + } + &-prefix { + margin-right: 4px; + transition: all 0.2s; + &-expand { + transform: rotate(90deg); + } + } + } + &-title, + &-file { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + cursor: pointer; + transition: all 0.15s; + position: relative; + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } + img { + width: 14px; + height: 14px; + } + span { + margin-left: 4px; + } + } + &-file { + position: relative; + &-selected { + background-color: rgba(62, 180, 189, 0.15); + } + } + } + .ilp-outline-bar { + width: 200px; + padding: 8px; + height: 100%; + &-title { + font-size: 14px; + height: 24px; + color: #333; + margin: 0; + display: flex; + justify-content: space-between; + align-items: center; + } + } + .ilp-outline-nodeList{ + width: 100%; + overflow-y: auto; + height: calc(100% - 24px); + line-height: 24px; + font-size: 12px; + color: #333; + } \ No newline at end of file diff --git a/packages/plugin-multiple-editor/src/components/Outline/index.tsx b/packages/plugin-multiple-editor/src/components/Outline/index.tsx new file mode 100644 index 0000000..6cdcd37 --- /dev/null +++ b/packages/plugin-multiple-editor/src/components/Outline/index.tsx @@ -0,0 +1,145 @@ +import { parse, generateOutline } from '../../utils/ghostBabel'; +import './index.less'; +import cls from 'classnames'; +import React, { FC, useEffect, useMemo, useState } from 'react'; +import { Icon } from '@alifd/next'; +import classIcon from './img/class.png'; +import functionIcon from './img/function.png'; +import propsIcon from './img/props.png'; +import { focusByContent } from './utils'; +import { editorController } from '../../Controller'; +export interface OutlineProps { + content?: string; + className?: string; + filePath?: string; + ilpOutLineStyle?: any; +} +export interface DirProps { + children?: DirProps[]; + name: string; + type: string; +} +export interface OutlineNodeProps { + disableAction?: boolean; + node: DirProps; + selectedKey?: string; + level?: number; + className?: string; +} +const Outline: FC = ({ + content = '', + className, + filePath, + ilpOutLineStyle, +}) => { + const iconMap: Record = { + FunctionDeclaration: functionIcon, + ClassMethod: functionIcon, + ObjectMethod: functionIcon, + ClassProperty: propsIcon, + VariableDeclarator: propsIcon, + ObjectProperty: propsIcon, + ClassDeclaration: classIcon, + }; + const [dir, setDir] = useState([]); + const [path, setPath] = useState(filePath); + useEffect(() => { + try { + const ast = parse(content); + if (ast && generateOutline(ast)) { + setDir(generateOutline(ast)); + } + } catch (e) { + if (path !== filePath) { + setPath(filePath); + setDir([]); + } + } + if (path !== filePath) { + setPath(filePath); + } + }, [content, filePath, path]); + const OutlineNode: FC = ({ + node, + className, + level = 1, + }) => { + const [selectedKey, setSelectedKey] = useState(); + const [expand, setExpand] = useState(true); // 根目录默认展开 + const levelStyle = useMemo(() => ({ paddingLeft: level * 8 }), [level]); + const onRelatedEventClick = (event: any) => { + setSelectedKey(event); + focusByContent( + editorController.codeEditor!, + event, + editorController.monaco!, + filePath! + ); + }; + return ( +
+
+
{ + onRelatedEventClick(node); + }} + > + {!!node?.children?.length && ( + setExpand(!expand)} + /> + )} + + {node.name} +
+
+
+ {expand && ( + <> + {node?.children?.map((f) => { + return ( +
{ + onRelatedEventClick(f); + }} + className={cls( + 'ilp-outline-node-file', + selectedKey === f.name && 'ilp-outline-node-file-selected' + )} + > + + {f.name} +
+ ); + })} + + )} +
+
+ ); + }; + return dir?.length ? ( +
+
+

+ 大纲树 +

+
+ {dir?.map((item) => { + return ; + })} +
+
+
+ ) : null; +}; +export default Outline; diff --git a/packages/plugin-multiple-editor/src/components/Outline/utils.ts b/packages/plugin-multiple-editor/src/components/Outline/utils.ts new file mode 100644 index 0000000..4f5a460 --- /dev/null +++ b/packages/plugin-multiple-editor/src/components/Outline/utils.ts @@ -0,0 +1,59 @@ +import { Monaco } from '@/types'; +import type { editor } from 'monaco-editor'; +export function focusCodeByContent( + editor: editor.IStandaloneCodeEditor, + content: string +) { + const matchedResult = editor + ?.getModel() + // @ts-ignore + ?.findMatches(content, false, true, false)?.[0]; + if (matchedResult) { + setTimeout(() => { + editor.revealLineInCenter(matchedResult.range.startLineNumber); + editor.setPosition({ + column: matchedResult.range.endColumn, + lineNumber: matchedResult.range.endLineNumber, + }); + editor.focus(); + }, 200); + } +} +export async function focusByContent( + editor: editor.IStandaloneCodeEditor, + params: { + name: string; + type: string; + }, + monaco: Monaco, + path: string +) { + const regMap: Record = { + FunctionDeclaration: 'function', + ClassMethod: 'function', + ObjectMethod: 'function', + ClassProperty: 'declarator', + VariableDeclarator: 'declarator', + ObjectProperty: 'ObjectProperty', + ClassDeclaration: 'ClassDeclaration', + }; + const modelUri = monaco.Uri.parse(path); + const model = monaco.editor.getModel(modelUri); + editor.setModel(model); + let content = ''; + switch (regMap[params.type]) { + case 'function': + content = `\\s*(?:async)?\\s*${params.name}\\s*\\([\\s\\S]*\\)[\\s\\S]*\\{`; + break; + case 'declarator': + content = `\\s*${params.name}\\s*=\\s*`; + break; + case 'ObjectProperty': + content = `\\s*${params.name}:\\s*`; + break; + case 'ClassDeclaration': + content = `${params.name} extends`; + break; + } + focusCodeByContent(editor, content); +} diff --git a/packages/plugin-multiple-editor/src/dev-config/sample-plugins/delete-hidden-transducer/index.ts b/packages/plugin-multiple-editor/src/dev-config/sample-plugins/delete-hidden-transducer/index.ts index 921cd03..faa1714 100644 --- a/packages/plugin-multiple-editor/src/dev-config/sample-plugins/delete-hidden-transducer/index.ts +++ b/packages/plugin-multiple-editor/src/dev-config/sample-plugins/delete-hidden-transducer/index.ts @@ -11,6 +11,6 @@ export const deleteHiddenTransducer = (ctx: any) => { }, IPublicEnumTransformStage.Save); }, }; -} +}; deleteHiddenTransducer.pluginName = 'deleteHiddenTransducer'; diff --git a/packages/plugin-multiple-editor/src/dev-config/sample-plugins/scenario-switcher/index.tsx b/packages/plugin-multiple-editor/src/dev-config/sample-plugins/scenario-switcher/index.tsx index 28bd634..7999e65 100644 --- a/packages/plugin-multiple-editor/src/dev-config/sample-plugins/scenario-switcher/index.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/sample-plugins/scenario-switcher/index.tsx @@ -1,31 +1,28 @@ import React from 'react'; -import { - ILowCodePluginContext, -} from '@alilc/lowcode-engine'; import { Select } from '@alifd/next'; import scenarios from '../../universal/scenarios.json'; const { Option } = Select; const getCurrentScenarioName = () => { // return 'index' - const list = location.href.split('/'); + const list = window.location.href.split('/'); return list[list.length - 1].replace('.html', ''); -} +}; function Switcher(props: any) { return () + ); } -export const scenarioSwitcher = (ctx: ILowCodePluginContext) => { +export const scenarioSwitcher = (ctx: any) => { return { name: 'scenarioSwitcher', async init() { diff --git a/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-antd/plugin.tsx b/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-antd/plugin.tsx index c812a4d..1abe147 100644 --- a/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-antd/plugin.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-antd/plugin.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { - ILowCodePluginContext, plugins, skeleton, project, @@ -47,7 +46,7 @@ export default async function registerPlugins() { (SimulatorResizer as any).pluginName = 'SimulatorResizer'; plugins.register(SimulatorResizer); - const editorInit = (ctx: ILowCodePluginContext) => { + const editorInit = (ctx: any) => { return { name: 'editor-init', async init() { @@ -73,7 +72,7 @@ export default async function registerPlugins() { editorInit.pluginName = 'editorInit'; await plugins.register(editorInit); - const builtinPluginRegistry = (ctx: ILowCodePluginContext) => { + const builtinPluginRegistry = (ctx: any) => { return { name: 'builtin-plugin-registry', async init() { @@ -117,7 +116,7 @@ export default async function registerPlugins() { await plugins.register(builtinPluginRegistry); // 设置内置 setter 和事件绑定、插件绑定面板 - const setterRegistry = (ctx: ILowCodePluginContext) => { + const setterRegistry = (ctx: any) => { const { setterMap, pluginMap } = AliLowCodeEngineExt; return { name: 'ext-setters-registry', @@ -155,7 +154,7 @@ export default async function registerPlugins() { // 注册中英文切换 await plugins.register(ZhEnPlugin); - const loadAssetsSample = (ctx: ILowCodePluginContext) => { + const loadAssetsSample = (ctx: any) => { return { name: 'loadAssetsSample', async init() { @@ -180,7 +179,7 @@ export default async function registerPlugins() { await plugins.register(loadAssetsSample); // 注册保存面板 - const saveSample = (ctx: ILowCodePluginContext) => { + const saveSample = (ctx: any) => { return { name: 'saveSample', async init() { @@ -208,7 +207,7 @@ export default async function registerPlugins() { ), }); - hotkey.bind('command+s', (e) => { + hotkey.bind('command+s', (e: any) => { e.preventDefault(); saveSchema('basic-antd'); }); @@ -225,10 +224,10 @@ export default async function registerPlugins() { // await plugins.register(CodeEditor); // 注册出码插件 - CodeGenPlugin.pluginName = 'CodeGenPlugin'; - await plugins.register(CodeGenPlugin); + // CodeGenPlugin.pluginName = 'CodeGenPlugin'; + // await plugins.register(CodeGenPlugin); - const previewSample = (ctx: ILowCodePluginContext) => { + const previewSample = (ctx: any) => { return { name: 'previewSample', async init() { @@ -252,7 +251,7 @@ export default async function registerPlugins() { previewSample.pluginName = 'previewSample'; await plugins.register(previewSample); - const customSetter = (ctx: ILowCodePluginContext) => { + const customSetter = (ctx: any) => { return { name: '___registerCustomSetter___', async init() { diff --git a/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion-with-single-component/plugin.tsx b/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion-with-single-component/plugin.tsx index 14e78be..ca06e11 100644 --- a/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion-with-single-component/plugin.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion-with-single-component/plugin.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { - ILowCodePluginContext, plugins, skeleton, project, @@ -47,7 +46,7 @@ export default async function registerPlugins() { (SimulatorResizer as any).pluginName = 'SimulatorResizer'; plugins.register(SimulatorResizer); - const editorInit = (ctx: ILowCodePluginContext) => { + const editorInit = (ctx: any) => { return { name: 'editor-init', async init() { @@ -73,7 +72,7 @@ export default async function registerPlugins() { editorInit.pluginName = 'editorInit'; await plugins.register(editorInit); - const builtinPluginRegistry = (ctx: ILowCodePluginContext) => { + const builtinPluginRegistry = (ctx: any) => { return { name: 'builtin-plugin-registry', async init() { @@ -117,7 +116,7 @@ export default async function registerPlugins() { await plugins.register(builtinPluginRegistry); // 设置内置 setter 和事件绑定、插件绑定面板 - const setterRegistry = (ctx: ILowCodePluginContext) => { + const setterRegistry = (ctx: any) => { const { setterMap, pluginMap } = AliLowCodeEngineExt; return { name: 'ext-setters-registry', @@ -155,7 +154,7 @@ export default async function registerPlugins() { // 注册中英文切换 await plugins.register(ZhEnPlugin); - const loadAssetsSample = (ctx: ILowCodePluginContext) => { + const loadAssetsSample = (ctx: any) => { return { name: 'loadAssetsSample', async init() { @@ -180,7 +179,7 @@ export default async function registerPlugins() { await plugins.register(loadAssetsSample); // 注册保存面板 - const saveSample = (ctx: ILowCodePluginContext) => { + const saveSample = (ctx: any) => { return { name: 'saveSample', async init() { @@ -232,7 +231,7 @@ export default async function registerPlugins() { // CodeEditor.pluginName = 'CodeEditor'; // await plugins.register(CodeEditor); - const previewSample = (ctx: ILowCodePluginContext) => { + const previewSample = (ctx: any) => { return { name: 'previewSample', async init() { @@ -259,7 +258,7 @@ export default async function registerPlugins() { previewSample.pluginName = 'previewSample'; await plugins.register(previewSample); - const customSetter = (ctx: ILowCodePluginContext) => { + const customSetter = (ctx: any) => { return { name: '___registerCustomSetter___', async init() { diff --git a/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion/plugin.tsx b/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion/plugin.tsx index a05fb96..8e7103f 100644 --- a/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion/plugin.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/scenarios/basic-fusion/plugin.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { - ILowCodePluginContext, plugins, skeleton, project, @@ -47,7 +46,7 @@ export default async function registerPlugins() { (SimulatorResizer as any).pluginName = 'SimulatorResizer'; plugins.register(SimulatorResizer); - const editorInit = (ctx: ILowCodePluginContext) => { + const editorInit = (ctx: any) => { return { name: 'editor-init', async init() { @@ -73,7 +72,7 @@ export default async function registerPlugins() { editorInit.pluginName = 'editorInit'; await plugins.register(editorInit); - const builtinPluginRegistry = (ctx: ILowCodePluginContext) => { + const builtinPluginRegistry = (ctx: any) => { return { name: 'builtin-plugin-registry', async init() { @@ -117,7 +116,7 @@ export default async function registerPlugins() { await plugins.register(builtinPluginRegistry); // 设置内置 setter 和事件绑定、插件绑定面板 - const setterRegistry = (ctx: ILowCodePluginContext) => { + const setterRegistry = (ctx: any) => { const { setterMap, pluginMap } = AliLowCodeEngineExt; return { name: 'ext-setters-registry', @@ -155,7 +154,7 @@ export default async function registerPlugins() { // 注册中英文切换 await plugins.register(ZhEnPlugin); - const loadAssetsSample = (ctx: ILowCodePluginContext) => { + const loadAssetsSample = (ctx: any) => { return { name: 'loadAssetsSample', async init() { @@ -180,7 +179,7 @@ export default async function registerPlugins() { await plugins.register(loadAssetsSample); // 注册保存面板 - const saveSample = (ctx: ILowCodePluginContext) => { + const saveSample = (ctx: any) => { return { name: 'saveSample', async init() { @@ -229,10 +228,10 @@ export default async function registerPlugins() { // await plugins.register(CodeEditor); // 注册出码插件 - CodeGenPlugin.pluginName = 'CodeGenPlugin'; - await plugins.register(CodeGenPlugin); + // CodeGenPlugin.pluginName = 'CodeGenPlugin'; + // await plugins.register(CodeGenPlugin); - const previewSample = (ctx: ILowCodePluginContext) => { + const previewSample = (ctx: any) => { return { name: 'previewSample', async init() { @@ -256,7 +255,7 @@ export default async function registerPlugins() { previewSample.pluginName = 'previewSample'; await plugins.register(previewSample); - const customSetter = (ctx: ILowCodePluginContext) => { + const customSetter = (ctx: any) => { return { name: '___registerCustomSetter___', async init() { diff --git a/packages/plugin-multiple-editor/src/dev-config/scenarios/next-pro/plugin.tsx b/packages/plugin-multiple-editor/src/dev-config/scenarios/next-pro/plugin.tsx index 1691a9e..ab2d80c 100644 --- a/packages/plugin-multiple-editor/src/dev-config/scenarios/next-pro/plugin.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/scenarios/next-pro/plugin.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { - ILowCodePluginContext, plugins, skeleton, project, @@ -53,7 +52,7 @@ export default async function registerPlugins() { (SimulatorResizer as any).pluginName = 'SimulatorResizer'; plugins.register(SimulatorResizer); - const editorInit = (ctx: ILowCodePluginContext) => { + const editorInit = (ctx: any) => { return { name: 'editor-init', async init() { @@ -79,7 +78,7 @@ export default async function registerPlugins() { editorInit.pluginName = 'editorInit'; await plugins.register(editorInit); - const builtinPluginRegistry = (ctx: ILowCodePluginContext) => { + const builtinPluginRegistry = (ctx: any) => { return { name: 'builtin-plugin-registry', async init() { @@ -123,7 +122,7 @@ export default async function registerPlugins() { await plugins.register(builtinPluginRegistry); // 设置内置 setter 和事件绑定、插件绑定面板 - const setterRegistry = (ctx: ILowCodePluginContext) => { + const setterRegistry = (ctx: any) => { const { setterMap, pluginMap } = AliLowCodeEngineExt; return { name: 'ext-setters-registry', @@ -161,7 +160,7 @@ export default async function registerPlugins() { // 注册中英文切换 await plugins.register(ZhEnPlugin); - const loadAssetsSample = (ctx: ILowCodePluginContext) => { + const loadAssetsSample = (ctx: any) => { return { name: 'loadAssetsSample', async init() { @@ -186,7 +185,7 @@ export default async function registerPlugins() { await plugins.register(loadAssetsSample); // 注册保存面板 - const saveSample = (ctx: ILowCodePluginContext) => { + const saveSample = (ctx: any) => { return { name: 'saveSample', async init() { @@ -230,7 +229,7 @@ export default async function registerPlugins() { // CodeEditor.pluginName = 'CodeEditor'; // await plugins.register(CodeEditor); - const previewSample = (ctx: ILowCodePluginContext) => { + const previewSample = (ctx: any) => { return { name: 'previewSample', async init() { @@ -254,7 +253,7 @@ export default async function registerPlugins() { previewSample.pluginName = 'previewSample'; await plugins.register(previewSample); - const customSetter = (ctx: ILowCodePluginContext) => { + const customSetter = (ctx: any) => { return { name: '___registerCustomSetter___', async init() { diff --git a/packages/plugin-multiple-editor/src/dev-config/scenarios/node-extended-actions/plugin.tsx b/packages/plugin-multiple-editor/src/dev-config/scenarios/node-extended-actions/plugin.tsx index 6c229f6..89e23f7 100644 --- a/packages/plugin-multiple-editor/src/dev-config/scenarios/node-extended-actions/plugin.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/scenarios/node-extended-actions/plugin.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { - ILowCodePluginContext, plugins, skeleton, project, @@ -37,7 +36,7 @@ import assets from './assets.json'; import schema from './schema.json'; export default async function registerPlugins() { - const addHelloAction = (ctx: ILowCodePluginContext) => { + const addHelloAction = (ctx: any) => { return { async init() { const { addBuiltinComponentAction } = ctx.material; @@ -60,7 +59,7 @@ export default async function registerPlugins() { addHelloAction.pluginName = 'addHelloAction'; await plugins.register(addHelloAction); - const removeCopyAction = (ctx: ILowCodePluginContext) => { + const removeCopyAction = (ctx: any) => { return { async init() { const { removeBuiltinComponentAction } = ctx.material; @@ -82,7 +81,7 @@ export default async function registerPlugins() { (SimulatorResizer as any).pluginName = 'SimulatorResizer'; plugins.register(SimulatorResizer); - const editorInit = (ctx: ILowCodePluginContext) => { + const editorInit = (ctx: any) => { return { name: 'editor-init', async init() { @@ -108,7 +107,7 @@ export default async function registerPlugins() { editorInit.pluginName = 'editorInit'; await plugins.register(editorInit); - const builtinPluginRegistry = (ctx: ILowCodePluginContext) => { + const builtinPluginRegistry = (ctx: any) => { return { name: 'builtin-plugin-registry', async init() { @@ -152,7 +151,7 @@ export default async function registerPlugins() { await plugins.register(builtinPluginRegistry); // 设置内置 setter 和事件绑定、插件绑定面板 - const setterRegistry = (ctx: ILowCodePluginContext) => { + const setterRegistry = (ctx: any) => { const { setterMap, pluginMap } = AliLowCodeEngineExt; return { name: 'ext-setters-registry', @@ -190,7 +189,7 @@ export default async function registerPlugins() { // 注册中英文切换 await plugins.register(ZhEnPlugin); - const loadAssetsSample = (ctx: ILowCodePluginContext) => { + const loadAssetsSample = (ctx: any) => { return { name: 'loadAssetsSample', async init() { @@ -215,7 +214,7 @@ export default async function registerPlugins() { await plugins.register(loadAssetsSample); // 注册保存面板 - const saveSample = (ctx: ILowCodePluginContext) => { + const saveSample = (ctx: any) => { return { name: 'saveSample', async init() { @@ -263,7 +262,7 @@ export default async function registerPlugins() { // CodeEditor.pluginName = 'CodeEditor'; // await plugins.register(CodeEditor); - const previewSample = (ctx: ILowCodePluginContext) => { + const previewSample = (ctx: any) => { return { name: 'previewSample', async init() { @@ -290,7 +289,7 @@ export default async function registerPlugins() { previewSample.pluginName = 'previewSample'; await plugins.register(previewSample); - const customSetter = (ctx: ILowCodePluginContext) => { + const customSetter = (ctx: any) => { return { name: '___registerCustomSetter___', async init() { diff --git a/packages/plugin-multiple-editor/src/dev-config/universal/plugin.tsx b/packages/plugin-multiple-editor/src/dev-config/universal/plugin.tsx index c55ddff..936fa1a 100644 --- a/packages/plugin-multiple-editor/src/dev-config/universal/plugin.tsx +++ b/packages/plugin-multiple-editor/src/dev-config/universal/plugin.tsx @@ -1,15 +1,11 @@ import React from 'react'; import { plugins, - skeleton, project, - setters, } from '@alilc/lowcode-engine'; import AliLowCodeEngineExt from '@alilc/lowcode-engine-ext'; import { Button } from '@alifd/next'; import ComponentsPane from '@alilc/lowcode-plugin-components-pane'; -import ZhEnPlugin from '@alilc/lowcode-plugin-zh-en'; -import SchemaPlugin from '@alilc/lowcode-plugin-schema'; import Inject, { injectAssets } from '@alilc/lowcode-plugin-inject'; // 注册到引擎 @@ -36,10 +32,6 @@ export default async function registerPlugins() { await plugins.register(deleteHiddenTransducer); - // plugin API 见 https://yuque.antfin.com/ali-lowcode/docs/cdukce - SchemaPlugin.pluginName = 'SchemaPlugin'; - await plugins.register(SchemaPlugin); - const editorInit = (ctx: any) => { return { name: 'editor-init', @@ -141,9 +133,6 @@ export default async function registerPlugins() { setterRegistry.pluginName = 'setterRegistry'; await plugins.register(setterRegistry); - // 注册中英文切换 - await plugins.register(ZhEnPlugin); - const loadAssetsSample = (ctx: any) => { return { name: 'loadAssetsSample', diff --git a/packages/plugin-multiple-editor/src/dev-config/universal/utils.ts b/packages/plugin-multiple-editor/src/dev-config/universal/utils.ts index e6e3af7..a3f734d 100644 --- a/packages/plugin-multiple-editor/src/dev-config/universal/utils.ts +++ b/packages/plugin-multiple-editor/src/dev-config/universal/utils.ts @@ -1,7 +1,7 @@ import { material, project } from '@alilc/lowcode-engine'; import { filterPackages } from '@alilc/lowcode-plugin-inject'; import { Message, Dialog } from '@alifd/next'; -import { IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import { TransformStage } from '@alilc/lowcode-types'; export const loadIncrementalAssets = () => { material?.onChangeAssets(() => { @@ -252,7 +252,7 @@ const setProjectSchemaToLocalStorage = (scenarioName: string) => { } window.localStorage.setItem( getLSName(scenarioName), - JSON.stringify(project.exportSchema(IPublicEnumTransformStage.Save)) + JSON.stringify(project.exportSchema(TransformStage.Save as any)) ); }; @@ -296,5 +296,6 @@ function request( headers?: object, otherProps?: any ): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires return Promise.resolve(require('./schema.json')); } diff --git a/packages/plugin-multiple-editor/src/global.d.ts b/packages/plugin-multiple-editor/src/global.d.ts index e238d01..8da7e27 100644 --- a/packages/plugin-multiple-editor/src/global.d.ts +++ b/packages/plugin-multiple-editor/src/global.d.ts @@ -1,2 +1,8 @@ declare module '*.png'; declare module '*.svg'; + +interface Window { + less: any; + prettier: typeof import('prettier'); + prettierPlugins: any; +} diff --git a/packages/plugin-multiple-editor/src/index.dev.tsx b/packages/plugin-multiple-editor/src/index.dev.tsx index 02fe5b5..b544dcc 100644 --- a/packages/plugin-multiple-editor/src/index.dev.tsx +++ b/packages/plugin-multiple-editor/src/index.dev.tsx @@ -10,6 +10,8 @@ import codePlugin from './index'; import { EditorController } from './Controller'; import registerAllPlugin from './dev-config/universal/plugin'; import { SearchPlugin } from './plugins/search-plugin'; +import { EditorPluginInterface, Service } from './Service'; +import { PrettierPlugin } from './plugins/prettier-plugin'; controller.updateMeta({ singleton: true }); @@ -17,15 +19,40 @@ controller.updateMeta({ singleton: true }); console.log((await getMonaco()) === (await getMonaco())); })(); +class TestPlugin implements EditorPluginInterface { + apply(service: Service): void { + service.registerAction({ + icon: ( + 111 + ), + key: 'hello', + action() { + alert('111'); + }, + title: '111', + priority: 0, + }); + } +} + async function initEditor(el: any) { await registerAllPlugin(); await plugins.register( codePlugin({ softSave: true, + useLess: true, // mode: 'single', es6: true, defaultFiles: { 'aspect.js': 'export default {}', + 'utils/index.js': 'xxx', + 'utils/u.js': 'xxx', + 'config/life/index.js': 'xxx', + 'config/life/a.js': 'xxx', + 'util.js': '', }, plugins: [ new SearchPlugin({ @@ -33,6 +60,8 @@ async function initEditor(el: any) { console.log(name); }, }), + new TestPlugin(), + new PrettierPlugin(), ], onInstall(controller: EditorController) { (window as any).codeEditorController = controller; @@ -62,10 +91,17 @@ async function initEditor(el: any) { }, 1000); controller.onEditCodeChange((v) => { - console.log('value change', v); + // console.log('value change', v); }); + + setTimeout(() => { + controller.addFiles({ + 'extends/index.js': 'console.log(1)', + 'extends/util.js': 'console.log(2)', + }); + }, 3000); }, - }) + }) as any ); await init(el, { locale: 'zh-CN', @@ -73,7 +109,7 @@ async function initEditor(el: any) { enableCanvasLock: true, // 默认绑定变量 supportVariableGlobally: true, - }); + } as any); } const LowcodeRender = () => { diff --git a/packages/plugin-multiple-editor/src/index.tsx b/packages/plugin-multiple-editor/src/index.tsx index 6b7b7dc..3e6363c 100644 --- a/packages/plugin-multiple-editor/src/index.tsx +++ b/packages/plugin-multiple-editor/src/index.tsx @@ -1,13 +1,18 @@ -import { project, editor } from '@alilc/lowcode-engine'; +import { project, event } from '@alilc/lowcode-engine'; +import { IPublicTypePluginConfig, IPublicModelPluginContext } from '@alilc/lowcode-types'; import { controller as baseController } from '@alilc/lowcode-plugin-base-monaco-editor/es/controller'; import { EditorProvider } from './Context'; import MultipleFileEditor from './MultipleFileEditor'; import React from 'react'; import { EditorController, editorController } from './Controller'; import { EditorPluginInterface, Service } from './Service'; +import { loadLess, loadPrettier } from './utils/script-loader'; export { editorController }; +loadPrettier(); +loadLess(); + baseController.registerMethod('getSchema', () => { return editorController.getSchema(); }); @@ -20,14 +25,16 @@ const pluginCodeEditor = ( onInstall?: (controller: EditorController) => void; plugins?: EditorPluginInterface[]; defaultFiles?: Record; + useLess?: boolean; } = {} ) => { - const plugin = (ctx: any): any => { + const plugin = (ctx: IPublicModelPluginContext): IPublicTypePluginConfig => { return { exports: () => ({}), init() { options.onInstall?.(editorController); editorController.es6 = options.es6; + editorController.useLess = options.useLess; editorController.defaultFiles = options.defaultFiles || {}; const schemaDock = ctx.skeleton.add({ area: 'leftArea', @@ -62,9 +69,10 @@ const pluginCodeEditor = ( schemaDock && schemaDock.disable(); project.onSimulatorRendererReady(() => { schemaDock.enable(); + const finalEditor = event; const service = new Service(editorController, ctx.skeleton); service.init({ plugins: options.plugins }); - editorController.init(project, editor, service); + editorController.init(project, finalEditor, service); }); }, }; diff --git a/packages/plugin-multiple-editor/src/plugins/prettier-plugin/index.tsx b/packages/plugin-multiple-editor/src/plugins/prettier-plugin/index.tsx new file mode 100644 index 0000000..cab4de5 --- /dev/null +++ b/packages/plugin-multiple-editor/src/plugins/prettier-plugin/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { EditorPluginInterface, Service } from '@/Service'; +import icon from './prettier.png'; +import { HookKeys } from '@/EditorHook'; +import { Message } from '@alifd/next'; + +export class PrettierPlugin implements EditorPluginInterface { + private service!: Service; + + apply(service: Service): void { + this.service = service; + this.service.registerAction({ + key: 'format', + title: '格式化', + icon: , + action: () => { + const currentFile = service.controller.codeEditorCtx?.currentFile; + if (currentFile?.file && window.prettier) { + const { file } = currentFile; + const { ext, content } = file; + const parseMap: any = { + js: 'babel', + ts: 'babel-ts', + less: 'less', + css: 'css', + }; + const formatted = window.prettier.format(content, { + parser: parseMap[ext as string], + semi: true, + singleQuote: true, + bracketSpacing: true, + trailingComma: 'es5', + // endOfLine: false, + plugins: [ + window.prettierPlugins?.babel, + window.prettierPlugins?.postcss, + ].filter(Boolean), + }); + const editor = service.controller.codeEditor; + editor?.executeEdits('', [ + { + range: editor.getModel()!.getFullModelRange(), + text: formatted, + forceMoveMarkers: true, + }, + ]); + editor?.pushUndoStop(); + service.controller.triggerHook(HookKeys.onEditCodeChange, { + file: file?.fullPath, + content: formatted, + }); + Message.success('格式化代码成功'); + } + }, + priority: 1, + }); + } +} diff --git a/packages/plugin-multiple-editor/src/plugins/prettier-plugin/prettier.png b/packages/plugin-multiple-editor/src/plugins/prettier-plugin/prettier.png new file mode 100644 index 0000000..2d8e9d4 Binary files /dev/null and b/packages/plugin-multiple-editor/src/plugins/prettier-plugin/prettier.png differ diff --git a/packages/plugin-multiple-editor/src/plugins/search-plugin/index.ts b/packages/plugin-multiple-editor/src/plugins/search-plugin/index.ts index 396e65f..329da3b 100644 --- a/packages/plugin-multiple-editor/src/plugins/search-plugin/index.ts +++ b/packages/plugin-multiple-editor/src/plugins/search-plugin/index.ts @@ -46,6 +46,7 @@ export class SearchPlugin implements EditorPluginInterface { }); service.onSelectFileChange( ({ filepath, content }: { filepath: string; content: string }) => { + if (!filepath) return; // 只需要初始化一次 if ( filepath.replace(/^\//, '') === 'index.js' && diff --git a/packages/plugin-multiple-editor/src/plugins/search-plugin/search.ts b/packages/plugin-multiple-editor/src/plugins/search-plugin/search.ts index 0a16eed..b3c3c6a 100644 --- a/packages/plugin-multiple-editor/src/plugins/search-plugin/search.ts +++ b/packages/plugin-multiple-editor/src/plugins/search-plugin/search.ts @@ -37,6 +37,9 @@ function getMethodFromNode(node: any): Methods { // 合并一个节点数组,例如 slot 的元素列表 const mergeNodeList = (list: any[]) => { + if (!isArray(list)) { + return; + } for (const item of list) { mergeMethod(methods, getMethodFromNode(item)); } diff --git a/packages/plugin-multiple-editor/src/types/index.ts b/packages/plugin-multiple-editor/src/types/index.ts index 4682325..9f07652 100644 --- a/packages/plugin-multiple-editor/src/types/index.ts +++ b/packages/plugin-multiple-editor/src/types/index.ts @@ -1,7 +1,9 @@ +import { IPublicTypeJSExpression } from '@alilc/lowcode-types'; + export type Monaco = typeof import('monaco-editor/esm/vs/editor/editor.api'); export type ObjectType = Record; -export interface IState { +export interface IState extends IPublicTypeJSExpression { originCode?: string; } diff --git a/packages/plugin-multiple-editor/src/utils/code.ts b/packages/plugin-multiple-editor/src/utils/code.ts index 796f89d..0f76a26 100644 --- a/packages/plugin-multiple-editor/src/utils/code.ts +++ b/packages/plugin-multiple-editor/src/utils/code.ts @@ -1,3 +1,4 @@ +import { IPublicTypeProjectSchema, IPublicTypeRootSchema } from '@alilc/lowcode-types'; // @ts-ignore import prettier from 'prettier/esm/standalone.mjs'; import parserBabel from 'prettier/parser-babel'; @@ -23,7 +24,9 @@ const prettierCssConfig = { printWidth: 120, // 超过120个字符强制换行 }; -export const initCode = (componentSchema: any | undefined) => { +export const initCode = ( + componentSchema: IPublicTypeRootSchema | undefined +) => { return ( (componentSchema as any)?.originCode || `export default class LowcodeComponent extends Component { @@ -72,6 +75,6 @@ export const beautifyCSS = (input: string, options: any): string => { }; // schema转换为CSS代码 -export const schema2CssCode = (schema: any, prettierOptions: any) => { +export const schema2CssCode = (schema: IPublicTypeProjectSchema, prettierOptions: any) => { return beautifyCSS(schema.componentsTree[0]?.css || '', prettierOptions); }; diff --git a/packages/plugin-multiple-editor/src/utils/constants.ts b/packages/plugin-multiple-editor/src/utils/constants.ts index 3e39b1c..76a7bdc 100644 --- a/packages/plugin-multiple-editor/src/utils/constants.ts +++ b/packages/plugin-multiple-editor/src/utils/constants.ts @@ -4,4 +4,6 @@ export const languageMap: ObjectType = { js: 'javascript', css: 'css', json: 'json', + ts: 'typescript', + less: 'less', }; diff --git a/packages/plugin-multiple-editor/src/utils/files.ts b/packages/plugin-multiple-editor/src/utils/files.ts index c9a5d09..3a3f1f2 100644 --- a/packages/plugin-multiple-editor/src/utils/files.ts +++ b/packages/plugin-multiple-editor/src/utils/files.ts @@ -4,7 +4,11 @@ export class File { public type = 'file'; - constructor(public name: string, public content: string) { + constructor( + public name: string, + public content: string, + public fullPath: string + ) { this.ext = name.match(/\.(\w+)$/)?.[1]; } } @@ -13,10 +17,17 @@ export class Dir { public dirs: Dir[]; public files: File[]; public type = 'dir'; - - constructor(public name: string, dirs?: Dir[], files?: File[]) { + public fullPath = ''; + + constructor( + public name: string, + dirs: Dir[], + files: File[], + fullPath: string + ) { this.dirs = dirs || []; this.files = files || []; + this.fullPath = fullPath || ''; } } @@ -25,9 +36,9 @@ export const getKey = (parent: string | undefined, cur: string) => { return `${finalParent}${cur}`; }; -export function getKeyByPath(path: string[], name: string) { - return ['', ...path, name].join('/'); -} +// export function getKeyByPath(path: string[], name: string) { +// return ['', ...path, name].join('/'); +// } export const parseKey = (key: string) => { const fragment = key.split('/').filter(Boolean); @@ -106,13 +117,13 @@ export function generateFile( for (const dir of path) { let found: Dir | undefined = nextDir.dirs.find((d) => d.name === dir); if (!found) { - found = new Dir(dir); + found = new Dir(dir, [], [], path.join('/')); nextDir.dirs.push(found); } nextDir = found; } // 添加文件 - nextDir.files.push(new File(filename, file.content)); + nextDir.files.push(new File(filename, file.content, file.name)); } /** @@ -124,7 +135,7 @@ export function generateFile( export function fileMapToTree(obj: any) { const { files } = compatGetSourceCodeMap(obj); const keys = Object.keys(files); - const dir = new Dir('/'); + const dir = new Dir('/', [], [], ''); for (const key of keys) { generateFile({ name: key, content: files[key] }, dir); } @@ -145,10 +156,14 @@ export function treeToMap(root: Dir, base = '') { return files; } -export function compatGetSourceCodeMap(origin: any = {}) { +export function compatGetSourceCodeMap(origin: any = {}, defaultFiles?: any) { const { meta, files, ...rest } = origin; + let finalFiles = files; + if (!finalFiles) { + finalFiles = Object.keys(rest).length ? rest : defaultFiles; + } return { - files: files || rest, + files: finalFiles || {}, meta: meta || {}, }; } diff --git a/packages/plugin-multiple-editor/src/utils/get-methods.ts b/packages/plugin-multiple-editor/src/utils/get-methods.ts index 4d3b760..6c1315b 100644 --- a/packages/plugin-multiple-editor/src/utils/get-methods.ts +++ b/packages/plugin-multiple-editor/src/utils/get-methods.ts @@ -111,7 +111,7 @@ export const getMethods = (fileContent: string) => { ?.code; const compiledCode = pureTranspile(codeStr, { esm: true }); state[ - (property.key as Identifier).name ?? property?.key?.extra?.rawValue as string + ((property.key as Identifier).name ?? (property?.key?.extra as any)?.rawValue) as string ] = { type: 'JSExpression', value: compiledCode.replace(/var *name *= */, '').replace(/;$/, ''), diff --git a/packages/plugin-multiple-editor/src/utils/ghostBabel.ts b/packages/plugin-multiple-editor/src/utils/ghostBabel.ts index 7ee0f79..d3e6883 100644 --- a/packages/plugin-multiple-editor/src/utils/ghostBabel.ts +++ b/packages/plugin-multiple-editor/src/utils/ghostBabel.ts @@ -2,7 +2,11 @@ * 使用 @babel/standalone 的能力实现 parse 和 traverse */ -import { transform, transformFromAst } from '@babel/standalone'; +import { + transform, + transformFromAst, + availablePlugins, +} from '@babel/standalone'; import type { Visitor, PluginItem, @@ -14,6 +18,10 @@ import { Node, File, file } from '@babel/types'; const defaultBabelOption: TransformOptions = { babelrc: false, sourceType: 'module', + plugins: [ + availablePlugins['syntax-jsx'], + availablePlugins['transform-react-jsx'], + ], }; export function traverse(code: string | ParseResult | Node, visitor: Visitor) { @@ -26,13 +34,13 @@ export function traverse(code: string | ParseResult | Node, visitor: Visitor) { if (typeof code === 'string') { transform(code, { ...defaultBabelOption, - plugins: [plugin], - }); + plugins: [...(defaultBabelOption.plugins || []), plugin], + } as any); } else { transformFromAst(code, undefined, { ...defaultBabelOption, - plugins: [plugin], - }); + plugins: [...(defaultBabelOption.plugins || []), plugin], + } as any); } } @@ -46,3 +54,62 @@ export function parse(code: string): File { // @ts-ignore return ast; } +export function generateOutline(ast: any) { + const outlineMap: any[] = []; + + function traverse(node: any, outline: any) { + if (!node) { + return; + } + if (!getName(node) && !node.type) return; + let outlineNode: any = {}; + if (getName(node)) { + outlineNode = { + name: getName(node), + type: node.type, + children: [], + }; + outline.push(outlineNode); + if ( + !(node?.type === 'ClassDeclaration' || node?.type === 'ObjectProperty') + ) + return; + } + for (const key in node) { + if (Object.prototype.hasOwnProperty.call(node, key)) { + const childNode = node[key]; + if (Array.isArray(childNode)) { + childNode.forEach((child) => + traverse(child, outlineNode?.children || outline) + ); + } else if (typeof childNode === 'object') { + traverse(childNode, outlineNode.children || outline); + } + } + } + } + + traverse(ast, outlineMap); + return outlineMap; +} + +export function getName(node: any) { + switch (node.type) { + case 'FunctionDeclaration': + return node.id.name; + case 'VariableDeclarator': + return node.id.name; + case 'ClassDeclaration': + return node.id.name; + case 'ClassProperty': + return node.key.name; + case 'ObjectProperty': + return node.key.name; + case 'ClassMethod': + return node.key.name; + case 'ObjectMethod': + return node.key.name; + default: + return null; + } +} diff --git a/packages/plugin-multiple-editor/src/utils/index.ts b/packages/plugin-multiple-editor/src/utils/index.ts index 3e3fcd0..8f90e59 100644 --- a/packages/plugin-multiple-editor/src/utils/index.ts +++ b/packages/plugin-multiple-editor/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './tsUtils'; export * from './files'; export * from './codeLint'; export * from './ghostBabel'; +export * from './path'; diff --git a/packages/plugin-multiple-editor/src/utils/multipleFile/babel/compile.ts b/packages/plugin-multiple-editor/src/utils/multipleFile/babel/compile.ts index 3533e65..c5c5b02 100644 --- a/packages/plugin-multiple-editor/src/utils/multipleFile/babel/compile.ts +++ b/packages/plugin-multiple-editor/src/utils/multipleFile/babel/compile.ts @@ -37,7 +37,11 @@ export function pureTranspile( ]; let plugins: any[] = [modulePlugin]; if (es6) { - plugins.unshift(availablePlugins['transform-modules-commonjs']); + plugins.unshift( + availablePlugins['transform-modules-commonjs'], + availablePlugins['syntax-jsx'], + availablePlugins['transform-react-jsx'] + ); } if (options?.presets) { presets = presets.concat(options.presets); diff --git a/packages/plugin-multiple-editor/src/utils/path.ts b/packages/plugin-multiple-editor/src/utils/path.ts new file mode 100644 index 0000000..4ffd40c --- /dev/null +++ b/packages/plugin-multiple-editor/src/utils/path.ts @@ -0,0 +1,68 @@ +import uniqBy from 'lodash/uniqBy'; + +export const pathResolve: (...args: string[]) => string = function () { + function normalizeArray(parts: string[], allowAboveRoot: boolean) { + const res = []; + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (!p || p === '.') continue; + if (p === '..') { + if (res.length && res[res.length - 1] !== '..') { + res.pop(); + } else if (allowAboveRoot) { + res.push('..'); + } + } else { + res.push(p); + } + } + return res; + } + let resolvedPath = '', + resolvedAbsolute = false; + for (let i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + // eslint-disable-next-line prefer-rest-params + const path = i >= 0 ? arguments[i] : '/'; + if (!path) { + continue; + } + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path[0] === '/'; + } + resolvedPath = normalizeArray( + resolvedPath.split('/'), + !resolvedAbsolute + ).join('/'); + return (resolvedAbsolute ? '/' : '') + resolvedPath || '.'; +}; + +export function resolveFilePath(filepath: string) { + const list = filepath.replace(/^\//, '').split('/'); + return list.slice(0, list.length - 1).join('/'); +} + +export function getFilesByPath(files: Record, path: string) { + const fileKeys = Object.keys(files); + const getDirsFromList = (list: string[]) => + uniqBy( + list + .filter((f) => f.replace(/^\//, '').split('/').length > 1) + .map((f) => ({ type: 'dir', path: f.split('/')[0] })), + (i) => i.path + ); + if (!path || path === '/') { + const fileList = fileKeys + .filter((key) => key.replace(/^\//, '').split('/').length === 1) + .map((f) => ({ type: 'file', path: f.split('/')[0] })); + return [...getDirsFromList(fileKeys), ...fileList]; + } + + const normalizedList = fileKeys + .filter((key) => key.replace(/^\//, '').startsWith(path.replace(/^\//, ''))) + .map((f) => f.replace(`${path.replace(/(\/$)|(^\/)/g, '')}/`, '')); + const fileList = normalizedList + .filter((f) => f.split('/').length === 1) + .map((f) => ({ type: 'file', path: f })); + + return [...getDirsFromList(normalizedList), ...fileList]; +} diff --git a/packages/plugin-multiple-editor/src/utils/script-loader.ts b/packages/plugin-multiple-editor/src/utils/script-loader.ts new file mode 100644 index 0000000..08efeed --- /dev/null +++ b/packages/plugin-multiple-editor/src/utils/script-loader.ts @@ -0,0 +1,50 @@ +export function loadScript(url: string) { + return new Promise((resolve) => { + let script = document.getElementById(url) as HTMLScriptElement; + if (!script) { + script = document.createElement('script'); + // script.id = url; + script.src = url; + script.type = 'text/javascript'; + } + script.onload = () => { + resolve(script); + }; + script.addEventListener('error', () => { + console.error('load script error', url); + }); + document.body.appendChild(script); + }); +} + +export async function loadLess() { + if (window.less) { + return window.less; + } + await loadScript( + 'https://gw.alipayobjects.com/os/lib/less/4.2.0/dist/less.min.js' + ); + return window.less; +} + +export async function loadPrettier() { + await Promise.all([ + loadScript( + 'https://g.alicdn.com/code/lib/prettier/2.6.0/standalone.min.js' + ), + loadScript( + 'https://g.alicdn.com/code/lib/prettier/2.6.0/parser-postcss.min.js' + ), + loadScript( + 'https://g.alicdn.com/code/lib/prettier/2.6.0/parser-babel.min.js' + ), + ]); + return window.prettier; +} + +export async function loadBabel(): Promise { + const url = + 'https://gw.alipayobjects.com/os/lib/babel/standalone/7.22.13/babel.min.js'; + await loadScript(url); + return (window as any).Babel; +} diff --git a/packages/plugin-multiple-editor/src/utils/transformUMD.ts b/packages/plugin-multiple-editor/src/utils/transformUMD.ts index 844531b..001437f 100644 --- a/packages/plugin-multiple-editor/src/utils/transformUMD.ts +++ b/packages/plugin-multiple-editor/src/utils/transformUMD.ts @@ -88,7 +88,7 @@ function preprocessIndex(content: string) { export function getInitFuncContent(fileMap: ObjectType, es6?: boolean) { const finalFileMap = Object.entries(fileMap) - .filter(([k]) => !['index.css'].includes(k)) + .filter(([k]) => !['index.css', 'index.less'].includes(k)) .map(([key, content]) => { let realContent = content; if (key === 'index.js') { diff --git a/packages/plugin-multiple-editor/tsconfig.json b/packages/plugin-multiple-editor/tsconfig.json index c820ed9..99b6c71 100644 --- a/packages/plugin-multiple-editor/tsconfig.json +++ b/packages/plugin-multiple-editor/tsconfig.json @@ -1,109 +1,28 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - // "incremental": true, /* Enable incremental compilation */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "jsx": "react" /* Specify what JSX code is generated. */, - "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "ES2020" /* Specify what module code is generated. */, - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "strict": false, + "rootDir": "./src", + "baseUrl": "./", + "outDir": "./es", + "moduleResolution": "node", + "declaration": true, + "resolveJsonModule": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "module": "es2020", + "target": "ESNext", + "jsx": "react", "paths": { "@/*": ["./src/*"], "utils": ["./src/utils"], "utils/*": ["./src/utils/*"], "types": ["./src/types"], - "types/*": ["./src/types/*"] - }, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - "resolveJsonModule": true /* Enable importing .json files */, - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./es", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": false /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "types/*": ["./src/types/*"], + "react": [ "./node_modules/@types/react" ] + } }, "include": ["src"], - "exclude": ["src/dev-config/*"] + "exclude": ["src/dev-config"] } diff --git a/packages/plugin-resource-tabs/package.json b/packages/plugin-resource-tabs/package.json index 5fdea66..2e6571c 100644 --- a/packages/plugin-resource-tabs/package.json +++ b/packages/plugin-resource-tabs/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-resource-tabs", - "version": "1.0.3", + "version": "2.0.1", "description": "alibaba lowcode resource tabs plugin", "files": [ "es", @@ -18,7 +18,7 @@ "editor" ], "dependencies": { - "@alilc/lowcode-types": "^1.0.0", + "@alilc/lowcode-types": "^1.3.0", "@alilc/lowcode-utils": "^1.0.0", "react": "^16.8.1", "react-dom": "^16.8.1" @@ -27,11 +27,12 @@ "@alib/build-scripts": "^0.1.3", "@alilc/build-plugin-alt": "^1.0.1", "@alilc/lowcode-engine": "^1.0.0", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/react": "^16.9.13", "@types/react-dom": "^16.9.4", "build-plugin-fusion": "^0.1.22", - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "build-plugin-moment-locales": "^0.1.0" + "build-plugin-moment-locales": "^0.1.0", + "webpack": "^5.89.0" }, "publishConfig": { "registry": "https://registry.npmjs.org/", diff --git a/packages/plugin-resource-tabs/src/index.tsx b/packages/plugin-resource-tabs/src/index.tsx index 011613c..0b376be 100644 --- a/packages/plugin-resource-tabs/src/index.tsx +++ b/packages/plugin-resource-tabs/src/index.tsx @@ -3,6 +3,7 @@ import { IPublicModelPluginContext, IPublicModelResource, IPublicModelWindow, + IPublicTypeContextMenuAction, IPublicTypePlugin, } from '@alilc/lowcode-types'; import React from 'react'; @@ -12,19 +13,21 @@ import { CloseIcon, LockIcon, WarnIcon } from './icon'; import { intl } from './locale'; function CustomTabItem(props: { - icon: any; - title: string; - onClose: () => void; - key: string; - ctx: IPublicModelPluginContext; - id: string; -}) { - const { id: propsId } = props; - const { event } = props.ctx; + pluginContext: IPublicModelPluginContext; + options: IOptions; + resource: IPublicModelResource; +}): React.ReactElement { + const { resource } = props; + const { icon: ResourceIcon } = resource; + const propsId = resource.id || resource.options.id; + const { event } = props.pluginContext; const [changed, setChanged] = useState(false); const [locked, setLocked] = useState(false); const [warned, setWarned] = useState(false); - const [title, setTitle] = useState(props.title); + const [title, setTitle] = useState(resource.title); + const onClose = useCallback(() => { + props.pluginContext.workspace.removeEditorWindow(resource); + }, []); useEffect(() => { event.on('common:windowChanged', (id, changed) => { if (propsId === id) { @@ -56,52 +59,52 @@ function CustomTabItem(props: { } }); }, []); - const ResourceIcon = props.icon; + const ContextMenu = props.pluginContext?.commonUI?.ContextMenu || React.Fragment; return ( -
-
- {ResourceIcon ? : null} -
-
{title}
-
-
{ - e.stopPropagation(); - if (changed) { - Dialog.show({ - v2: true, - title: intl('resource_tabs.src.Warning'), - content: intl('resource_tabs.src.TheCurrentWindowHasUnsaved'), - onOk: () => {}, - onCancel: () => { - props.onClose(); - }, - cancelProps: { - children: intl('resource_tabs.src.DiscardChanges'), - }, - okProps: { - children: intl('resource_tabs.src.ContinueEditing'), - }, - }); - return; - } - props.onClose(); - }} - className="resource-tab-item-close-icon" - > - + +
+
+ {ResourceIcon ? : null}
-
- {changed && !warned ? ( - - ) : null} +
{title}
+
+
{ + e.stopPropagation(); + if (changed) { + Dialog.show({ + v2: true, + title: intl('resource_tabs.src.Warning'), + content: intl('resource_tabs.src.TheCurrentWindowHasUnsaved'), + onOk: () => {}, + onCancel: onClose, + cancelProps: { + children: intl('resource_tabs.src.DiscardChanges'), + }, + okProps: { + children: intl('resource_tabs.src.ContinueEditing'), + }, + }); + return; + } + onClose(); + }} + className="resource-tab-item-close-icon" + > + +
+
+ {changed && !warned ? ( + + ) : null} - {locked ? : null} + {locked ? : null} - {warned ? : null} + {warned ? : null} +
-
+
); } @@ -110,15 +113,18 @@ interface ITabItem { windowId: string; } -function Content(props: { - ctx: IPublicModelPluginContext; - appKey?: string; - onSort?: (windows: IPublicModelWindow[]) => IPublicModelWindow[]; - shape?: 'pure' | 'wrapped' | 'text' | 'capsule'; - tabClassName?: string; +function TabsContent(props: { + pluginContext: IPublicModelPluginContext; + options: IOptions; }) { - const { ctx } = props; - const { workspace } = ctx; + const { pluginContext, options } = props; + const { + onSort, + appKey, + shape, + tabClassName, + } = options; + const { workspace } = pluginContext; const [resourceListMap, setResourceListMap] = useState<{ [key: string]: IPublicModelResource; @@ -126,8 +132,8 @@ function Content(props: { const getTabs = useCallback((): ITabItem[] => { let windows = workspace.windows; - if (props.onSort) { - windows = props.onSort(workspace.windows); + if (onSort) { + windows = onSort(workspace.windows); } return windows.map((d) => { @@ -145,14 +151,14 @@ function Content(props: { const saveTabsToLocal = useCallback(() => { localStorage.setItem( - '___lowcode_plugin_resource_tabs___' + props.appKey, + '___lowcode_plugin_resource_tabs___' + appKey, JSON.stringify(getTabs()) ); localStorage.setItem( - '___lowcode_plugin_resource_tabs_active_title___' + props.appKey, + '___lowcode_plugin_resource_tabs_active_title___' + appKey, JSON.stringify({ id: - workspace.window?.resource.id || + workspace.window?.resource?.id || workspace.window?.resource?.options.id, }) ); @@ -165,7 +171,7 @@ function Content(props: { }); workspace.onChangeActiveWindow(() => { setActiveTitle( - workspace.window?.resource.id || workspace.window?.resource?.options.id + workspace.window?.resource?.id || workspace.window?.resource?.options.id ); saveTabsToLocal(); }); @@ -173,7 +179,9 @@ function Content(props: { useEffect(() => { const initResourceListMap = () => { - const resourceListMap = {}; + const resourceListMap: { + [key: string]: IPublicModelResource; + } = {}; workspace.resourceList.forEach((d) => { resourceListMap[d.id || d.options.id] = d; }); @@ -194,15 +202,15 @@ function Content(props: { } const value: ITabItem[] = JSON.parse( localStorage.getItem( - '___lowcode_plugin_resource_tabs___' + props.appKey - ) + '___lowcode_plugin_resource_tabs___' + appKey + ) || 'null' ); const activeValue: { id: string; } = JSON.parse( localStorage.getItem( - '___lowcode_plugin_resource_tabs_active_title___' + props.appKey - ) + '___lowcode_plugin_resource_tabs_active_title___' + appKey + ) || 'null' ); if (value && value.length) { @@ -231,9 +239,9 @@ function Content(props: { void; - value: string; } ) => ( )} onChange={(name) => { @@ -274,13 +277,8 @@ function Content(props: { return ( { - (workspace as any).removeEditorWindow(resource); - }} + resource={resource} /> ); })} @@ -288,14 +286,36 @@ function Content(props: { ); } +function Content(props: { + pluginContext: IPublicModelPluginContext; + options: IOptions; +}) { + const ContextMenu = props.pluginContext?.commonUI?.ContextMenu || React.Fragment; + return ( + + + + ) +} + +interface IOptions { + appKey?: string; + onSort?: (windows: IPublicModelWindow[]) => IPublicModelWindow[]; + shape?: 'pure' | 'wrapped' | 'text' | 'capsule'; + tabClassName?: string; + /** + * 右键菜单项 + */ + contextMenuActions: (ctx: IPublicModelPluginContext) => IPublicTypeContextMenuAction[]; + /** + * 右键 Tab 菜单项 + */ + tabContextMenuActions: (ctx: IPublicModelPluginContext, resource: IPublicModelResource) => IPublicTypeContextMenuAction[]; +} + const resourceTabs: IPublicTypePlugin = function ( ctx: IPublicModelPluginContext, - options: { - appKey?: string; - onSort?: (windows: IPublicModelWindow[]) => IPublicModelWindow[]; - shape?: string; - tabClassName?: string; - } + options: IOptions, ) { const { skeleton } = ctx; return { @@ -311,11 +331,8 @@ const resourceTabs: IPublicTypePlugin = function ( index: -1, content: Content, contentProps: { - ctx, - appKey: options?.appKey, - onSort: options?.onSort, - shape: options?.shape, - tabClassName: options?.tabClassName, + pluginContext: ctx, + options, }, }); }, @@ -350,8 +367,21 @@ resourceTabs.meta = { type: 'string', description: 'Tab className', }, + { + key: 'contextMenuActions', + type: 'function', + description: '右键菜单项', + }, + { + key: 'tabContextMenuActions', + type: 'function', + description: '右键 Tab 菜单项', + } ], }, + engines: { + lowcodeEngine: '^1.3.0', // 插件需要配合 ^1.0.0 的引擎才可运行 + }, }; export default resourceTabs; diff --git a/packages/plugin-test/package.json b/packages/plugin-test/package.json index 650c04a..09d5504 100644 --- a/packages/plugin-test/package.json +++ b/packages/plugin-test/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-test", - "version": "1.0.0", + "version": "1.0.2", "description": "alibaba lowcode editor test plugin", "files": [ "es", diff --git a/packages/plugin-undo-redo/src/index.tsx b/packages/plugin-undo-redo/src/index.tsx index e9c1efb..7c42a84 100644 --- a/packages/plugin-undo-redo/src/index.tsx +++ b/packages/plugin-undo-redo/src/index.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; -import { ILowCodePluginContext, project } from '@alilc/lowcode-engine'; +import { project } from '@alilc/lowcode-engine'; import { Button, Icon } from '@alifd/next'; -import { PluginProps, IPublicTypeDisposable } from '@alilc/lowcode-types'; +import { PluginProps, IPublicTypeDisposable, IPublicModelPluginContext } from '@alilc/lowcode-types'; import './index.scss'; @@ -89,7 +89,7 @@ class UndoRedo extends PureComponent { } } -const plugin = (ctx: ILowCodePluginContext) => { +const plugin = (ctx: IPublicModelPluginContext) => { return { // 插件名,注册环境下唯一 name: 'PluginUndoRedo', diff --git a/packages/plugin-view-manager-pane/package.json b/packages/plugin-view-manager-pane/package.json index 12fc34f..5c7b92c 100644 --- a/packages/plugin-view-manager-pane/package.json +++ b/packages/plugin-view-manager-pane/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-view-manager-pane", - "version": "1.0.4", + "version": "2.0.1", "description": "alibaba lowcode editor undo redo plugin", "files": [ "es", @@ -18,7 +18,7 @@ "editor" ], "dependencies": { - "@alilc/lowcode-types": "^1.0.0", + "@alilc/lowcode-types": "^1.3.0", "@alilc/lowcode-utils": "^1.0.0", "react": "^16.8.1", "react-dom": "^16.8.1" diff --git a/packages/plugin-view-manager-pane/src/components/addFile/behaviors.tsx b/packages/plugin-view-manager-pane/src/components/addFile/behaviors.tsx index c421f9c..a1bda9c 100644 --- a/packages/plugin-view-manager-pane/src/components/addFile/behaviors.tsx +++ b/packages/plugin-view-manager-pane/src/components/addFile/behaviors.tsx @@ -1,102 +1,31 @@ import React from 'react'; -import { Balloon } from '@alifd/next'; -import { IPublicModelResource } from '@alilc/lowcode-types'; +import { IPublicModelPluginContext, IPublicModelResource } from '@alilc/lowcode-types'; import { OthersIcon } from '../resourceTree/icon'; import { IOptions } from '../..'; -import { intl } from '../../locale'; export function Behaviors(props: { + pluginContext: IPublicModelPluginContext; resource: IPublicModelResource; options: IOptions; - showBehaviors: boolean; - onVisibleChange: any; safeNode: any; }) { - const { description, name } = props.resource; - let behaviors = []; - if (name === 'lowcode') { - props.options?.onEditComponent && behaviors.push('edit'); - props.options?.onCopyComponent && behaviors.push('copy'); - props.options?.onDeleteComponent && behaviors.push('delete'); - } else if (name === 'page') { - props.options?.onEditPage && behaviors.push('edit'); - props.options?.onCopyPage && behaviors.push('copy'); - props.options?.onDeletePage && behaviors.push('delete'); - } + const menus = (props.options?.resourceContextMenuActions?.(props.pluginContext, props.resource) || []).filter(d => !d.condition || d.condition && d.condition()); + const ContextMenu = props.pluginContext.commonUI?.ContextMenu || React.Fragment; - if (!behaviors.length) { + if (!menus.length) { return null; } return ( - { - e.stopPropagation(); - e.preventDefault(); - }} - > - -
- } - triggerType="click" - align="bl" - popupClassName="view-pane-popup" - closable={false} - visible={props.showBehaviors} - safeNode={props.safeNode} - onVisibleChange={props.onVisibleChange} +
{ + e.stopPropagation(); + e.preventDefault(); + ContextMenu.create(menus, e); + }} > - {behaviors.map((d) => { - let text, handleLowcodeClick, handlePageClick; - switch (d) { - case 'edit': - text = intl( - 'view_manager.components.addFile.behaviors.DescriptionSettings', - { description: description } - ); - handleLowcodeClick = props.options?.onEditComponent; - handlePageClick = props.options?.onEditPage; - break; - case 'copy': - text = intl( - 'view_manager.components.addFile.behaviors.CopyDescription', - { description: description } - ); - handleLowcodeClick = props.options?.onCopyComponent; - handlePageClick = props.options?.onCopyPage; - break; - case 'delete': - text = intl( - 'view_manager.components.addFile.behaviors.DeleteDescription', - { description: description } - ); - handleLowcodeClick = props.options?.onDeleteComponent; - handlePageClick = props.options?.onDeletePage; - break; - } - - return ( -
{ - e.stopPropagation(); - e.preventDefault(); - props.onVisibleChange(false); - if (name === 'lowcode') { - handleLowcodeClick(props.resource); - } else { - handlePageClick(props.resource); - } - }} - className="view-pane-popup-item" - > - {text} -
- ); - })} - + +
); } diff --git a/packages/plugin-view-manager-pane/src/components/addFile/index.scss b/packages/plugin-view-manager-pane/src/components/addFile/index.scss index 0f3f1e2..e6621c3 100644 --- a/packages/plugin-view-manager-pane/src/components/addFile/index.scss +++ b/packages/plugin-view-manager-pane/src/components/addFile/index.scss @@ -7,18 +7,6 @@ &::after { display: none; } - - .view-pane-popup-item { - height: 28px; - line-height: 28px; - padding: 0 12px; - color: var(--color-text); - cursor: pointer; - - &:hover { - background-color: var(--color-block-background-light, #f1f3f6); - } - } } .add-file-icon-wrap { diff --git a/packages/plugin-view-manager-pane/src/components/addFile/index.tsx b/packages/plugin-view-manager-pane/src/components/addFile/index.tsx index b66e4e2..165e664 100644 --- a/packages/plugin-view-manager-pane/src/components/addFile/index.tsx +++ b/packages/plugin-view-manager-pane/src/components/addFile/index.tsx @@ -1,55 +1,30 @@ -import { Balloon } from '@alifd/next'; import * as React from 'react'; import { observer } from 'mobx-react'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; import { AddIcon } from '../../icon'; import { IOptions } from '../..'; - import './index.scss'; -import { intl } from '../../locale'; -function AddFileComponent(props: { options: IOptions }) { - if (!props.options?.onAddPage && !props.options?.onAddComponent) { +function AddFileComponent(props: { options: IOptions, pluginContext: IPublicModelPluginContext }) { + if (props.options?.renderAddFileComponent && typeof props.options.renderAddFileComponent === 'function') { + return props.options.renderAddFileComponent(); + } + + const menus = props.options?.contextMenuActions?.(props.pluginContext); + + if (!menus || !menus.length) { return null; } - return ( - <> - - - - } - triggerType="click" - align="bl" - popupClassName="view-pane-popup" - closable={false} - > - {props.options.onAddPage ? ( -
{ - props.options.onAddPage?.(); - }} - className="view-pane-popup-item" - > - {intl('view_manager.components.addFile.CreatePage')} -
- ) : null} + const ContextMenu = props.pluginContext?.commonUI?.ContextMenu || React.Fragment; - {props.options.onAddComponent ? ( -
{ - props.options.onAddComponent?.(); - }} - > - {intl('view_manager.components.addFile.CreateAComponent')} -
- ) : null} -
- - ); + return ( + { + ContextMenu.create(menus, e) + }} className='add-file-icon-wrap'> + + + ) } export const AddFile = observer(AddFileComponent); diff --git a/packages/plugin-view-manager-pane/src/components/resourceTree/index.scss b/packages/plugin-view-manager-pane/src/components/resourceTree/index.scss index 009c22b..ad7da44 100644 --- a/packages/plugin-view-manager-pane/src/components/resourceTree/index.scss +++ b/packages/plugin-view-manager-pane/src/components/resourceTree/index.scss @@ -25,6 +25,11 @@ display: flex; align-items: center; height: 30px; + cursor: pointer; + + &:hover { + background-color: var(--color-block-background-light, #f7f8fa); + } } &-expand { @@ -72,12 +77,6 @@ font-size: 12px; color: var(--color-title, #5f697a); - - &.resource-tree-group-item-pro-code { - color: var(--color-text-disabled, #bcc5d1); - pointer-events: none; - } - &.active .resource-tree-title { background-color: var(--color-block-background-light, #f7f8fa); } @@ -144,6 +143,13 @@ } } + &-group-disabled { + .resource-tree-title { + opacity: 0.4; + cursor: not-allowed; + } + } + &-title { height: 24px; display: flex; diff --git a/packages/plugin-view-manager-pane/src/components/resourceTree/index.tsx b/packages/plugin-view-manager-pane/src/components/resourceTree/index.tsx index c83e419..b3f9e95 100644 --- a/packages/plugin-view-manager-pane/src/components/resourceTree/index.tsx +++ b/packages/plugin-view-manager-pane/src/components/resourceTree/index.tsx @@ -3,7 +3,7 @@ import { IPublicModelPluginContext, IPublicModelResource, } from '@alilc/lowcode-types'; -import { Search, Overlay, Balloon } from '@alifd/next'; +import { Search, Balloon } from '@alifd/next'; import React, { useCallback, useState, useEffect, useRef } from 'react'; import { FileIcon, IconArrowRight } from './icon'; import './index.scss'; @@ -11,13 +11,21 @@ import { IOptions } from '../..'; import { intl } from '../../locale'; import { AddFile } from '../addFile'; +function filterResourceList(resourceList: IPublicModelResource[] | undefined, handler?: Function) { + if (typeof handler === 'function') { + return handler(resourceList); + } + + return resourceList; +} + export function ResourcePaneContent(props: IPluginOptions) { const { workspace } = props.pluginContext || {}; const [resourceList, setResourceList] = useState( - workspace?.resourceList + filterResourceList(workspace?.resourceList, props?.options?.filterResourceList) ); workspace?.onResourceListChange(() => { - setResourceList(workspace.resourceList); + setResourceList(filterResourceList(workspace?.resourceList, props?.options?.filterResourceList)); }); return ( - +
{Array.from(Object.entries(category)).map( @@ -111,9 +119,6 @@ function ResourceGroup( props.defaultExpandAll || props.defaultExpandedCategoryKeys?.includes(props.categoryName) ); - const [visible, setVisible] = useState(false); - const ref = useRef(null); - const resourceArr = props.resourceArr.filter( (d) => !props.filterValue || @@ -149,71 +154,40 @@ function ResourceGroup( ); } + const ContextMenu = props.pluginContext?.commonUI?.ContextMenu || React.Fragment; + const indent = props.depth * 28 + 12; + + const style = { + paddingLeft: indent, + marginLeft: -indent, + } + return (
-
{ - e.preventDefault(); - e.stopPropagation(); - setVisible(!visible); - }} - ref={ref} +
{ setExpanded(!expanded); }} > - -
-
- +
+ +
+
+ +
+
{props.categoryName}
-
{props.categoryName}
- { - [intl('view_manager.components.resourceTree.Page'), intl('view_manager.components.resourceTree.Component')].includes(props.categoryName) ? ( - { - setVisible(false); - }} - safeNode={ref?.current} - // @ts-ignore - placement="br" - className="view-pane-popup" - > -
-
{ - if ( - props.categoryName === - intl('view_manager.components.resourceTree.Page') - ) { - props.options.onAddPage?.(); - } else { - props.options.onAddComponent?.(); - } - }} - className="view-pane-popup-item" - > - {intl('view_manager.components.resourceTree.CreateItem', { - categoryName: props.categoryName === intl('view_manager.components.resourceTree.Page') - ? intl('view_manager.components.resourceTree.Page') - : intl('view_manager.components.resourceTree.Component'), - })} -
-
-
- ) : null - } -
+ {expanded && (
{resourceArr.map((d) => ( @@ -247,7 +221,6 @@ function ResourceItem(props: { }) { const [expanded, setExpanded] = useState(false); const ref = useRef(null); - const [showBehaviors, setShowBehaviors] = useState(false); const PropsIcon = props.icon; const Behaviors = props.behaviors; const display = (props.resource?.config as any)?.display ?? true; @@ -262,118 +235,132 @@ function ResourceItem(props: { } const children = props.children?.filter(d => d.config?.display !== false); + const { + disabled, + tips, + } = props.resource?.config || {}; + const ContextMenu = props.pluginContext?.commonUI?.ContextMenu || React.Fragment; - return ( -
{ - e.preventDefault(); - e.stopPropagation(); - setShowBehaviors(!showBehaviors); - }} - data-depth={props.depth} - > + const context = ( +
{ - props.resource && props.pluginContext?.workspace.openEditorWindow(props.resource); - }} - className="resource-tree-title" - style={style} + ref={ref} + className={`resource-tree-group-node ${ + disabled + ? 'resource-tree-group-disabled' + : '' + } ${props.activeId === props.resource?.options.id || props.activeId === props.resource?.id ? 'active' : ''}`} + data-depth={props.depth} > - {props.resource?.options.modified ? ( -
} - triggerType="hover" - align='bl' - title="" - > - {props.resource.options.modifiedTips} - - ) : null} +
{ + if (disabled) { + return; + } + props.resource && props.pluginContext?.workspace.openEditorWindow(props.resource); + }} + className="resource-tree-title" + style={style} + > + {props.resource?.options.modified ? ( +
} + triggerType="hover" + align='bl' + title="" + > + {props.resource.options.modifiedTips} + + ) : null} - {((children && children.length) || null) && ( -
{ - setExpanded(!expanded); - e.stopPropagation(); - e.preventDefault(); - }} - > - -
- )} + {((children && children.length) || null) && ( +
{ + setExpanded(!expanded); + e.stopPropagation(); + e.preventDefault(); + }} + > + +
+ )} -
- {PropsIcon && } -
-
- {props.resource?.options?.label || props.resource?.title} - {props.resource?.options.isProCodePage - ? intl('view_manager.components.resourceTree.SourceCode') - : ''} +
+ {PropsIcon && } +
+
+ {props.resource?.options?.label || props.resource?.title} - { - props.resource?.options?.slug || - props.resource?.options?.componentName ? ( - - ({ props.resource.options?.slug || props.resource.options?.componentName }) - - ) : null - } -
+ { + props.resource?.options?.slug || + props.resource?.options?.componentName ? ( + + ({ props.resource.options?.slug || props.resource.options?.componentName }) + + ) : null + } +
-
- {Behaviors && - (props.resource?.config as any)?.disableBehaviors !== true ? ( - { - setShowBehaviors(visible); - }} - safeNode={ref?.current} - /> - ) : null} +
+ {Behaviors && + (props.resource?.config as any)?.disableBehaviors !== true ? ( + + ) : null} +
-
- { - expanded && children?.length ? ( -
- { - props.children?.map((child) => ( - - )) - } -
- ) : null - } -
+ { + expanded && children?.length ? ( +
+ { + props.children?.map((child) => ( + + )) + } +
+ ) : null + } +
+ ); + + if (tips) { + return ( + { context }
} + triggerType="hover" + align='r' + title="" + > + {tips} + + ); + } + + return context; } interface IPluginOptions { defaultExpandedCategoryKeys?: string[]; defaultExpandAll?: boolean; - pluginContext?: IPublicModelPluginContext; + pluginContext: IPublicModelPluginContext; behaviors?: any; options: IOptions; } diff --git a/packages/plugin-view-manager-pane/src/index.tsx b/packages/plugin-view-manager-pane/src/index.tsx index c0c6308..c1541f0 100644 --- a/packages/plugin-view-manager-pane/src/index.tsx +++ b/packages/plugin-view-manager-pane/src/index.tsx @@ -3,6 +3,7 @@ import { IPublicModelPluginContext, IPublicModelResource, IPublicTypeSkeletonConfig, + IPublicTypeContextMenuAction, } from '@alilc/lowcode-types'; import Icon from './icon'; import { Pane } from './pane'; @@ -12,27 +13,30 @@ import { intl } from './locale'; export interface IOptions { init?: (ctx: IPublicModelPluginContext) => {}; - onAddPage?: () => {}; + renderAddFileComponent?: () => React.JSX.Element; - onDeletePage?: (resource: IPublicModelResource) => {}; - - onEditPage?: (resource: IPublicModelResource) => {}; - - onCopyPage?: (resource: IPublicModelResource) => {}; - - onAddComponent?: () => {}; + handleClose?: (force?: boolean) => void; - onEditComponent?: (resource: IPublicModelResource) => {}; + filterResourceList?: () => {}; - onCopyComponent?: (resource: IPublicModelResource) => {}; + showIconText?: boolean; - onDeleteComponent?: (resource: IPublicModelResource) => {}; + skeletonConfig?: IPublicTypeSkeletonConfig; - handleClose?: (force?: boolean) => void; + /** + * 右键菜单项 + */ + contextMenuActions?: (ctx: IPublicModelPluginContext) => IPublicTypeContextMenuAction[]; - showIconText?: boolean; + /** + * 右键资源项,菜单项 + */ + resourceContextMenuActions?: (ctx: IPublicModelPluginContext, resource: IPublicModelResource) => IPublicTypeContextMenuAction[]; - skeletonConfig?: IPublicTypeSkeletonConfig; + /** + * 右键资源组,菜单项 + */ + resourceGroupContextMenuActions?: (ctx: IPublicModelPluginContext, resources: IPublicModelResource[]) => IPublicTypeContextMenuAction[]; } const ViewManagerPane = ( @@ -78,7 +82,7 @@ ViewManagerPane.meta = { // 依赖的插件(插件名数组) dependencies: [], engines: { - lowcodeEngine: '^1.0.0', // 插件需要配合 ^1.0.0 的引擎才可运行 + lowcodeEngine: '^1.3.0', // 插件需要配合 ^1.0.0 的引擎才可运行 }, preferenceDeclaration: { title: intl('view_manager.src.ViewManagementPanelPlugIn'), @@ -89,47 +93,37 @@ ViewManagerPane.meta = { description: '', }, { - key: 'onAddPage', - type: 'function', - description: '', - }, - { - key: 'onDeletePage', - type: 'function', - description: '', - }, - { - key: 'onEditPage', + key: 'handleClose', type: 'function', description: '', }, { - key: 'onCopyPage', - type: 'function', + key: 'showIconText', + type: 'boolean', description: '', }, { - key: 'onAddComponent', - type: 'function', + key: 'skeletonConfig', + type: 'object', description: '', }, { - key: 'onEditComponent', + key: 'contextMenuActions', type: 'function', - description: '', + description: '右键菜单项', }, { - key: 'onCopyComponent', + key: 'resourceContextMenuActions', type: 'function', - description: '', + description: '右键资源项,菜单项', }, { - key: 'onDeleteComponent', + key: 'resourceGroupContextMenuActions', type: 'function', - description: '', + description: '右键资源组,菜单项', }, { - key: 'handleClose', + key: 'filterResourceList', type: 'function', description: '', }, diff --git a/packages/plugin-view-manager-pane/src/pane.tsx b/packages/plugin-view-manager-pane/src/pane.tsx index 4ba6c5c..05eca45 100644 --- a/packages/plugin-view-manager-pane/src/pane.tsx +++ b/packages/plugin-view-manager-pane/src/pane.tsx @@ -12,25 +12,29 @@ export function Pane(props: { props.options?.init?.(props.pluginContext); }, []); + const ContextMenu = props.pluginContext.commonUI?.ContextMenu || React.Fragment; + return ( -
+
{ - e.stopPropagation(); - }} + className="workspace-view-pane" > - { - return ; +
{ + e.stopPropagation(); }} - options={props.options} - /> + > + { + return ; + }} + options={props.options} + /> +
-
+ ); } diff --git a/packages/plugin-zh-en/src/index.tsx b/packages/plugin-zh-en/src/index.tsx index cd54be0..035e423 100644 --- a/packages/plugin-zh-en/src/index.tsx +++ b/packages/plugin-zh-en/src/index.tsx @@ -1,6 +1,6 @@ import { PureComponent } from 'react'; -import { ILowCodePluginContext, common } from '@alilc/lowcode-engine'; -import { PluginProps } from '@alilc/lowcode-types'; +import { common } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext, PluginProps } from '@alilc/lowcode-types'; import { intl } from './locale'; import { IconZh } from './icons/zh'; import { IconEn } from './icons/en'; @@ -43,7 +43,7 @@ class ZhEn extends PureComponent { } } -const plugin = (ctx: ILowCodePluginContext) => { +const plugin = (ctx: IPublicModelPluginContext) => { return { // 插件名,注册环境下唯一 name: 'PluginZhEn',