From 5ebbb428fb3fb786d330ab01959028443338d315 Mon Sep 17 00:00:00 2001 From: Wells Date: Fri, 20 Oct 2023 16:48:49 +0800 Subject: [PATCH] fix: enhance expSetter (#43) * docs: Update * fix: register history tool * fix: add eslint to InputCode * fix: enhance expSetter * fix: transform value to expression code * fix: value change of expSetter * fix: input code without wrapper * fix: disable eslint for InputCode by default --------- Co-authored-by: wwsun --- apps/playground/src/mock/project.ts | 2 +- apps/website/docs/designer/quick-start.mdx | 8 +- apps/website/docs/intro.md | 40 ++-- apps/website/docusaurus.config.js | 2 +- .../src/components/HomepageFeatures/index.tsx | 24 +-- apps/website/static/img/cpu-f.svg | 1 + apps/website/static/img/digital-content-4.svg | 1 + apps/website/static/img/html-d.svg | 1 + .../static/img/undraw_docusaurus_mountain.svg | 171 ------------------ .../static/img/undraw_docusaurus_react.svg | 170 ----------------- .../static/img/undraw_docusaurus_tree.svg | 40 ---- packages/core/src/helpers/code-helpers.ts | 60 ++++++ packages/core/src/helpers/index.ts | 1 + packages/core/src/helpers/string.ts | 10 - packages/core/tests/engine.test.ts | 2 - .../designer/src/setters/event-setter.tsx | 11 +- .../src/setters/expression-setter.tsx | 82 ++++----- packages/designer/src/toolbar/mode-switch.tsx | 3 +- packages/designer/src/widgets.ts | 9 +- packages/helpers/src/helpers/string.ts | 25 ++- packages/helpers/tests/helpers.test.ts | 2 + packages/setting-form/package.json | 1 - packages/setting-form/src/form-item.tsx | 2 +- packages/setting-form/src/form-model.tsx | 21 ++- packages/ui/package.json | 8 +- packages/ui/src/input-code.tsx | 34 +++- yarn.lock | 61 ++++++- 27 files changed, 277 insertions(+), 515 deletions(-) create mode 100644 apps/website/static/img/cpu-f.svg create mode 100644 apps/website/static/img/digital-content-4.svg create mode 100644 apps/website/static/img/html-d.svg delete mode 100644 apps/website/static/img/undraw_docusaurus_mountain.svg delete mode 100644 apps/website/static/img/undraw_docusaurus_react.svg delete mode 100644 apps/website/static/img/undraw_docusaurus_tree.svg create mode 100644 packages/core/src/helpers/code-helpers.ts diff --git a/apps/playground/src/mock/project.ts b/apps/playground/src/mock/project.ts index 8a5164a..80625ea 100644 --- a/apps/playground/src/mock/project.ts +++ b/apps/playground/src/mock/project.ts @@ -149,7 +149,7 @@ class App extends React.Component { render() { return ( -
+
diff --git a/apps/website/docs/designer/quick-start.mdx b/apps/website/docs/designer/quick-start.mdx index 530afdf..1b8c999 100644 --- a/apps/website/docs/designer/quick-start.mdx +++ b/apps/website/docs/designer/quick-start.mdx @@ -2,10 +2,10 @@ import Link from '@docusaurus/Link'; # 快速开始 -Tango 是一个用于快速构建低代码工具的前端框架。借助 Tango 只需要少量代码就可以快速的搭建一个基本的低代码设计器。本文将会帮助你借助 Tango 实现一个基本的低代码可视化设计器。 +Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。 :::warning 测试版免责声明 -基于 Tango 低代码引擎的低代码平台已经在网易云音乐内部服务了内部众多业务开发场景。由于开源工作涉及到大量的代码分离工作,当前开源版本为低代码引擎的 alpha 演示版本,API 可能不稳定,不建议在生产环境中使用。我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。 +当前开源版本为低代码引擎的 beta 演示版本,API 可能不稳定,不建议在生产环境中使用。为了保障社区版本能够有效与内部版本解藕,并且项目开源涉及到大量的额外的代码分离工作,我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。 ::: ## 环境要求 @@ -34,6 +34,10 @@ Tango 是一个用于快速构建低代码工具的前端框架。借助 Tango 本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。 +:::tip +沙箱部分来源于 codesandbox,不属于 tango 的核心代码库,我们在原有的 codesandbox 基础上进行了一系列的改造 +::: + 如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 playground 共用一级域名,请直接跳至第 7 步修改 `bundlerURL` 为在线沙箱的地址。 1. 克隆仓库至本地 diff --git a/apps/website/docs/intro.md b/apps/website/docs/intro.md index e24407d..bdadcad 100644 --- a/apps/website/docs/intro.md +++ b/apps/website/docs/intro.md @@ -2,36 +2,30 @@ import Link from '@docusaurus/Link'; # 简介 -Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器**直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对代码的修改**。借助 Tango 可以实现 **源码进,源码出**的效果。 +Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。 -## 引擎架构概览 +preview -Tango 低代码引擎在实现上进行了分层解耦,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。另一方面,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。 +## 核心特性 -如下图所示: +- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等 +- 基于源码 AST 驱动,无私有 DSL 和协议 +- 提供实时出码能力,支持源码进,源码出 +- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件 +- 使用 TypeScript 开发,提供完整的类型定义文件 - +## 实现原理 -## 引擎设计器构成概览 +Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。具体的过程如下图所示,首先将源代码解析为 AST。用户的拖拉拽等操作则映射为对 AST 的遍历和修改。最后将新的 AST 重新生成代码,交给设计器沙箱去渲染执行。而对 AST 的解析、遍历、修改、生成,则可以借助大量的社区工具,这里我们选择的是 babel! -Tango 低代码引擎设计器用于低成本初始化一个基本的低代码平台,其前端部分主要包括如下几个部分: +![Tango 低代码引擎实现分析](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578073085/61cd/b2db/e103/9ed9dd334a6679c6ec18a02270efe446.png) -- 设计器主框架:进行外框架初始化,状态管理,拖拽引擎绑定等核心逻辑。 -- 侧边栏面板:提供可扩展的侧边面板,除了内置通用的面板,用户还可以任意添加自定义的面板。 -- 属性设置面板:属性设置器面板,支持用户进行属性配置。 -- 运行时沙箱:用于设计器运行时视图的渲染。 -- Web IDE:用于进行在线源代码编辑。 +## 技术架构 - +Tango 低代码引擎主要包括 3 个核心组成部分,分别是: -## 基于源代码的引擎内核 +- 引擎内核:扶额建立文件,节点模型,提供输入输出能力。 +- 拖拽引擎和可视化面板:提供可视化开发能力 +- 渲染沙箱:提供源码在浏览器上的编译执行能力。 -Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的借助 [Schema 驱动的低代码方案](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA) 相比,不受到私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。 - - - -## 源码进,源码出 - -由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。从而使得,Tango 构建的线上研发能力可以与团队现有的研发服务(代码托管、构建、部署、CDN)无缝衔接与集成。 - - +![引擎构成](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579167082/1404/27e2/b8e5/0c719ca82494a282080d73adeff7196e.png) diff --git a/apps/website/docusaurus.config.js b/apps/website/docusaurus.config.js index 008cf58..f78743b 100644 --- a/apps/website/docusaurus.config.js +++ b/apps/website/docusaurus.config.js @@ -80,7 +80,7 @@ const config = { type: 'docSidebar', sidebarId: 'designer', position: 'left', - label: '引擎文档', + label: '文档', }, { type: 'docSidebar', diff --git a/apps/website/src/components/HomepageFeatures/index.tsx b/apps/website/src/components/HomepageFeatures/index.tsx index 5a1e750..eedfac4 100644 --- a/apps/website/src/components/HomepageFeatures/index.tsx +++ b/apps/website/src/components/HomepageFeatures/index.tsx @@ -3,37 +3,37 @@ import clsx from 'clsx'; import styles from './styles.module.css'; import Translate, { translate } from '@docusaurus/Translate'; -type FeatureItem = { +interface FeatureItem { title: string; Svg: React.ComponentType>; description: JSX.Element; -}; +} const FeatureList: FeatureItem[] = [ { - title: translate({ id: 'homepage.features.low-code', message: '可视化搭建' }), - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }), + Svg: require('@site/static/img/html-d.svg').default, description: ( - - 提供通用低代码引擎,支持通过拖拽和配置的方式快捷创建和修改应用,轻松完成模式化应用开发。 + + 基于项目源码提供低代码能力,提供源码级的自定义扩展能力 ), }, { - title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }), - Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, + title: translate({ id: 'homepage.features.low-code', message: '实时出码' }), + Svg: require('@site/static/img/digital-content-4.svg').default, description: ( - - 低代码引擎底层使用源代码驱动,无私有搭建协议和DSL,支持在线低代码和源码双模式同步开发。 + + 源码进,源码出,可视化和源码自由切换 ), }, { title: translate({ id: 'homepage.features.outbox', message: '开箱即用' }), - Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, + Svg: require('@site/static/img/cpu-f.svg').default, description: ( - 提供开箱即用的低代码设计器组件,支持轻松网关,快捷构建基于低代码引擎的低代码生产力工具。 + 提供灵活易用的设计器框架,支持开发者自由扩展封装 ), }, diff --git a/apps/website/static/img/cpu-f.svg b/apps/website/static/img/cpu-f.svg new file mode 100644 index 0000000..7e669c6 --- /dev/null +++ b/apps/website/static/img/cpu-f.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/website/static/img/digital-content-4.svg b/apps/website/static/img/digital-content-4.svg new file mode 100644 index 0000000..5094308 --- /dev/null +++ b/apps/website/static/img/digital-content-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/website/static/img/html-d.svg b/apps/website/static/img/html-d.svg new file mode 100644 index 0000000..344e5f1 --- /dev/null +++ b/apps/website/static/img/html-d.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/website/static/img/undraw_docusaurus_mountain.svg b/apps/website/static/img/undraw_docusaurus_mountain.svg deleted file mode 100644 index af961c4..0000000 --- a/apps/website/static/img/undraw_docusaurus_mountain.svg +++ /dev/null @@ -1,171 +0,0 @@ - - Easy to Use - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/website/static/img/undraw_docusaurus_react.svg b/apps/website/static/img/undraw_docusaurus_react.svg deleted file mode 100644 index 94b5cf0..0000000 --- a/apps/website/static/img/undraw_docusaurus_react.svg +++ /dev/null @@ -1,170 +0,0 @@ - - Powered by React - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/website/static/img/undraw_docusaurus_tree.svg b/apps/website/static/img/undraw_docusaurus_tree.svg deleted file mode 100644 index d9161d3..0000000 --- a/apps/website/static/img/undraw_docusaurus_tree.svg +++ /dev/null @@ -1,40 +0,0 @@ - - Focus on What Matters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/core/src/helpers/code-helpers.ts b/packages/core/src/helpers/code-helpers.ts new file mode 100644 index 0000000..82f0205 --- /dev/null +++ b/packages/core/src/helpers/code-helpers.ts @@ -0,0 +1,60 @@ +import { getVariableContent, isVariableString } from '@music163/tango-helpers'; +import { value2node, expression2code } from './ast'; + +/** + * 将 js value 转换为代码字符串 + */ +export function value2code(value: any) { + const node = value2node(value); + const code = expression2code(node); + return code; +} + +/** + * 是否是字符串代码 + * @param code + * @returns + */ +function isStringCode(code: string) { + return /^".*"$/.test(code?.trim()); +} + +/** + * js value 转为表达式代码 + * @example 1 => 1 + * @example hello => "hello" + * @example { foo: bar } => {{ foo: bar }} + * @example [1,2,3] => {[1,2,3]} + * + * @param val js value + * @returns 表达式代码 + */ +export function value2expressionCode(val: any) { + if (!val) return ''; + + let ret; + + switch (typeof val) { + case 'string': { + if (isVariableString(val)) { + ret = getVariableContent(val); + } else if (isStringCode(val)) { + ret = val; + } else { + ret = `"${val}"`; + } + break; + } + case 'number': + ret = String(val); + break; + case 'object': + ret = value2code(val); + break; + default: + ret = ''; + break; + } + + return ret; +} diff --git a/packages/core/src/helpers/index.ts b/packages/core/src/helpers/index.ts index c24c287..06bdbc1 100644 --- a/packages/core/src/helpers/index.ts +++ b/packages/core/src/helpers/index.ts @@ -1,5 +1,6 @@ export * from './ast'; export * from './assert'; +export * from './code-helpers'; export * from './string'; export * from './object'; export * from './prototype'; diff --git a/packages/core/src/helpers/string.ts b/packages/core/src/helpers/string.ts index feaabca..7cb7c3e 100644 --- a/packages/core/src/helpers/string.ts +++ b/packages/core/src/helpers/string.ts @@ -1,6 +1,5 @@ import path from 'path'; import { camelCase, upperCamelCase } from '@music163/tango-helpers'; -import { value2node, expression2code } from './ast'; import { FileType } from './../types'; /** @@ -141,15 +140,6 @@ export function formatCode(code: string, parser = 'babel') { return code; } -/** - * 将 js value 转换为代码字符串 - */ -export function value2code(value: any) { - const node = value2node(value); - const code = expression2code(node); - return code; -} - /** * 是否匹配路由 * @example isPathnameMatchRoute('/user/123', '/users/:id') -> true diff --git a/packages/core/tests/engine.test.ts b/packages/core/tests/engine.test.ts index afe7192..53e426f 100644 --- a/packages/core/tests/engine.test.ts +++ b/packages/core/tests/engine.test.ts @@ -27,8 +27,6 @@ describe('engine', () => { }), }); expect(engine.workspace.activeViewModule).toBeUndefined(); - expect(engine.workspace.serviceModule).toBeUndefined(); expect(engine.workspace.routeModule).toBeUndefined(); - expect(engine.workspace?.storeModules.length).toEqual(0); }); }); diff --git a/packages/designer/src/setters/event-setter.tsx b/packages/designer/src/setters/event-setter.tsx index 66b3e6d..107f8ac 100644 --- a/packages/designer/src/setters/event-setter.tsx +++ b/packages/designer/src/setters/event-setter.tsx @@ -4,7 +4,8 @@ import { AutoComplete } from 'antd'; import { ActionSelect } from '@music163/tango-ui'; import { FormItemComponentProps } from '@music163/tango-setting-form'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; -import { ExpressionModal, getInputValue } from './expression-setter'; +import { wrapCode } from '@music163/tango-helpers'; +import { ExpressionModal } from './expression-setter'; enum EventAction { NoAction = 'noAction', @@ -71,19 +72,19 @@ export function EventSetter(props: EventSetterProps) { }; const actionText = getActionText(type, temp, value); - const inputValue = getInputValue(value); + const inputValue = value; return ( setExpModalVisible(false)} onOk={(nextValue) => { setExpModalVisible(false); - handleChange(nextValue); + handleChange(wrapCode(nextValue)); }} dataSource={actionVariables} /> @@ -141,7 +142,7 @@ function getActionText(type: EventAction, temp: string, value: any) { if (handlerMap[type]) { text = getExpressionValue(type, temp); } else if (value) { - text = getInputValue(value); + text = value; } text = text || '请选择'; return text; diff --git a/packages/designer/src/setters/expression-setter.tsx b/packages/designer/src/setters/expression-setter.tsx index 20f17ed..d912c21 100644 --- a/packages/designer/src/setters/expression-setter.tsx +++ b/packages/designer/src/setters/expression-setter.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Box, css } from 'coral-system'; +import { Box, Text, css } from 'coral-system'; import { Modal } from 'antd'; -import { value2code, isValidExpressionCode } from '@music163/tango-core'; +import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core'; import { isVariableString, getVariableContent, noop, useBoolean, getValue, + wrapCode, } from '@music163/tango-helpers'; import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons'; import { IconButton, Panel, InputCode } from '@music163/tango-ui'; @@ -36,31 +37,6 @@ export const jsonValueValidate = (value: string) => { } }; -// FIXME: 这里应该用原始的代码 raw value -export const getInputValue = (val: any) => { - if (!val) return ''; - - let ret; - - switch (typeof val) { - case 'string': - ret = val; - break; - case 'number': - ret = String(val); - break; - case 'object': - ret = value2code(val); - ret = `{${ret}}`; - break; - default: - ret = ''; - break; - } - - return ret; -}; - const suffixStyle = css` .anticon-close-circle { color: rgba(0, 0, 0, 0.25); @@ -83,32 +59,37 @@ export function ExpressionSetter(props: ExpressionSetterProps) { modalTitle, modalTip, autoCompleteOptions, - placeholder = '表达式请使用 {} 包裹', + placeholder = '输入JS表达式代码', value: valueProp, status, allowClear = true, } = props; const [visible, { on, off }] = useBoolean(); const [inputValue, setInputValue] = useState(() => { - return getInputValue(valueProp); + return value2expressionCode(valueProp); }); const sandbox = useSandboxQuery(); const evaluateContext = sandbox.window; // when receive new value, sync state useEffect(() => { - setInputValue(getInputValue(valueProp)); + setInputValue(value2expressionCode(valueProp)); }, [valueProp]); - const change = (code: string) => { - if (!code) { - code = undefined; - } + const change = useCallback( + (code: string) => { + if (!code) { + onChange(undefined); + return; + } + if (getVariableContent(code) === value2expressionCode(valueProp)) { + return; + } - if (code !== valueProp) { - onChange(code); - } - }; + onChange(wrapCode(code)); + }, + [valueProp, onChange], + ); return ( @@ -147,7 +128,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) { placeholder={placeholder} autoCompleteOptions={autoCompleteOptions} visible={visible} - defaultValue={inputValue} + value={inputValue} onCancel={() => off()} onOk={(value) => { onChange(value); @@ -164,6 +145,7 @@ export interface ExpressionModalProps { placeholder?: string; visible?: boolean; defaultValue?: string; + value?: string; onCancel?: () => void; onOk?: (value: string) => void; dataSource?: IVariableTreeNode[]; @@ -178,10 +160,12 @@ export function ExpressionModal({ onCancel = noop, onOk = noop, defaultValue, + value, dataSource, autoCompleteOptions, }: ExpressionModalProps) { - const [exp, setExp] = useState(defaultValue); + const [exp, setExp] = useState(value ?? defaultValue); + const [error, setError] = useState(''); const workspace = useWorkspace(); const onAction = useCallback( (action: string, args: unknown[]) => { @@ -196,6 +180,11 @@ export function ExpressionModal({ const handleExpInputChange = (val: string) => { setExp(val?.trim()); }; + + useEffect(() => { + setExp(value); + }, [value]); + return ( { onOk(exp); }} + okButtonProps={{ + disabled: !!error, + }} + bodyStyle={{ + padding: 0, + }} > { + setError(expressionValueValidate(exp)); + }} autoCompleteContext={evaluateContext} autoCompleteOptions={autoCompleteOptions} /> + {error ? 输入的表达式存在语法错误,请修改后再提交! : null} - + { if (!node.key) { diff --git a/packages/designer/src/toolbar/mode-switch.tsx b/packages/designer/src/toolbar/mode-switch.tsx index 3712e89..09d29fe 100644 --- a/packages/designer/src/toolbar/mode-switch.tsx +++ b/packages/designer/src/toolbar/mode-switch.tsx @@ -21,7 +21,8 @@ export const ModeSwitchTool = observer(() => { shape="ghost" selected={designer.activeView === 'code'} onClick={() => { - designer.setActiveView('code'); + designer.setActiveView('code'); // 切换到源码视图 + designer.setActiveSidebarPanel(''); // 关闭左侧面板 }} tip="源码视图" > diff --git a/packages/designer/src/widgets.ts b/packages/designer/src/widgets.ts index 99c0894..4dbb720 100644 --- a/packages/designer/src/widgets.ts +++ b/packages/designer/src/widgets.ts @@ -1,5 +1,11 @@ import { getValue, setValue } from '@music163/tango-helpers'; -import { ModeSwitchTool, PreviewTool, RouteSwitchTool, TogglePanelTool } from './toolbar'; +import { + HistoryTool, + ModeSwitchTool, + PreviewTool, + RouteSwitchTool, + TogglePanelTool, +} from './toolbar'; import { ComponentsPanel, DataSourcePanel, @@ -30,6 +36,7 @@ export function getWidget(key: string) { registerWidget('toolbar.modeSwitch', ModeSwitchTool); registerWidget('toolbar.preview', PreviewTool); +registerWidget('toolbar.history', HistoryTool); registerWidget('toolbar.routeSwitch', RouteSwitchTool); registerWidget('toolbar.togglePanel', TogglePanelTool); diff --git a/packages/helpers/src/helpers/string.ts b/packages/helpers/src/helpers/string.ts index e140abd..96822dc 100644 --- a/packages/helpers/src/helpers/string.ts +++ b/packages/helpers/src/helpers/string.ts @@ -72,7 +72,7 @@ export function parseDndTrackId(str: string) { return [parts[0], str]; } -type DndIdParsedType = { +interface DndIdParsedType { /** * 完整的 ID */ @@ -93,7 +93,7 @@ type DndIdParsedType = { * 序号 */ index?: string; -}; +} /** * 解析 dnd id @@ -178,7 +178,7 @@ export function isValidObjectString(str: string) { return false; } -const templatePattern = /^\{(.+)\}$/s; +const templatePattern = /^{(.+)}$/s; /** * 判断给定字符串是否为变量字符串 @@ -191,15 +191,30 @@ const templatePattern = /^\{(.+)\}$/s; */ export function isVariableString(str: string) { // 先检查是否是简单的对象 - // TODO: 是不是可以忽略这个检查 if (code2object(str)) { return false; } - // 排除简单对象后,在用正则匹配 + // 排除简单对象后,再用正则匹配 return templatePattern.test(str); } +/** + * 给输入代码加上花括号 + * @example foo -> {foo} + * @example "hello" => {"hello"} + * @example () => {} => {() => {}} + * + * @param code 输入代码 + * @returns 加上花括号后的代码 + */ +export function wrapCode(code: string) { + if (isVariableString(code)) { + return code; + } + return `{${code}}`; +} + /** * 是否为简单字符串,非变量字符串 * @param str diff --git a/packages/helpers/tests/helpers.test.ts b/packages/helpers/tests/helpers.test.ts index b6c9dfc..ec55e8e 100644 --- a/packages/helpers/tests/helpers.test.ts +++ b/packages/helpers/tests/helpers.test.ts @@ -43,6 +43,8 @@ describe('string', () => { expect(isVariableString('{"hello"}')).toBeTruthy(); expect(isVariableString('{ foo: "bar" }')).toBeFalsy(); expect(isVariableString('{{ foo: "bar" }}')).toBeTruthy(); + expect(isVariableString('{() => {}}')).toBeTruthy(); + expect(isVariableString('{() => { console.log(); }}')).toBeTruthy(); }); it('getVariableContent', () => { diff --git a/packages/setting-form/package.json b/packages/setting-form/package.json index 6e0c918..4243839 100644 --- a/packages/setting-form/package.json +++ b/packages/setting-form/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "@ant-design/icons": "^4.8.0", - "@music163/tango-core": "^0.2.1", "@music163/tango-helpers": "^0.1.3", "@music163/tango-ui": "^0.1.6", "antd": "^4.24.2", diff --git a/packages/setting-form/src/form-item.tsx b/packages/setting-form/src/form-item.tsx index a736a87..7a7cc8e 100644 --- a/packages/setting-form/src/form-item.tsx +++ b/packages/setting-form/src/form-item.tsx @@ -148,7 +148,7 @@ export function createFormItem(options: IFormItemCreateOptions) { size="s" shape="outline" type="primary" - tooltip="切换为 JS 表达式" + tooltip={isVariable ? '关闭 JS 表达式' : '使用 JS 表达式'} tooltipPlacement="left" selected={isVariable} onClick={() => toggleIsVariable()} diff --git a/packages/setting-form/src/form-model.tsx b/packages/setting-form/src/form-model.tsx index 17b62f5..0e4ed1f 100644 --- a/packages/setting-form/src/form-model.tsx +++ b/packages/setting-form/src/form-model.tsx @@ -11,9 +11,9 @@ export interface IFormModel { onChange: (name: string, value: any, field?: Field) => void; } -export type FormModelOptionsType = { +export interface FormModelOptionsType { onChange?: IFormModel['onChange']; -}; +} export class FormModel implements IFormModel { state = {}; @@ -141,21 +141,21 @@ class SubModel implements IFormModel { } } -type FieldCreateOptionsType = { +interface FieldCreateOptionsType { parent: IFormModel; name: string; -}; +} -type FieldConfigType = { +interface FieldConfigType { validate?: (value: any, field: Field, trigger: string) => string | Promise; -}; +} -type FieldStateType = { +interface FieldStateType { error?: any; validating?: boolean; cancelValidation?: () => void; [key: string]: any; -}; +} type FieldValidateTriggerType = '*' | 'blur' | 'change'; @@ -233,11 +233,12 @@ export class Field { this.state.validating = false; this.state.error = error; - this.parent?.onChange?.(this.name, this.value, this); - if (error) { return error; } + + // 校验错误,不触发 onChange + this.parent?.onChange?.(this.name, this.value, this); }), ); }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 2741559..5eda2f5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,14 +34,16 @@ }, "dependencies": { "@ant-design/icons": "^4.8.0", - "@codemirror/autocomplete": "^6.7.1", - "@codemirror/lang-javascript": "^6.1.2", - "@codemirror/search": "^6.2.3", + "@codemirror/autocomplete": "^6.10.2", + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lint": "^6.4.2", + "@codemirror/search": "^6.5.4", "@music163/tango-helpers": "^0.1.3", "@uiw/react-codemirror": "^4.19.3", "antd": "^4.24.2", "antd-easy-form": "^1.1.3", "coral-system": "^1.0.5", + "eslint-linter-browserify": "^8.51.0", "react-json-view": "^1.21.3", "react-monaco-editor-lite": "^1.2.3" }, diff --git a/packages/ui/src/input-code.tsx b/packages/ui/src/input-code.tsx index 6ea5754..62df5e9 100644 --- a/packages/ui/src/input-code.tsx +++ b/packages/ui/src/input-code.tsx @@ -1,10 +1,12 @@ import React, { useMemo } from 'react'; +import { Box, HTMLCoralProps } from 'coral-system'; import CodeMirror, { ReactCodeMirrorProps } from '@uiw/react-codemirror'; -import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; +import { javascript, javascriptLanguage, esLint } from '@codemirror/lang-javascript'; import { CompletionContext } from '@codemirror/autocomplete'; import { syntaxTree } from '@codemirror/language'; +import { linter, lintGutter } from '@codemirror/lint'; +import * as eslint from 'eslint-linter-browserify'; import { getValue } from '@music163/tango-helpers'; -import { Box, HTMLCoralProps } from 'coral-system'; const completePropertyAfter = ['PropertyName', '.', '?.']; const dontCompleteIn = [ @@ -15,6 +17,17 @@ const dontCompleteIn = [ 'PropertyDefinition', ]; +const eslintConfig = { + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + env: { + browser: true, + }, + rules: {}, +}; + function completeProperties(from: number, object: Object) { const options = []; for (const name in object) { @@ -116,6 +129,10 @@ export interface InputCodeProps extends ReactCodeMirrorProps { * 自定义补全的选项列表 */ autoCompleteOptions?: string[]; + /** + * 启用 ESLint + */ + enableESLint?: boolean; } export function InputCode({ @@ -126,6 +143,7 @@ export function InputCode({ autoCompleteOptions, showLineNumbers, showFoldGutter, + enableESLint = false, ...rest }: InputCodeProps) { const globalJavaScriptCompletions = useMemo(() => { @@ -136,15 +154,16 @@ export function InputCode({ }); }, [autoCompleteContext, autoCompleteOptions]); const { rootStyle, codeSetup } = useInputCode({ shape, status, showLineNumbers, showFoldGutter }); + const extensions = [javascript({ jsx: true }), globalJavaScriptCompletions]; + if (enableESLint) { + // @ts-ignore + extensions.push(lintGutter(), linter(esLint(new eslint.Linter(), eslintConfig))); + } return ( - + {suffix} @@ -166,7 +185,6 @@ function useInputCode({ border: 'solid', borderColor: status === 'error' ? 'error.60' : 'line.normal', borderRadius: 's', - px: 'm', }; lineNumbers = false; foldGutter = false; diff --git a/yarn.lock b/yarn.lock index 53e1aef..674b89b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,7 +1495,7 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.7.1": +"@codemirror/autocomplete@^6.0.0": version "6.9.0" resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz" integrity sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog== @@ -1505,6 +1505,16 @@ "@codemirror/view" "^6.6.0" "@lezer/common" "^1.0.0" +"@codemirror/autocomplete@^6.10.2": + version "6.10.2" + resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.10.2.tgz#d3a7e2a4ddd1e8a2992b1b17d4e7692542c71052" + integrity sha512-3dCL7b0j2GdtZzWN5j7HDpRAJ26ip07R4NGYz7QYthIYMiX8I4E4TNrYcdTayPJGeVQtd/xe7lWU4XL7THFb/w== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.17.0" + "@lezer/common" "^1.0.0" + "@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0": version "6.2.4" resolved "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.2.4.tgz" @@ -1515,16 +1525,16 @@ "@codemirror/view" "^6.0.0" "@lezer/common" "^1.0.0" -"@codemirror/lang-javascript@^6.1.2": - version "6.1.9" - resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz" - integrity sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w== +"@codemirror/lang-javascript@^6.2.1": + version "6.2.1" + resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.1.tgz#8068d44365d13cdb044936fb4e3483301c12ef95" + integrity sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A== dependencies: "@codemirror/autocomplete" "^6.0.0" "@codemirror/language" "^6.6.0" "@codemirror/lint" "^6.0.0" "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.0.0" + "@codemirror/view" "^6.17.0" "@lezer/common" "^1.0.0" "@lezer/javascript" "^1.0.0" @@ -1549,7 +1559,16 @@ "@codemirror/view" "^6.0.0" crelt "^1.0.5" -"@codemirror/search@^6.0.0", "@codemirror/search@^6.2.3": +"@codemirror/lint@^6.4.2": + version "6.4.2" + resolved "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.4.2.tgz#c13be5320bde9707efdc94e8bcd3c698abae0b92" + integrity sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/search@^6.0.0": version "6.5.1" resolved "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.1.tgz" integrity sha512-4jupk4JwkeVbrN2pStY74q6OJEYqwosB4koA66nyLeVedadtX9MHI38j2vbYmnfDGurDApP3OZO46MrWalcjiQ== @@ -1558,6 +1577,15 @@ "@codemirror/view" "^6.0.0" crelt "^1.0.5" +"@codemirror/search@^6.5.4": + version "6.5.4" + resolved "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.4.tgz#54005697bf581f7dccbbb4a0c34d3a7aa25a513a" + integrity sha512-YoTrvjv9e8EbPs58opjZKyJ3ewFrVSUzQ/4WXlULQLSDDr1nGPJ67mMXFNNVYwdFhybzhrzrtqgHmtpJwIF+8g== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + "@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0": version "6.2.1" resolved "https://registry.npmmirror.com/@codemirror/state/-/state-6.2.1.tgz" @@ -1582,6 +1610,15 @@ style-mod "^4.0.0" w3c-keyname "^2.2.4" +"@codemirror/view@^6.17.0": + version "6.21.3" + resolved "https://registry.npmmirror.com/@codemirror/view/-/view-6.21.3.tgz#cf8e3ee6f08e06a6912f18bc90548b4b74badb7a" + integrity sha512-8l1aSQ6MygzL4Nx7GVYhucSXvW4jQd0F6Zm3v9Dg+6nZEfwzJVqi4C2zHfDljID+73gsQrWp9TgHc81xU15O4A== + dependencies: + "@codemirror/state" "^6.1.4" + style-mod "^4.1.0" + w3c-keyname "^2.2.4" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.npmmirror.com/@colors/colors/-/colors-1.5.0.tgz" @@ -9294,6 +9331,11 @@ eslint-import-resolver-typescript@^3.6.0: is-core-module "^2.11.0" is-glob "^4.0.3" +eslint-linter-browserify@^8.51.0: + version "8.51.0" + resolved "https://registry.npmmirror.com/eslint-linter-browserify/-/eslint-linter-browserify-8.51.0.tgz#ab6a6a83b7ee0763b7f2eaa2d02fa4fc6b6e4ac1" + integrity sha512-N5wp3QVonbdDs6AImgiCjwpEtcApYCO2BI/lpjNYHT9MrqefL4uecPpyAxS0eF8Xuh8EFYH2sLjh5nLjpSrSsg== + eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: version "2.8.0" resolved "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz" @@ -18826,6 +18868,11 @@ style-mod@^4.0.0: resolved "https://registry.npmmirror.com/style-mod/-/style-mod-4.0.3.tgz" integrity sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw== +style-mod@^4.1.0: + version "4.1.0" + resolved "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.0.tgz#a313a14f4ae8bb4d52878c0053c4327fb787ec09" + integrity sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA== + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.npmmirror.com/style-to-object/-/style-to-object-0.3.0.tgz"