Compare commits

..

16 Commits

Author SHA1 Message Date
Wells c48a49961a chore: upgrade dependencies (#209)
* fix: check object value

* chore: up

* chore: upgrade mobx

* chore: up
2024-09-10 17:04:52 +08:00
Wells eae45003c6 feat!: refactor base class (#191)
* feat: refactor types

* feat: rename models

* fix: update types

* fix: init workspace and viewNode

* fix: update fileTypes

* fix: update

* fix: update
2024-09-10 16:02:53 +08:00
wwsun 1802fc0eb8 docs: update 2024-09-10 15:40:45 +08:00
wwsun 8a3ab3c093 chore(release): publish
- @music163/tango-context@1.1.10
 - @music163/tango-core@1.4.4
 - @music163/tango-designer@1.4.6
 - @music163/tango-helpers@1.2.4
 - @music163/tango-sandbox@1.0.13
 - @music163/tango-setting-form@1.2.15
 - @music163/tango-ui@1.4.5
2024-09-09 15:58:16 +08:00
Wells 55602fc961 fix: add classNameSetter and classNameInput (#207)
* feat: add basic classNameInput

* fix: add classNameSetter
2024-09-09 15:50:56 +08:00
864907600cc a3c0e463ca chore: update code editor (#205) 2024-09-09 15:08:02 +08:00
BoBoooooo 79f9a8c179 fix: service support set description field (#206)
* fix: service support set description field

* fix: undefined string bug

* fix: update title display
2024-09-09 15:07:38 +08:00
Wells ea96872261 fix: renderSetter with template (#202)
* fix: renderSetter with template

* fix: update types
2024-09-03 16:52:59 +08:00
wwsun a86e0e049c chore(release): publish
- @music163/tango-context@1.1.9
 - @music163/tango-core@1.4.3
 - @music163/tango-designer@1.4.5
 - @music163/tango-helpers@1.2.3
 - @music163/tango-sandbox@1.0.12
 - @music163/tango-setting-form@1.2.14
 - @music163/tango-ui@1.4.4
2024-09-02 10:50:59 +08:00
Wells c4e1ed67f5 fix: add prop value template & fix components popover (#201)
* fix: clear value in CodeSetter

* fix: update variable path in codesetter

* fix: add prop value template

* fix: update  recommendedList for ComponentsPopover

* fix: update icon size in line mode for ComponentsPanel
2024-08-30 18:09:01 +08:00
wwsun 179de0cc52 chore(release): publish
- @music163/tango-designer@1.4.4
 - @music163/tango-setting-form@1.2.13
 - @music163/tango-ui@1.4.3
2024-08-16 17:50:55 +08:00
BoBoooooo 6de54884b5 fix: CodeInput 支持 jsx css 代码补全 (#198)
* feat: code setter support jsx css completion

* fix: refactor expression css mode

* fix: update story

* fix: revert code
2024-08-16 11:16:39 +08:00
wwsun 55bf417ddf chore(release): publish
- @music163/tango-designer@1.4.3
 - @music163/tango-setting-form@1.2.12
 - @music163/tango-ui@1.4.2
2024-08-09 15:49:28 +08:00
BoBoooooo ca56edd9ae fix: file errors overlay support click to error file & optimize ui (#197) 2024-08-09 13:52:35 +08:00
Wells 65149f1326 fix: update mobile simulator ui (#196)
* fix: update mobile simulator styles

* docs: update

* fix: update icon
2024-08-08 11:20:20 +08:00
864907600cc 939466648d fix: route matching without query string & toggle button wrapping (#195)
* fix: fix route is not matched when url contains query string

* fix: fix mode switch button group wrapping

* fix: fix toggle button text wrapping
2024-08-07 15:51:36 +08:00
89 changed files with 9246 additions and 7591 deletions
+5 -5
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Group } from 'coral-system';
import { Box, Group, Text } from 'coral-system';
import { Avatar, Space, Switch } from 'antd';
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { registerSetter } from '@music163/tango-designer';
@@ -66,12 +66,12 @@ export function Logo() {
export function ProjectDetail() {
return (
<Box display="flex" alignItems="center" columnGap="l">
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
community-test
</Box>
<Box className="BranchName" as="code" fontSize="14px">
</Text>
<Text className="BranchName" as="code" fontSize="14px" truncated>
<BranchesOutlined /> feature/list
</Box>
</Text>
</Box>
);
}
+7 -1
View File
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
import {
Page,
Section,
Box,
Button,
Input,
FormilyForm,
@@ -167,7 +168,9 @@ class App extends React.Component {
render() {
return (
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
<Section tid="section0" />
<Section tid="section0">
<Box></Box>
</Section>
<Section tid="section1" title="Section Title">
your input: <Input tid="input1" defaultValue="hello" />
copy input: <Input value={tango.page.input1?.value} />
@@ -374,13 +377,16 @@ export default defineServices({
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
description: '新增用户'
},
update: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
description: '更新用户'
},
delete: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
description: '删除用户'
},
});
`;
+68 -2
View File
@@ -48,7 +48,6 @@ const Snippet3ColumnLayout: IComponentPrototype = {
icon: 'icon-column-3',
type: 'snippet',
package: '@music163/antd',
// FIXME: Column 组件 需要更新为 defineComponent
initChildren: `
<Columns columns={12}>
<Column colSpan={4}>
@@ -132,11 +131,14 @@ export const nativeDomPrototypes = () => {
title: '样式',
group: 'style',
setter: 'expressionSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'className',
title: '类名',
setter: 'textSetter',
setter: 'classNameSetter',
},
{
name: 'id',
@@ -166,6 +168,38 @@ const prototypes: Dict<IComponentPrototype> = {
Snippet2ColumnLayout,
Snippet3ColumnLayout,
SnippetButtonGroup,
Section: {
...basePrototypes['Section'],
props: [
// ...(basePrototypes['Section'].props as any),
{
name: 'title',
title: '标题',
setter: 'textSetter',
},
{
name: 'extra',
title: '额外内容',
setter: 'renderSetter',
options: [
{
label: '按钮组',
value: 'ButtonGroup',
renderBody:
'<ButtonGroup><Button>button1</Button><Button>button2</Button></ButtonGroup>',
relatedImports: ['ButtonGroup', 'Button'],
},
],
template: '(v) => {\n return {{content}};\n}',
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
group: 'style',
},
],
},
Box: {
name: 'Box',
title: '盒子',
@@ -197,6 +231,38 @@ const prototypes: Dict<IComponentPrototype> = {
title: 'd',
setter: 'textSetter',
},
{
name: 'onClick',
title: '点击事件',
setter: 'eventSetter',
template: '(e) => {\n {{content}}\n}',
tip: '回调参数说明:e 为事件对象',
},
{
name: 'renderFoo',
title: 'foo自定义渲染',
setter: 'renderSetter',
options: [
{
label: '占位空间',
value: 'Box',
render: '() => <Box>test</Box>',
relatedImports: ['Box'],
},
{
label: '占位文本',
value: 'Text',
render: '() => <Text>text</Text>',
relatedImports: ['Text'],
},
],
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
group: 'style',
},
],
},
Columns: {
+1 -1
View File
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
import 'antd/dist/antd.css';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: '^on.*' },
controls: {
matchers: {
color: /(background|color)$/i,
+1 -1
View File
@@ -16,7 +16,7 @@
"dependencies": {
"@music163/tango-setting-form": "*",
"@music163/tango-ui": "*",
"mobx": "6.12.3",
"mobx": "6.13.2",
"mobx-react-lite": "4.0.7"
},
"devDependencies": {
@@ -69,6 +69,14 @@ const prototypeHasBasicProps: IComponentPrototype = {
title: 'codeSetter',
setter: 'codeSetter',
},
{
name: 'style',
title: 'codeSetter(cssObject)',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'text',
title: 'textSetter',
@@ -268,6 +276,9 @@ export function InitValues() {
name: 'style',
title: 'codeSetter',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'object',
@@ -0,0 +1,29 @@
import React from 'react';
import { ActionSelect } from '@music163/tango-ui';
export default {
title: 'UI/ActionSelect',
component: ActionSelect,
};
const options = [
{ label: 'action1', value: 'action1' },
{ label: 'action2', value: 'action2' },
{ label: 'action3', value: 'action3' },
];
export const Basic = {
args: {
defaultText: '选择动作',
options,
onSelect: console.log,
},
};
export const showInput = {
args: {
text: '选择动作',
options,
showInput: true,
},
};
@@ -0,0 +1,15 @@
import React, { useState } from 'react';
import { ClassNameInput } from '@music163/tango-ui';
export default {
title: 'UI/ClassNameInput',
};
export function Basic() {
return <ClassNameInput defaultValue="hello world" onChange={console.log} />;
}
export function Controlled() {
const [value, setValue] = useState('');
return <ClassNameInput value={value} onChange={setValue} />;
}
+5 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Action, InputCode } from '@music163/tango-ui';
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
import { BlockOutlined } from '@ant-design/icons';
export default {
@@ -28,6 +28,10 @@ export function Basic() {
);
}
export function CSS() {
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
}
const code = `
function foo() {
console.log("test");
+4 -4
View File
@@ -23,9 +23,9 @@
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.23.6",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@types/jest": "^29.5.7",
@@ -58,7 +58,7 @@
"react": "^17.0.0",
"react-dom": "^17.0.0",
"styled-components": "^5.3.6",
"typedoc": "^0.25.4",
"typedoc": "^0.26.7",
"typescript": "^5.3.3"
},
"engines": {
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.10](https://github.com/netease/tango/compare/@music163/tango-context@1.1.9...@music163/tango-context@1.1.10) (2024-09-09)
### Bug Fixes
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
## [1.1.9](https://github.com/netease/tango/compare/@music163/tango-context@1.1.8...@music163/tango-context@1.1.9) (2024-09-02)
**Note:** Version bump only for package @music163/tango-context
## [1.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
**Note:** Version bump only for package @music163/tango-context
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "1.1.8",
"version": "1.1.10",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,8 +31,8 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^1.4.2",
"@music163/tango-helpers": "^1.2.2",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
+15 -8
View File
@@ -1,4 +1,4 @@
import type { Engine } from '@music163/tango-core';
import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
export interface ITangoEngineContext {
@@ -21,8 +21,12 @@ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>
export { TangoEngineProvider };
/**
* 获取 CodeWorkspace 实例
* @returns
*/
export const useWorkspace = () => {
return useTangoEngine()?.engine.workspace;
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
};
export const useDesigner = () => {
@@ -90,12 +94,15 @@ export const useWorkspaceData = () => {
key: prefix,
selectable: false,
showAddButton: true,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
})),
children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
const title = value.description ? `${key}(${value.description})` : key;
return {
title,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
};
}),
});
});
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-core@1.4.3...@music163/tango-core@1.4.4) (2024-09-09)
**Note:** Version bump only for package @music163/tango-core
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-core@1.4.2...@music163/tango-core@1.4.3) (2024-09-02)
**Note:** Version bump only for package @music163/tango-core
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
### Bug Fixes
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "1.4.2",
"version": "1.4.4",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -24,14 +24,14 @@
"prepublishOnly": "yarn build"
},
"dependencies": {
"@babel/generator": "^7.23.5",
"@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5",
"@music163/tango-helpers": "^1.2.2",
"@babel/generator": "^7.25.6",
"@babel/parser": "^7.25.6",
"@babel/traverse": "^7.25.6",
"@babel/types": "^7.25.6",
"@music163/tango-helpers": "^1.2.4",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.3",
"@types/babel__traverse": "^7.20.6",
"mobx": "6.13.2",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
import { MenuDataType } from '@music163/tango-helpers';
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
import { IWorkspace } from './models/interfaces';
import { AbstractWorkspace } from './models/abstract-workspace';
interface ICreateEngineOptions {
/**
* 自定义工作区
*/
workspace?: IWorkspace;
workspace?: AbstractWorkspace;
/**
* 菜单信息
*/
+2 -2
View File
@@ -28,7 +28,7 @@ import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type {
IRouteData,
IStorePropertyData,
ITangoViewNodeData,
IViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierData,
@@ -1234,7 +1234,7 @@ export function cloneJSXElement(node: t.JSXElement, overrideProps?: Dict) {
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
const nodes: Array<IViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true);
const cleanAst = clearTrackingData(cloneAst);
const variables: string[] = []; // 使用的 tango 变量
+13 -21
View File
@@ -8,65 +8,57 @@ import { FileType } from './../types';
export function inferFileType(filename: string): FileType {
// 增加 tangoConfigJson Module
if (/\/tango\.config\.json$/.test(filename)) {
return FileType.TangoConfigJson;
return FileType.TangoConfigJsonFile;
}
if (/\/appJson\.json$/.test(filename)) {
return FileType.AppJson;
return FileType.AppJsonFile;
}
if (/\/package\.json$/.test(filename)) {
return FileType.PackageJson;
return FileType.PackageJsonFile;
}
if (/\/routes\.js$/.test(filename)) {
return FileType.RouteModule;
return FileType.JsRouteConfigFile;
}
// 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.jsx?$/.test(filename)) {
return FileType.JsxViewModule;
return FileType.JsViewFile;
}
// 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
return FileType.JsonViewModule;
return FileType.JsonViewFile;
}
if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.ComponentsEntryModule;
return FileType.JsLocalComponentsEntryFile;
}
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
return FileType.JsServiceFile;
}
if (/service\.js$/.test(filename)) {
return FileType.ServiceModule;
return FileType.JsServiceFile;
}
if (/\/stores\/index\.js$/.test(filename)) {
return FileType.StoreEntryModule;
return FileType.JsStoreEntryFile;
}
if (/\/stores\/.+\.js$/.test(filename)) {
return FileType.StoreModule;
return FileType.JsStoreFile;
}
if (/\.jsx?$/.test(filename)) {
return FileType.Module;
return FileType.JsFile;
}
if (/\.json$/.test(filename)) {
return FileType.Json;
}
if (/\.less$/.test(filename)) {
return FileType.Less;
}
if (/\.scss$/.test(filename)) {
return FileType.Scss;
return FileType.JsonFile;
}
return FileType.File;
@@ -0,0 +1,264 @@
import {
Dict,
isStoreVariablePath,
parseServiceVariablePath,
parseStoreVariablePath,
} from '@music163/tango-helpers';
import { inferFileType, getFilepath } from '../helpers';
import { TangoFile } from './file';
import { FileType } from '../types';
import { JsRouteConfigFile } from './js-route-config-file';
import { JsStoreEntryFile } from './js-store-entry-file';
import { JsServiceFile } from './js-service-file';
import { JsViewFile } from './js-view-file';
import { JsLocalComponentsEntryFile } from './js-local-components-entry-file';
import { JsAppEntryFile } from './js-app-entry-file';
import { JsFile } from './js-file';
import { AbstractWorkspace, IWorkspaceInitConfig } from './abstract-workspace';
import { JsonFile } from './json-file';
import { JsStoreFile } from './js-store-file';
/**
* CodeWorkspace 抽象基类
*/
export abstract class AbstractCodeWorkspace extends AbstractWorkspace {
/**
* 模型入口配置模块
*/
storeEntryModule: JsStoreEntryFile;
/**
* 状态管理模块
*/
storeModules: Record<string, JsStoreFile>;
/**
* 数据服务模块
*/
serviceModules: Record<string, JsServiceFile>;
constructor(options: IWorkspaceInitConfig) {
super(options);
this.storeModules = {};
this.serviceModules = {};
if (options?.files) {
this.addFiles(options.files);
}
}
/**
* 添加文件到工作区
* @param filename 文件名
* @param code 代码片段
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!fileType && filename === this.entry) {
fileType = FileType.JsAppEntryFile;
}
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.JsAppEntryFile:
module = new JsAppEntryFile(this, props);
this.jsAppEntryFile = module;
break;
case FileType.JsStoreEntryFile:
module = new JsStoreEntryFile(this, props);
this.storeEntryModule = module;
break;
case FileType.JsLocalComponentsEntryFile:
module = new JsLocalComponentsEntryFile(this, props);
this.componentsEntryModule = module;
break;
case FileType.JsRouteConfigFile: {
module = new JsRouteConfigFile(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
}
case FileType.JsViewFile:
module = new JsViewFile(this, props);
break;
case FileType.JsServiceFile:
module = new JsServiceFile(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.JsStoreFile:
module = new JsStoreFile(this, props);
this.storeModules[module.name] = module;
break;
case FileType.JsFile:
module = new JsFile(this, props);
break;
case FileType.PackageJsonFile:
module = new JsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJsonFile:
module = new JsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.JsonFile:
module = new JsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
}
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.JsServiceFile);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
/**
* 添加新的模型文件
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
this.addStoreFile(name, code);
}
/**
* 删除模型文件
* @param name
*/
removeStoreModule(name: string) {
const filename = getFilepath(name, '/src/stores', '.js');
this.storeEntryModule.removeStore(name).update();
this.removeFile(filename);
}
/**
* 添加模型属性
* @param storeName
* @param stateName
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
* 删除模型属性
* @param storeName
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
*/
listServiceFunctions() {
const ret: Record<string, Dict> = {};
Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/**
* 更新服务函数
*/
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
}
+63
View File
@@ -0,0 +1,63 @@
import { AbstractWorkspace } from './abstract-workspace';
import type { FileType, IFileConfig } from '../types';
/**
* 普通文件抽象基类,不进行 AST 解析
*/
export abstract class AbstractFile {
readonly workspace: AbstractWorkspace;
/**
* 文件名
*/
readonly filename: string;
/**
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
get code() {
return this._code;
}
// FIXME: cleanCode 是不是只有 viewFile 有 ????
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
this.filename = props.filename;
this.type = props.type;
this.lastModified = Date.now();
this.isError = false;
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
if (isSyncCode) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
abstract update(code?: string): void;
}
@@ -1,5 +1,4 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import {
code2ast,
@@ -9,16 +8,16 @@ import {
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { TangoFile } from './file';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
/**
* JS
* JS
* - ast this
* - observable state _foo getter
*/
export class TangoModule extends TangoFile {
export abstract class AbstractJsFile extends AbstractFile {
ast: t.File;
/**
@@ -31,7 +30,7 @@ export class TangoModule extends TangoFile {
*/
importList: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
@@ -137,24 +136,3 @@ export class TangoModule extends TangoFile {
this.importList = imports;
}
}
/**
* JS
*/
export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
}
@@ -0,0 +1,106 @@
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { formatCode } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
export abstract class AbstractJsonFile extends AbstractFile {
_object;
abstract get json(): object;
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._object = {};
this.update(props.code);
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
@@ -0,0 +1,59 @@
import { Dict } from '@music163/tango-helpers';
import { AbstractFile } from './abstract-file';
export interface IViewNodeInitConfig<RawNodeType = unknown, ViewFileType = AbstractFile> {
id: string;
component: string;
rawNode: RawNodeType;
file: ViewFileType;
}
export abstract class AbstractViewNode<RawNodeType = unknown, ViewFileType = AbstractFile> {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: RawNodeType;
/**
* 节点所属的文件对象
*/
file: ViewFileType;
/**
* 节点所属的文件对象
*/
props: Record<string, any>;
/**
* 节点的位置信息
*/
abstract get loc(): unknown;
constructor(props: IViewNodeInitConfig<RawNodeType, ViewFileType>) {
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.file = props.file;
}
/**
* 销毁当前节点,清空文件和节点的关联关系
*/
destroy() {
this.file = undefined;
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns 返回克隆的原始节点
*/
abstract cloneRawNode(overrideProps?: Dict): RawNodeType;
}
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { MenuDataType } from '@music163/tango-helpers';
import { AbstractWorkspace } from './abstract-workspace';
export type SimulatorNameType = 'desktop' | 'phone';
@@ -18,7 +18,7 @@ interface IViewportBounding {
}
interface IDesignerOptions {
workspace: IWorkspace;
workspace: AbstractWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
/**
* 菜单配置
@@ -108,7 +108,7 @@ export class Designer {
*/
_menuData?: MenuDataType = null;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get simulator(): ISimulatorType {
return toJS(this._simulator);
+3 -3
View File
@@ -1,7 +1,7 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers';
import { DropTarget } from './drop-target';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
/**
* 拖拽来源类,被拖拽的物体
@@ -22,7 +22,7 @@ export class DragSource {
*/
dropTarget: DropTarget;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get node() {
return this.workspace.getNode(this.data?.id, this.data?.filename);
@@ -47,7 +47,7 @@ export class DragSource {
return this.data?.bounding;
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
this.data = null;
this.isDragging = false;
+3 -3
View File
@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
export enum DropMethod {
ReplaceNode = 'replaceNode', // 替换节点
@@ -23,7 +23,7 @@ export class DropTarget {
*/
data: ISelectedItemData;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get node() {
return this.workspace.getNode(this.data.id, this.data.filename);
@@ -48,7 +48,7 @@ export class DropTarget {
return this.data?.display;
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
this.method = DropMethod.InsertAfter;
this.data = null;
+2 -2
View File
@@ -1,5 +1,5 @@
import { AbstractWorkspace } from './abstract-workspace';
import { Designer } from './designer';
import { IWorkspace } from './interfaces';
/**
* 设计器引擎
@@ -8,7 +8,7 @@ export class Engine {
/**
* 工作区状态
*/
workspace: IWorkspace;
workspace: AbstractWorkspace;
/**
* 设计器状态
*/
+16 -194
View File
@@ -1,66 +1,22 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { FileType, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { formatCode } from '../helpers';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
/**
* 普通文件,不进行 AST 解析
*/
export class TangoFile {
readonly workspace: IWorkspace;
/**
* 文件名
*/
readonly filename: string;
/**
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
get code() {
return this._code;
export class TangoFile extends AbstractFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
this.filename = props.filename;
this.type = props.type;
this.lastModified = Date.now();
this.isError = false;
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
if (isSyncCode) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
update(code?: string) {
if (!isNil(code)) {
this.lastModified = Date.now();
@@ -70,137 +26,3 @@ export class TangoFile {
this.workspace.onFilesChange([this.filename]);
}
}
export class TangoLessFile extends TangoFile {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
}
export class TangoJsonFile extends TangoFile {
_object = {};
/**
* @deprecated 使用 file.json 代替
*/
get object() {
return toJS(this._object);
}
get json() {
return toJS(this._object);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
_object: observable,
code: computed,
cleanCode: computed,
object: computed,
json: computed,
update: action,
setValue: action,
});
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
export enum HistoryMessage {
InitView = 'initView',
@@ -43,7 +43,7 @@ export class TangoHistory {
// 最多记录数
_maxSize = 100;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get index() {
return this._index;
@@ -65,7 +65,7 @@ export class TangoHistory {
return this._records.length > this._index + 1;
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
makeObservable(this, {
+21 -13
View File
@@ -1,17 +1,25 @@
export * from './engine';
export * from './workspace';
export * from './abstract-workspace';
export * from './abstract-code-workspace';
export * from './abstract-file';
export * from './abstract-js-file';
export * from './abstract-json-file';
export * from './abstract-view-node';
export * from './designer';
export * from './drop-target';
export * from './select-source';
export * from './drag-source';
export * from './history';
export * from './drop-target';
export * from './engine';
export * from './file';
export * from './module';
export * from './entry-module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './component-module';
export * from './node';
export * from './history';
export * from './interfaces';
export * from './js-app-entry-file';
export * from './js-file';
export * from './js-local-components-entry-file';
export * from './js-route-config-file';
export * from './js-service-file';
export * from './js-store-entry-file';
export * from './js-store-file';
export * from './js-view-file';
export * from './json-file';
export * from './select-source';
export * from './view-node';
export * from './workspace';
+20 -260
View File
@@ -1,28 +1,17 @@
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers';
import { TangoHistory } from './history';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { Dict } from '@music163/tango-helpers';
import {
IFileConfig,
FileType,
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IImportSpecifierSourceData,
IImportSpecifierData,
IFileError,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { IdGenerator } from '../helpers';
import { AppEntryModule } from './entry-module';
import { AbstractViewNode } from './abstract-view-node';
export interface IViewFile {
readonly workspace: IWorkspace;
readonly filename: string;
readonly type: FileType;
/**
* 文件名
*/
filename: string;
/**
* ID 生成器
@@ -55,267 +44,38 @@ export interface IViewFile {
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode;
getNode: (targetNodeId: string) => AbstractViewNode;
removeNode: (targetNodeId: string) => IViewFile;
removeNode: (targetNodeId: string) => this;
insertChild: (
targetNodeId: string,
newNode: any,
position?: InsertChildPositionType,
) => IViewFile;
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
insertAfter: (targetNodeId: string, newNode: any) => this;
insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
insertBefore: (targetNodeId: string, newNode: any) => this;
replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
replaceNode: (targetNodeId: string, newNode: any) => this;
replaceViewChildren: (rawNodes: any[]) => IViewFile;
replaceViewChildren: (rawNodes: any[]) => this;
updateNodeAttribute: (
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) => IViewFile;
) => this;
updateNodeAttributes: (
nodeId: string,
config: Record<string, any>,
relatedImports?: string[],
) => IViewFile;
) => this;
get code(): string;
get nodes(): Map<string, IViewNode>;
get nodes(): Map<string, AbstractViewNode>;
get nodesTree(): object[];
get tree(): any;
}
export interface IViewNode {
/**
* 所属的文件
*/
file: IViewFile;
/**
* 节点 ID
*/
readonly id: string;
/**
* 对应的组件
*/
readonly component: string;
/**
* 原始节点对象
*/
readonly rawNode: unknown;
/**
* 属性集合
*/
readonly props: Record<string, any>;
/**
* 克隆原始节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode: (overrideProps?: Dict) => unknown;
/**
* 销毁节点
* @returns
*/
destroy: () => void;
/**
* 原始节点的位置信息
*/
get loc(): unknown;
}
export interface IWorkspace {
history: TangoHistory;
selectSource: SelectSource;
dragSource: DragSource;
files: Map<string, TangoFile>;
componentPrototypes: Map<string, IComponentPrototype>;
entry: string;
activeFile: string;
activeViewFile: string;
activeRoute: string;
/**
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
*/
tangoConfigJson: TangoJsonFile;
/**
* app.js 入口文件解析后的模块
*/
appEntryModule: AppEntryModule;
/**
* 解析后的路由模块
*/
routeModule?: TangoRouteModule;
/**
* 解析后的状态管理模块 Map
*/
storeModules?: Record<string, TangoStoreModule>;
/**
* 解析后的服务模块 Map
*/
serviceModules?: Record<string, TangoServiceModule>;
ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
// ----------------- 文件操作 -----------------
addFiles: (files: IFileConfig[]) => void;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addServiceFile: (serviceName: string, code: string) => void;
addStoreFile: (storeName: string, code: string) => void;
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
/**
* 更新文件
* @param filename 文件名
* @param code 代码
* @param isSyncAst 是否同步 ast
*/
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
/**
* 检查并同步文件的 ast
*/
syncFiles: () => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
/**
* 文件变化回调
* @param filenames 文件名列表
*/
onFilesChange: (filenames: string[]) => void;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
dropNode: () => void;
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => {
name: string;
moduleName: string;
config: Dict<object>;
};
listServiceFunctions?: () => Dict<object>;
removeServiceFunction?: (serviceKey: string) => void;
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreModule?: (storeName: string) => void;
removeStoreVariable?: (variablePath: string) => void;
updateStoreVariable?: (variablePath: string, code: string) => void;
// ----------------- 视图文件操作 -----------------
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: (
name: string,
version: string,
options?: {
package?: ITangoConfigPackages;
[x: string]: any;
},
) => void;
removeDependency?: (name: string) => void;
addBizComp?: (
name: string,
version: string,
options?: {
package?: ITangoConfigPackages;
[x: string]: any;
},
) => void;
removeBizComp?: (name: string) => void;
// ----------------- getter -----------------
/**
* 解析后的项目配置信息
*/
get projectConfig(): ITangoConfigJson;
/**
* 当前活动的视图文件
*/
get activeViewModule(): IViewFile;
get pages(): any[];
get bizComps(): string[];
get baseComps(): string[];
get localComps(): string[];
get fileErrors(): IFileError[];
/**
* 是否是有效的项目
* - 包含 tango.config.json
* - 包含视图模块
* - 没有文件错误
*/
get isValid(): boolean;
/**
* 文件中的代码
*/
get code(): string;
}
@@ -1,12 +1,12 @@
import { traverseEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractJsFile } from './abstract-js-file';
import { AbstractWorkspace } from './abstract-workspace';
export class AppEntryModule extends TangoModule {
export class JsAppEntryFile extends AbstractJsFile {
routerType: string;
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
}
+25
View File
@@ -0,0 +1,25 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* 普通 JS 文件
*/
export class JsFile extends AbstractJsFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
}
@@ -1,17 +1,17 @@
import path from 'path';
import { action, computed, makeObservable, observable } from 'mobx';
import { TangoModule } from './module';
import { IWorkspace } from './interfaces';
import { IExportSpecifierData, IFileConfig } from '../types';
import { traverseComponentsEntryFile } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* '/components/index.js' `/blocks/index.js`
*/
export class TangoComponentsEntryModule extends TangoModule {
export class JsLocalComponentsEntryFile extends AbstractJsFile {
exportList: Record<string, IExportSpecifierData>;
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
@@ -7,20 +7,20 @@ import {
updateRouteToRouteFile,
} from '../helpers';
import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractJsFile } from './abstract-js-file';
import { AbstractCodeWorkspace } from './abstract-code-workspace';
/**
*
*/
export class TangoRouteModule extends TangoModule {
export class JsRouteConfigFile extends AbstractJsFile {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractCodeWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
@@ -8,13 +8,13 @@ import {
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
*
*/
export class TangoServiceModule extends TangoModule {
export class JsServiceFile extends AbstractJsFile {
/**
* index
*/
@@ -39,7 +39,7 @@ export class TangoServiceModule extends TangoModule {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
@@ -0,0 +1,56 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { traverseStoreEntryFile, addStoreToEntryFile, removeStoreToEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* stores 入口文件
*/
export class JsStoreEntryFile extends AbstractJsFile {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
@@ -1,73 +1,19 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { action, computed, makeObservable, observable } from 'mobx';
import {
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
getModuleNameByFilename,
addStoreState,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
} from '../helpers';
import { IFileConfig, IStorePropertyData } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
*
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
*
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
*
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
*
*/
export class TangoStoreModule extends TangoModule {
export class JsStoreFile extends AbstractJsFile {
/**
*
*/
@@ -79,7 +25,7 @@ export class TangoStoreModule extends TangoModule {
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
@@ -20,18 +20,19 @@ import {
addImportDeclarationLegacy,
updateImportDeclarationLegacy,
} from '../helpers';
import { TangoNode } from './node';
import { JsxViewNode } from './view-node';
import {
IFileConfig,
ITangoViewNodeData,
IViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierSourceData,
ImportDeclarationDataType,
IImportSpecifierData,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractWorkspace } from './abstract-workspace';
import { IViewFile } from './interfaces';
import { AbstractJsFile } from './abstract-js-file';
/**
* -> map
@@ -56,8 +57,8 @@ function buildImportMap(importedModules: ImportDeclarationDataType) {
* tree data
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
function nodeListToTreeData(list: IViewNodeData[]) {
const map: Record<string, IViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
@@ -82,9 +83,9 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
/**
*
*/
export class TangoViewModule extends TangoModule implements IViewFile {
export class JsViewFile extends AbstractJsFile implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[] = [];
_nodesTree: IViewNodeData[] = [];
/**
*
*/
@@ -108,7 +109,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* <id, Node>
*/
private _nodes: Map<string, TangoNode>;
private _nodes: Map<string, JsxViewNode>;
/**
*
* @deprecated
@@ -127,7 +128,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this.idGenerator = new IdGenerator({ prefix: props.filename });
@@ -176,8 +177,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._codeIdList = [];
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
const node = new JsxViewNode({
id: cur.id,
component: cur.component,
rawNode: cur.rawNode,
file: this,
});
this._nodes.set(cur.id, node);
+24
View File
@@ -0,0 +1,24 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import type { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsonFile } from './abstract-json-file';
export class JsonFile extends AbstractJsonFile {
get json(): object {
return toJS(this._object);
}
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
_object: observable,
code: computed,
cleanCode: computed,
json: computed,
update: action,
setValue: action,
});
}
}
-62
View File
@@ -1,62 +0,0 @@
import { JSXElement, SourceLocation } from '@babel/types';
import { Dict } from '@music163/tango-helpers';
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
file: TangoViewModule;
};
/**
* 视图节点类
*/
export class TangoNode implements IViewNode {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: JSXElement;
/**
* 节点所属的文件对象
*/
file: TangoViewModule;
props: Record<string, any>;
get loc(): SourceLocation {
return this.rawNode?.loc;
}
constructor(props: TangoNodeConstructorPropsType) {
this.file = props.file;
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode(overrideProps?: Dict) {
return cloneJSXElement(this.rawNode, overrideProps);
}
/**
* 清空节点的指向,交给 GC 去回收
*/
destroy() {
this.file = null;
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { ISelectedItemData, MousePoint } from '@music163/tango-helpers';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IViewFile, IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
import { IViewFile } from './interfaces';
type StartDataType = {
point: MousePoint;
@@ -24,7 +25,7 @@ export class SelectSource {
element: null,
};
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get start() {
return toJS(this._start);
@@ -74,7 +75,7 @@ export class SelectSource {
.filter((node) => !!node);
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
makeObservable(this, {
_items: observable,
+28
View File
@@ -0,0 +1,28 @@
import { JSXElement } from '@babel/types';
import { Dict } from '@music163/tango-helpers';
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
import { JsViewFile } from './js-view-file';
import { AbstractViewNode, IViewNodeInitConfig } from './abstract-view-node';
/**
* 视图节点类
*/
export class JsxViewNode extends AbstractViewNode<JSXElement, JsViewFile> {
get loc() {
return this.rawNode?.loc;
}
constructor(props: IViewNodeInitConfig<JSXElement, JsViewFile>) {
super(props);
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode(overrideProps?: Dict) {
return cloneJSXElement(this.rawNode, overrideProps);
}
}
File diff suppressed because it is too large Load Diff
+19 -31
View File
@@ -6,40 +6,28 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
* 文件类型枚举
*/
export enum FileType {
// js 文件
Module = 'module',
AppEntryModule = 'appEntryModule',
StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule',
ServiceModule = 'serviceModule',
StoreModule = 'storeModule',
File = 'file',
JsFile = 'jsFile',
JsAppEntryFile = 'jsAppEntryFile',
JsRouteConfigFile = 'jsRouteConfigFile',
JsStoreEntryFile = 'jsStoreEntryFile',
JsStoreFile = 'jsStoreFile',
JsServiceFile = 'jsServiceFile',
JsLocalComponentsEntryFile = 'jsLocalComponentsEntryFile',
JsViewFile = 'jsViewFile',
JsonViewFile = 'jsonViewFile',
JsonFile = 'jsonFile',
PackageJsonFile = 'packageJsonFile',
TangoConfigJsonFile = 'tangoConfigJsonFile',
AppJsonFile = 'appJsonFile',
// 组件配置文件
ComponentPrototypeModule = 'componentPrototypeModule',
// 组件运行调试入口文件,一般为 `/app.js`
ComponentDemoEntryModule = 'componentDemoEntryModule',
/**
* 本地组件目录的入口文件
*/
ComponentsEntryModule = 'componentsEntryModule',
/**
* @deprecated 已废弃
*/
BlockEntryModule = 'blockEntryModule',
// jsx 类型视图文件
JsxViewModule = 'jsxViewModule',
// json 类型视图文件
JsonViewModule = 'jsonViewModule',
// 非 js 文件
PackageJson = 'packageJson',
TangoConfigJson = 'tangoConfigJson',
AppJson = 'appJson',
File = 'file',
Json = 'json',
Less = 'less',
Scss = 'scss',
}
export interface IFileConfig {
@@ -65,7 +53,7 @@ export interface IFileError {
/**
* 视图节点数据类型
*/
export interface ITangoViewNodeData<T = JSXElement> {
export interface IViewNodeData<T = JSXElement> {
/**
* 节点 ID
*/
@@ -97,7 +85,7 @@ export interface ITangoViewNodeData<T = JSXElement> {
/**
* 子节点列表
*/
children?: Array<ITangoViewNodeData<T>>;
children?: Array<IViewNodeData<T>>;
}
/**
+5 -5
View File
@@ -179,12 +179,12 @@ describe('string helpers', () => {
});
it('inferFileType', () => {
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsxViewModule);
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsxViewModule);
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsViewFile);
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsViewFile);
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
expect(inferFileType('/src/index.scss')).toBe(FileType.Scss);
expect(inferFileType('/src/index.less')).toBe(FileType.Less);
expect(inferFileType('/src/index.json')).toBe(FileType.Json);
expect(inferFileType('/src/index.scss')).toBe(FileType.File);
expect(inferFileType('/src/index.less')).toBe(FileType.File);
expect(inferFileType('/src/index.json')).toBe(FileType.JsonFile);
});
});
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.5...@music163/tango-designer@1.4.6) (2024-09-09)
### Bug Fixes
- add classNameSetter and classNameInput ([#207](https://github.com/netease/tango/issues/207)) ([55602fc](https://github.com/netease/tango/commit/55602fc961b5c20d923891ce13d7a51588bd047f))
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.4...@music163/tango-designer@1.4.5) (2024-09-02)
### Bug Fixes
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.3...@music163/tango-designer@1.4.4) (2024-08-16)
### Bug Fixes
- CodeInput 支持 jsx css 代码补全 ([#198](https://github.com/netease/tango/issues/198)) ([6de5488](https://github.com/netease/tango/commit/6de54884b50d683c74d695a8c13184dc0ce8d3c4))
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.2...@music163/tango-designer@1.4.3) (2024-08-09)
### Bug Fixes
- file errors overlay support click to error file & optimize ui ([#197](https://github.com/netease/tango/issues/197)) ([ca56edd](https://github.com/netease/tango/commit/ca56edd9ae201dbeb84b03ad390a09085433a257))
- route matching without query string & toggle button wrapping ([#195](https://github.com/netease/tango/issues/195)) ([9394666](https://github.com/netease/tango/commit/939466648d40331800f15e886256a0789b7e7623))
- update mobile simulator ui ([#196](https://github.com/netease/tango/issues/196)) ([65149f1](https://github.com/netease/tango/commit/65149f1326eaf78e8a27222993c3cacc674cd378))
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.1...@music163/tango-designer@1.4.2) (2024-08-06)
**Note:** Version bump only for package @music163/tango-designer
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "1.4.2",
"version": "1.4.6",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.2.0",
"@music163/tango-context": "^1.1.8",
"@music163/tango-core": "^1.4.2",
"@music163/tango-helpers": "^1.2.2",
"@music163/tango-sandbox": "^1.0.11",
"@music163/tango-setting-form": "^1.2.11",
"@music163/tango-ui": "^1.4.1",
"@music163/tango-context": "^1.1.10",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"@music163/tango-sandbox": "^1.0.13",
"@music163/tango-setting-form": "^1.2.15",
"@music163/tango-ui": "^1.4.5",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.5.1",
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
// 推荐使用的子组件
const insertedList = useMemo(
() =>
Array.isArray(prototype?.childrenName)
? prototype?.childrenName
: [prototype?.childrenName].filter(Boolean),
[prototype?.childrenName],
);
// 推荐使用的代码片段
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
const recommendedList = useMemo(() => {
if (type === 'inner') {
return prototype?.childrenName
? Array.isArray(prototype?.childrenName)
? prototype.childrenName
: [prototype.childrenName]
: [];
}
// 默认推荐使用相同类型的组件作为兄弟节点
return prototype.siblingNames || [prototype.name];
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
const tipsTextMap = useMemo(
() => ({
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
const menuList = JSON.parse(JSON.stringify(designer.menuData));
const commonList = menuList['common'] ?? [];
if (commonList?.length && siblingList?.length) {
commonList.unshift({
title: '代码片段',
items: siblingList,
});
}
if (commonList?.length && insertedList?.length) {
if (commonList?.length && recommendedList.length) {
commonList.unshift({
title: '推荐使用',
items: insertedList,
items: recommendedList,
});
}
return menuList;
}, [insertedList, siblingList, designer.menuData]);
}, [recommendedList, designer.menuData]);
const innerTypeProps =
// 手动触发 适用于 点击添加组件
@@ -1,11 +1,39 @@
import React, { useState } from 'react';
import { Box, css } from 'coral-system';
import { observer, useWorkspace } from '@music163/tango-context';
import { Box, css, Text } from 'coral-system';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { CloseOutlined } from '@ant-design/icons';
const errorMessageStyle = css`
padding: 24px;
padding: 35px;
position: relative;
white-space: pre-wrap;
color: #f45755;
font-weight: bold;
height: 100%;
background-color: #171717;
border-radius: 8px;
.top-bar {
border-radius: 8px 8px 0 0;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 10px;
background-color: #f45755;
}
.WorkspaceErrorOverlayItem {
font-size: 18px;
padding-top: 20px;
padding-bottom: 20px;
.WorkspaceErrorOverlayItemFilename {
color: #48cacb;
text-decoration: underline;
letter-spacing: 1px;
&:hover {
color: #51b1ff;
}
}
}
`;
/**
@@ -14,11 +42,17 @@ const errorMessageStyle = css`
export const FileErrorsOverlay = observer(() => {
const [isVisible, setIsVisible] = useState<boolean>(true);
const workspace = useWorkspace();
const designer = useDesigner();
const handleClose = () => {
setIsVisible(false);
};
const handleOpenErrorFile = (fileName: string) => {
workspace.setActiveFile(fileName);
designer.setActiveView('code');
};
if (workspace.fileErrors.length === 0) {
return null;
}
@@ -35,32 +69,63 @@ export const FileErrorsOverlay = observer(() => {
left="0"
right="0"
height="100%"
bg="rgba(244, 244, 244, 0.9)"
bg="rgb(84, 84, 84)"
padding="30px"
color="red"
overflow="auto"
zIndex={1999}
>
<Box position="relative" p="4">
<Box position="relative" p="4" boxShadow="0 0 10px rgba(0, 0, 0, 0.5)">
<CloseOutlined
style={{
position: 'absolute',
top: 12,
top: 24,
right: 12,
color: '#000',
color: '#fff',
zIndex: 2,
}}
onClick={handleClose}
/>
<Box css={errorMessageStyle}>
<Box fontSize="20px" mb="l">
<Box className="top-bar" />
<Box fontSize="18px" mb="l">
<Text letterSpacing=".5px" color="#c29ef5">
[plugin: @babel/parser]
</Text>
<Text marginLeft={'8px'} color="#f45755">
SyntaxError in {workspace.fileErrors.length} filesClick file to modify the error
code
</Text>
</Box>
{workspace.fileErrors.map((fileError) => (
<Box key={fileError.filename} borderBottom="solid" borderBottomColor="line2" py="l">
<Box fontWeight="bold">{fileError.filename}</Box>
<Box as="code" display="block">
{fileError.message}
<Box>
{workspace.fileErrors.map((fileError, index) => (
<Box
className="WorkspaceErrorOverlayItem"
key={fileError.filename}
borderBottom={index === workspace.fileErrors.length - 1 ? 'none' : 'dashed'}
borderBottomColor="line2"
py="l"
>
<Box
className="WorkspaceErrorOverlayItemFilename"
fontWeight="bold"
style={{ cursor: 'pointer' }}
onClick={() => handleOpenErrorFile(fileError.filename)}
>
{fileError.filename}
</Box>
<Box
as="code"
display="block"
color="#daa15e"
fontSize="16px"
fontWeight={'normal'}
>
{fileError.message}
</Box>
</Box>
</Box>
))}
))}
</Box>
</Box>
</Box>
</Box>
@@ -88,6 +88,10 @@ export function AddServiceForm({
>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
{/* 备注 */}
<Form.Item label="备注" name="description">
<Input placeholder="请输入备注信息" />
</Form.Item>
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { DropMethod, FileType, Designer, IWorkspace } from '@music163/tango-core';
import { DropMethod, FileType, Designer, AbstractCodeWorkspace } from '@music163/tango-core';
import { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers';
import {
setElementStyle,
@@ -12,7 +12,7 @@ import { Hotkey } from './hotkey';
import { SelectModeType } from '../types';
interface UseDndProps {
workspace: IWorkspace;
workspace: AbstractCodeWorkspace;
designer: Designer;
/**
* 沙箱内的 DOM 查询操作
@@ -345,7 +345,7 @@ export function useDnd({
// 区块不能拖拽到区块中
if (
workspace.dragSource.prototype.type === 'block' &&
closetDropTargetNode.file.type === FileType.BlockEntryModule
closetDropTargetNode.file.type === FileType.JsLocalComponentsEntryFile
) {
return;
}
+1 -1
View File
@@ -109,7 +109,7 @@ function useSandbox({
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
const display = isActive ? 'block' : 'none';
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
const routerMode = fixRouterMode(workspace.jsAppEntryFile?.routerType);
const sandboxProps = isPreview
? {
@@ -1,11 +1,11 @@
import React, { useCallback } from 'react';
import { Radio, RadioProps, Tooltip } from 'antd';
import type { OptionType } from '@music163/tango-helpers';
import type { IOptionItem } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { IconFont } from '@music163/tango-ui';
interface ChoiceSetterProps {
options?: OptionType[];
options?: IOptionItem[];
}
export function ChoiceSetter({
@@ -0,0 +1,7 @@
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ClassNameInput } from '@music163/tango-ui';
import React from 'react';
export function ClassNameSetter({ value, onChange }: FormItemComponentProps<string>) {
return <ClassNameInput value={value} onChange={onChange} />;
}
@@ -2,9 +2,17 @@ import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system';
import { Dropdown, Button } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core';
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
import {
Panel,
InputCode,
Action,
DragPanel,
PopOutOutlined,
InputCodeProps,
InputStyleCode,
} from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components';
@@ -26,6 +34,19 @@ export const jsonValueValidate = (value: string) => {
}
};
/**
*
* @param value
* @param template
* @returns
*/
export function getCallbackValue(value: string, template?: string) {
if (!value) {
return;
}
return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`;
}
const suffixStyle = css`
display: flex;
align-items: center;
@@ -37,26 +58,32 @@ const suffixStyle = css`
}
`;
export interface ExpressionSetterProps extends FormItemComponentProps<string> {
export interface CodeSetterProps extends FormItemComponentProps<string> {
modalTitle?: string;
modalTip?: string;
autoCompleteOptions?: string[];
allowClear?: boolean;
showOptionsDropDown?: boolean;
/**
* css object
*/
expressionType?: 'cssObject';
}
export function ExpressionSetter(props: ExpressionSetterProps) {
export function CodeSetter(props: CodeSetterProps) {
const {
onChange,
modalTitle,
modalTip,
autoCompleteOptions,
placeholder = '在这里输入JS代码',
template,
placeholder = '在这里输入代码',
value: valueProp,
status,
allowClear = true,
newStoreTemplate,
showOptionsDropDown = true,
expressionType,
} = props;
// const codeValue = getCodeOfWrappedCode(valueProp);
const [inputValue, setInputValue] = useState(valueProp);
@@ -79,10 +106,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
return (
<Box className="ExpressionSetter">
{/* 同时支持下拉框展示 */}
<InputCode
<InputCodeComponent
placeholder={placeholder}
suffix={
<Box css={suffixStyle}>
@@ -90,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
<CloseCircleFilled
title="清空"
onClick={() => {
change('');
change(undefined);
}}
/>
)}
@@ -119,8 +148,10 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
template={template}
newStoreTemplate={newStoreTemplate}
value={inputValue}
expressionType={expressionType}
onOk={(value) => {
change(value);
}}
@@ -141,7 +172,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
);
}
export interface ExpressionPopoverProps {
export interface ExpressionPopoverProps extends InputCodeProps {
title?: string;
subTitle?: string;
placeholder?: string;
@@ -150,10 +181,18 @@ export interface ExpressionPopoverProps {
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
/**
*
*/
template?: string;
/**
* store
*/
newStoreTemplate?: string;
/**
* css object
*/
expressionType?: 'cssObject';
children?: React.ReactNode;
}
@@ -166,12 +205,15 @@ export function ExpressionPopover({
value,
dataSource,
autoCompleteOptions,
template,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
children,
expressionType,
}: ExpressionPopoverProps) {
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
@@ -207,7 +249,7 @@ export function ExpressionPopover({
body={
<Box display="flex" flexDirection="column">
<Box p="m">
<InputCode
<InputCodeComponent
shape="inset"
minHeight="56px"
maxHeight="160px"
@@ -223,19 +265,18 @@ export function ExpressionPopover({
autoCompleteOptions={autoCompleteOptions}
/>
{error ? (
<Text color="red" fontSize="12px" as="p">
<Text color="red" fontSize="12px" as="div">
</Text>
) : null}
{subTitle && (
<Text fontSize="12px" color="text3" as="p">
{subTitle}
<Box fontSize="12px" color="text2">
<Text display="block"></Text>
{subTitle && <Text display="block">{subTitle}</Text>}
<Text display="block">
javascript 使 jsx
</Text>
)}
<Text fontSize="12px" color="text3" as="p">
javascript 使 jsx
</Text>
</Box>
</Box>
<Panel
title="从变量列表中选中"
@@ -253,7 +294,7 @@ export function ExpressionPopover({
height="100%"
showViewButton
dataSource={dataSource || expressionVariables}
appContext={evaluateContext['tango']}
appContext={evaluateContext?.['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
@@ -274,7 +315,11 @@ export function ExpressionPopover({
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `tango.${node.key.replaceAll('.', '?.')}`;
// 从匹配到的第二个点开始替换为 ?.,因为第一个点是 stores 或 services
str = `tango.${node.key.replace(/(?<=\..*?)\./g, '?.')}`;
if (node.type === 'function') {
str = getCallbackValue(`${str}();`, template);
}
} else {
str = `${node.key}`;
}
+27 -31
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useMemo, useState } from 'react';
import { css, Box, Text } from 'coral-system';
import { AutoComplete } from 'antd';
import { AutoComplete, Input } from 'antd';
import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { Dict, wrapCode } from '@music163/tango-helpers';
import { ExpressionPopover } from './expression-setter';
import { ExpressionPopover, getCallbackValue } from './code-setter';
import { value2code } from '@music163/tango-core';
enum EventAction {
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
* 事件监听函数绑定器
*/
export function EventSetter(props: EventSetterProps) {
const { value, onChange, modalTitle } = props;
const { value, onChange, modalTitle, modalTip, template } = props;
const [type, setType] = useState<EventAction>(); // 事件类型
const [temp, setTemp] = useState(''); // 二级暂存值
const { actionVariables, routeOptions } = useWorkspaceData();
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
label: (
<ExpressionPopover
title={modalTitle}
subTitle={modalTip}
value={value}
template={template}
onOk={(nextValue) => {
handleChange(nextValue);
}}
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
{ label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal },
],
[modalTitle, value, actionVariables, handleChange],
[modalTitle, value, actionVariables, template, handleChange],
);
const onAction = (key: string) => {
setType(key as EventAction); // 记录事件类型
setTemp(''); // 重置二级选项值
switch (key) {
case EventAction.ConsoleLog:
handleChange('(...args) => console.log(...args)');
break;
case EventAction.NoAction:
handleChange(undefined);
break;
default:
break;
if (key === EventAction.NoAction) {
handleChange(undefined);
return;
}
};
const actionText = getActionText(type, temp, code);
return (
<Box css={wrapperStyle}>
<ActionSelect options={options} onSelect={onAction} text={actionText} />
<ActionSelect options={options} onSelect={onAction} defaultText="请选择动作类型" />
{type === EventAction.ConsoleLog && (
<Input
placeholder="输入 Console.log 日志内容"
value={temp}
onChange={(e) => setTemp(e.target.value)}
onBlur={() => {
if (temp) {
handleChange(getExpressionValue(type, temp));
}
}}
/>
)}
{type === EventAction.NavigateTo && (
<AutoComplete
placeholder="选择或输入页面路由"
@@ -142,25 +148,15 @@ export function EventSetter(props: EventSetterProps) {
}
const handlerMap: Dict = {
[EventAction.OpenModal]: 'openModal',
[EventAction.CloseModal]: 'closeModal',
[EventAction.NavigateTo]: 'navigateTo',
[EventAction.ConsoleLog]: 'console.log',
[EventAction.OpenModal]: 'tango.openModal',
[EventAction.CloseModal]: 'tango.closeModal',
[EventAction.NavigateTo]: 'tango.navigateTo',
};
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
let text;
if (handlerMap[type]) {
text = getExpressionValue(type, temp);
} else if (fallbackCode) {
text = fallbackCode;
}
text = text || '请选择';
return text;
}
function getExpressionValue(type: EventAction, value = '') {
const handler = handlerMap[type];
if (handler) {
return `() => tango.${handler}("${value}")`;
return getCallbackValue(`${handler}("${value}");`);
}
}
+10 -15
View File
@@ -5,14 +5,14 @@ import { CssSetter } from './css-setter';
import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
import { EnumSetter } from './enum-setter';
import { EventSetter } from './event-setter';
import { ExpressionSetter } from './expression-setter';
import { CodeSetter } from './code-setter';
import { JSONSetter } from './json-setter';
import { JsxSetter } from './jsx-setter';
import { ListSetter } from './list-setter';
import { ModelSetter } from './model-setter';
import { OptionSetter } from './option-setter';
import { PickerSetter } from './picker-setter';
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
import { RenderSetter } from './render-setter';
import { RouterSetter } from './router-setter';
import {
SpacingSetter,
@@ -26,6 +26,7 @@ import {
FlexDirectionSetter,
} from './style-setter';
import { ChoiceSetter } from './choice-setter';
import { ClassNameSetter } from './classname-setter';
import { isValidExpressionCode } from '@music163/tango-core';
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
@@ -49,10 +50,14 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
{
name: 'codeSetter',
alias: ['expSetter', 'expressionSetter'],
component: ExpressionSetter,
component: CodeSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'classNameSetter',
component: ClassNameSetter,
},
{
name: 'radioGroupSetter',
alias: ['choiceSetter'],
@@ -126,22 +131,12 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
component: PickerSetter,
},
{
name: 'renderPropsSetter',
name: 'renderSetter',
alias: ['renderPropsSetter'],
component: RenderSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'tableCellSetter',
component: TableCellSetter,
type: 'code',
},
{
name: 'tableExpandableSetter',
component: TableExpandableSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'routerSetter',
component: RouterSetter,
+10 -80
View File
@@ -2,22 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ActionSelect, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Box } from 'coral-system';
import { Dict } from '@music163/tango-helpers';
interface IRenderOption {
label: string;
value: string;
render?: string;
relatedImports?: string[];
}
import { Dict, IOptionItem } from '@music163/tango-helpers';
import { getCallbackValue } from './code-setter';
export interface RenderSetterProps {
text?: string;
options?: IRenderOption[];
fallbackOption?: IRenderOption;
options?: IOptionItem[];
fallbackOption?: IOptionItem;
}
const defaultOptions: IRenderOption[] = [
const defaultOptions: IOptionItem[] = [
{ label: '取消自定义', value: '' },
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
];
@@ -30,6 +24,7 @@ export function RenderSetter({
onChange,
text = '自定义渲染为',
options = defaultOptions,
template = `() => {{content}}`,
fallbackOption,
}: FormItemComponentProps & RenderSetterProps) {
const [inputValue, setInputValue] = useState(value || '');
@@ -47,13 +42,14 @@ export function RenderSetter({
const onSelect = useCallback(
(key: string) => {
const option = optionsMap[key] || fallbackOption;
if (option?.render) {
onChange(option.render, { relatedImports: option.relatedImports });
const next = option?.render || getCallbackValue(option.renderBody, template);
if (next) {
onChange(next, { relatedImports: option.relatedImports });
} else {
onChange(undefined);
}
},
[optionsMap, fallbackOption, onChange],
[optionsMap, fallbackOption, onChange, template],
);
return (
<Box>
@@ -68,69 +64,3 @@ export function RenderSetter({
</Box>
);
}
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
let code;
switch (type) {
case 'tableCell':
code = `(value, record, index) => ${content}`;
break;
case 'tableExpandable':
code = `{
expandedRowRender: (record) => ${content},
rowExpandable: (record) => true
}`;
break;
default:
code = `() => ${content}`;
break;
}
return code;
};
const tableCellOptions: RenderSetterProps['options'] = [
{ label: '取消自定义', value: '' },
{
label: '自定义区域',
value: 'Box',
render: getRender('<Box></Box>', 'tableCell'),
relatedImports: ['Box'],
},
{
label: '标签',
value: 'Tag',
render: getRender('<Tag>tag</Tag>', 'tableCell'),
relatedImports: ['Tag'],
},
{
label: '按钮',
value: 'Button',
render: getRender('<Button>button</Button>', 'tableCell'),
relatedImports: ['Button'],
},
{
label: '图片',
value: 'Image',
render: getRender('<Image width={150} src="https://picsum.photos/100" />', 'tableCell'),
relatedImports: ['Image'],
},
];
const tableExpandableOptions: RenderSetterProps['options'] = [
{
label: '设置可展开行',
value: 'Box',
render: getRender('<Box></Box>', 'tableExpandable'),
relatedImports: ['Box'],
},
{ label: '取消可展开行', value: '' },
];
export function TableCellSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableCellOptions} {...props} />;
}
// FIXME: 应该直接用 props 嵌套的模式
export function TableExpandableSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
}
@@ -365,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
) : (
<Box className="material-icon">
<img src={icon} alt={data.name} />
<img src={icon} alt={data.name} height="32" />
</Box>
);
@@ -21,7 +21,6 @@ import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { isUndefined } from 'lodash-es';
import { Workspace } from '@music163/tango-core';
import { useSandboxQuery } from '../context';
enum DependencyItemType {
@@ -169,7 +168,7 @@ function RenderItem({
}: RenderItemProps) {
const [open, { on, off }] = useBoolean(false);
const workspace = useWorkspace() as Workspace;
const workspace = useWorkspace();
const basePackage = useMemo(() => {
if (type !== DependencyItemType. || !templateBaseDependencies) return undefined;
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui';
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { observer, useWorkspace } from '@music163/tango-context';
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
import { DropMethod, IViewNodeData } from '@music163/tango-core';
import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers';
@@ -65,7 +65,7 @@ const filedNames = {
children: 'children',
};
const getNodeKeys = (data: ITangoViewNodeData[]) => {
const getNodeKeys = (data: IViewNodeData[]) => {
const ids: string[] = [];
data?.forEach((node) => {
ids.push(node.id);
@@ -76,7 +76,7 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
return ids;
};
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
const OutlineTreeNode: React.FC<{ node: IViewNodeData } & ComponentsTreeProps> = observer(
({ node, showToggleVisibleIcon, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
@@ -142,7 +142,7 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const nodesTree = (file?.nodesTree ?? []) as IViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
const [contextMenuOpen, setContextMenuOpen] = useState(false);
+35 -12
View File
@@ -25,22 +25,45 @@ function DesktopSimulator({ children }: HTMLCoralProps<'div'>) {
}
const mobileStyle = css`
--device-viewport-width: 375px;
--device-viewport-height: 812px;
--device-frame-width: 13px;
background-image: url(https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/21897838692/b1c5/2665/56db/10558c1bb2030d55ceabbcc74431de99.png);
background-repeat: no-repeat;
background-size: 100% 100%;
box-shadow: 0 2px 8px rgb(2 8 20 / 10%), 0 8px 16px rgb(2 8 20 / 8%);
border-radius: 62px;
overflow: hidden;
--device-viewport-width: 360px;
--device-viewport-height: 640px;
position: relative;
margin: auto;
border: 16px black solid;
border-top-width: 60px;
border-bottom-width: 60px;
border-radius: 36px;
&::before {
content: '';
display: block;
width: 60px;
height: 5px;
position: absolute;
top: -30px;
left: 50%;
transform: translate(-50%, -50%);
background: #333;
border-radius: 10px;
}
&::after {
content: '';
display: block;
width: 35px;
height: 35px;
position: absolute;
left: 50%;
bottom: -65px;
transform: translate(-50%, -50%);
background: #333;
border-radius: 50%;
}
.MobileSimulatorDeviceFrame {
border-radius: 50px;
width: var(--device-viewport-width);
height: var(--device-viewport-height);
margin: var(--device-frame-width);
overflow: hidden;
background: white;
}
`;
@@ -9,7 +9,7 @@ export const ModeSwitchTool = observer(() => {
const designer = useDesigner();
return (
<Group attached>
<Group attached whiteSpace="nowrap">
<ToggleButton
shape="ghost"
selected={designer.activeView === 'design'}
@@ -47,11 +47,14 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
setVisible(false);
};
const current = optionMap[value];
const pathname = value?.split?.('?')[0];
const current = optionMap[pathname];
return (
<ToggleButton
shape="ghost"
minWidth={100}
maxWidth={200}
textAlign="left"
dropdownProps={{
open: visible,
onOpenChange: setVisible,
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.3...@music163/tango-helpers@1.2.4) (2024-09-09)
### Bug Fixes
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.2...@music163/tango-helpers@1.2.3) (2024-09-02)
### Bug Fixes
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.1...@music163/tango-helpers@1.2.2) (2024-08-05)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "1.2.2",
"version": "1.2.4",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",
+14
View File
@@ -125,3 +125,17 @@ export function parseDndId(str: string): DndIdParsedType {
id: str,
};
}
/**
* 替换模板中的变量
* @example interpolate('hello {{name}}', { name: 'world' }) -> 'hello world'
*
* @param template 带模板变量的字符串
* @param props 变量字典
* @returns 返回替换后的字符串
*/
export function interpolate(template: string, props: Record<string, any>) {
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
return props[key];
});
}
+20 -4
View File
@@ -10,11 +10,19 @@ export type MousePoint = {
y: number;
};
export type OptionType = {
/**
* @deprecated 使用 IOptionItem 代替
*/
export type OptionType = IOptionItem<any>;
/**
* 列表项
*/
export interface IOptionItem<T = string> {
/**
* 值
*/
value: any;
value: T;
/**
* 显示的文本
*/
@@ -28,10 +36,18 @@ export type OptionType = {
*/
tip?: string;
/**
* 关联的导入
* 自定义渲染函数,仅用于 renderSetter
*/
render?: string;
/**
* 自定义渲染函数的 body(可以用来和 template 配合),仅用于 renderSetter
*/
renderBody?: string;
/**
* 关联的导入,仅用于 renderSetter
*/
relatedImports?: string[];
};
}
/**
* 变量树节点类型
+7 -2
View File
@@ -2,7 +2,7 @@
* 物料描述类型
*/
import { OptionType } from './advanced';
import { IOptionItem } from './advanced';
import { PartialRecord } from './base';
/**
@@ -88,6 +88,11 @@ export interface IComponentProp<T = any> {
* 自动补全的提示值,仅对 ExpressionSetter 有效
*/
autoCompleteOptions?: string[];
/**
* value 的模板,一般用于函数类型的属性,便于 setter 用来拼装返回值
* @example "(arg1, arg2, arg3) => { {{content}}}"
*/
template?: string;
/**
* 设置器
*/
@@ -108,7 +113,7 @@ export interface IComponentProp<T = any> {
/**
* 配置项的可选值,setterProps.options 的简写
*/
options?: OptionType[];
options?: IOptionItem[];
/**
* 是否禁用变量绑定
*/
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.13](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.12...@music163/tango-sandbox@1.0.13) (2024-09-09)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.12](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.11...@music163/tango-sandbox@1.0.12) (2024-09-02)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.11](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.10...@music163/tango-sandbox@1.0.11) (2024-08-06)
**Note:** Version bump only for package @music163/tango-sandbox
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "1.0.11",
"version": "1.0.13",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,8 +29,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.4.2",
"@music163/tango-helpers": "^1.2.2",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.15](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.14...@music163/tango-setting-form@1.2.15) (2024-09-09)
### Bug Fixes
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
## [1.2.14](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.13...@music163/tango-setting-form@1.2.14) (2024-09-02)
### Bug Fixes
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
## [1.2.13](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.12...@music163/tango-setting-form@1.2.13) (2024-08-16)
### Bug Fixes
- CodeInput 支持 jsx css 代码补全 ([#198](https://github.com/netease/tango/issues/198)) ([6de5488](https://github.com/netease/tango/commit/6de54884b50d683c74d695a8c13184dc0ce8d3c4))
## [1.2.12](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.11...@music163/tango-setting-form@1.2.12) (2024-08-09)
**Note:** Version bump only for package @music163/tango-setting-form
## [1.2.11](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.10...@music163/tango-setting-form@1.2.11) (2024-08-06)
**Note:** Version bump only for package @music163/tango-setting-form
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "1.2.11",
"version": "1.2.15",
"description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,12 +28,12 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.4.2",
"@music163/tango-helpers": "^1.2.2",
"@music163/tango-ui": "^1.4.1",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"@music163/tango-ui": "^1.4.5",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.12.3",
"mobx": "6.13.2",
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
+11 -4
View File
@@ -182,6 +182,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
placeholder,
docs,
autoCompleteOptions,
template,
setter: setterProp,
setterProps,
defaultValue,
@@ -230,14 +231,20 @@ export function createFormItem(options: IFormItemCreateOptions) {
// FIXME: 重新考虑这段代码的位置,外置这个逻辑
if (
['codeSetter', 'expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(
setter,
)
[
'codeSetter',
'expressionSetter',
'expSetter',
'actionSetter',
'eventSetter',
'renderSetter',
].includes(setter)
) {
expProps = {
modalTitle: title,
modalTip: tip,
autoCompleteOptions,
template,
};
}
@@ -246,7 +253,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
const CodeSetter = REGISTERED_FORM_ITEM_MAP['codeSetter']?.config?.component;
const setterNode = isCodeSetter ? (
<CodeSetter {...expProps} {...baseComponentProps} />
<CodeSetter {...expProps} {...baseComponentProps} {...setterProps} />
) : (
renderSetter({
...expProps,
+1 -1
View File
@@ -42,7 +42,7 @@ export const SettingFormObject = observer(
note={name}
tip={tip}
docs={docs}
checked={!!subModel.values ?? !!defaultValue}
checked={!!subModel.values || !!defaultValue}
onCheck={(checked) => {
const nextValue = checked ? {} : undefined;
parent.setValue(name, nextValue);
+25
View File
@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.4...@music163/tango-ui@1.4.5) (2024-09-09)
### Bug Fixes
- add classNameSetter and classNameInput ([#207](https://github.com/netease/tango/issues/207)) ([55602fc](https://github.com/netease/tango/commit/55602fc961b5c20d923891ce13d7a51588bd047f))
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.3...@music163/tango-ui@1.4.4) (2024-09-02)
### Bug Fixes
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.2...@music163/tango-ui@1.4.3) (2024-08-16)
### Bug Fixes
- CodeInput 支持 jsx css 代码补全 ([#198](https://github.com/netease/tango/issues/198)) ([6de5488](https://github.com/netease/tango/commit/6de54884b50d683c74d695a8c13184dc0ce8d3c4))
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.1...@music163/tango-ui@1.4.2) (2024-08-09)
### Bug Fixes
- route matching without query string & toggle button wrapping ([#195](https://github.com/netease/tango/issues/195)) ([9394666](https://github.com/netease/tango/commit/939466648d40331800f15e886256a0789b7e7623))
- update mobile simulator ui ([#196](https://github.com/netease/tango/issues/196)) ([65149f1](https://github.com/netease/tango/commit/65149f1326eaf78e8a27222993c3cacc674cd378))
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.0...@music163/tango-ui@1.4.1) (2024-08-05)
**Note:** Version bump only for package @music163/tango-ui
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "1.4.1",
"version": "1.4.5",
"description": "ui widgets of tango",
"keywords": [
"react",
@@ -38,7 +38,7 @@
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lint": "^6.7.1",
"@codemirror/search": "^6.5.6",
"@music163/tango-helpers": "^1.2.2",
"@music163/tango-helpers": "^1.2.4",
"@uiw/react-codemirror": "^4.22.0",
"antd": "^4.24.2",
"classnames": "^2.5.1",
@@ -46,7 +46,7 @@
"eslint-linter-browserify": "^8.51.0",
"react-draggable": "^4.4.5",
"react-json-view": "^1.21.3",
"react-monaco-editor-lite": "^1.3.11"
"react-monaco-editor-lite": "^1.3.16"
},
"publishConfig": {
"access": "public",
+21 -12
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Box, css, Text } from 'coral-system';
import { Button, Dropdown, Input, Menu } from 'antd';
import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons';
@@ -23,9 +23,13 @@ interface ActionSelectProps {
*/
onInputChange?: (value: string) => void;
/**
* 提示文本
* 受控的提示文本
*/
text?: string;
/**
* 默认的提示文本
*/
defaultText?: string;
/**
* 选择菜单时的回调
*/
@@ -50,11 +54,6 @@ const actionInputStyle = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--tango-colors-text3);
&:hover {
color: var(--tango-colors-text2);
}
}
.anticon-down {
@@ -71,15 +70,25 @@ const inputStyle = { width: 'calc(100% - 82px)' };
export function ActionSelect({
showInput = false,
defaultInputValue,
text,
text: textProp,
defaultText,
options = [],
onSelect,
onInputChange,
}: ActionSelectProps) {
const [text, setText] = useState<string>(defaultText);
const menu = (
<Menu onClick={({ key }) => onSelect(key)}>
<Menu>
{options.map((item) => (
<Menu.Item key={item.value}>
<Menu.Item
key={item.value}
onClick={() => {
onSelect?.(item.value);
if (!textProp) {
setText(item.label as string);
}
}}
>
<Box display="flex" alignItems="center" minWidth={200} columnGap="m">
<Box flex="1">{item.label}</Box>
<Text fontSize="12px" color="text.note">
@@ -122,8 +131,8 @@ export function ActionSelect({
mb="m"
css={actionInputStyle}
>
<Box className="action-input-text" color="text.body">
{text}
<Box className="action-input-text" color="text2">
{textProp ?? text}
</Box>
<DownOutlined />
</Box>
+312
View File
@@ -0,0 +1,312 @@
import React, { useState, useRef, KeyboardEvent, ChangeEvent, useEffect } from 'react';
import { Dropdown, Menu, Tag } from 'antd';
import styled from 'styled-components';
const InputWrapper = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
min-height: 32px;
&:hover {
border-color: #40a9ff;
}
&:focus-within {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
`;
const Input = styled.input`
flex: 1;
border: none;
outline: none;
padding: 0;
font-size: 14px;
min-width: 50px;
height: 24px;
line-height: 24px;
`;
const StyledTag = styled(Tag)`
margin: 2px 4px 2px 0;
`;
// Tailwind CSS 基础类名列表
const tailwindClasses = [
// 布局
'container',
'flex',
'grid',
'block',
'inline',
'inline-block',
'hidden',
// 弹性布局
'flex-row',
'flex-col',
'flex-wrap',
'flex-nowrap',
'justify-start',
'justify-end',
'justify-center',
'justify-between',
'justify-around',
'items-start',
'items-end',
'items-center',
'items-baseline',
'items-stretch',
// 网格布局
'grid-cols-1',
'grid-cols-2',
'grid-cols-3',
'grid-cols-4',
'grid-cols-5',
'grid-cols-6',
'grid-cols-12',
// 间距
'p-0',
'p-1',
'p-2',
'p-3',
'p-4',
'p-5',
'p-6',
'p-8',
'p-10',
'p-12',
'p-16',
'p-20',
'm-0',
'm-1',
'm-2',
'm-3',
'm-4',
'm-5',
'm-6',
'm-8',
'm-10',
'm-12',
'm-16',
'm-20',
// 尺寸
'w-full',
'w-auto',
'w-1/2',
'w-1/3',
'w-2/3',
'w-1/4',
'w-3/4',
'h-full',
'h-auto',
'h-screen',
// 字体
'text-xs',
'text-sm',
'text-base',
'text-lg',
'text-xl',
'text-2xl',
'text-3xl',
'text-4xl',
'text-5xl',
'font-thin',
'font-light',
'font-normal',
'font-medium',
'font-semibold',
'font-bold',
'font-extrabold',
// 文本颜色
'text-black',
'text-white',
'text-gray-100',
'text-gray-200',
'text-gray-300',
'text-gray-400',
'text-gray-500',
'text-red-500',
'text-blue-500',
'text-green-500',
'text-yellow-500',
'text-purple-500',
'text-pink-500',
// 背景颜色
'bg-transparent',
'bg-black',
'bg-white',
'bg-gray-100',
'bg-gray-200',
'bg-gray-300',
'bg-gray-400',
'bg-gray-500',
'bg-red-500',
'bg-blue-500',
'bg-green-500',
'bg-yellow-500',
'bg-purple-500',
'bg-pink-500',
// 边框
'border',
'border-0',
'border-2',
'border-4',
'border-8',
'border-black',
'border-white',
'border-gray-300',
'border-gray-400',
'border-gray-500',
// 圆角
'rounded-none',
'rounded-sm',
'rounded',
'rounded-lg',
'rounded-full',
// 阴影
'shadow-sm',
'shadow',
'shadow-md',
'shadow-lg',
'shadow-xl',
'shadow-2xl',
'shadow-none',
// 不透明度
'opacity-0',
'opacity-25',
'opacity-50',
'opacity-75',
'opacity-100',
];
interface ClassNameInputProps {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}
export function ClassNameInput({ value, defaultValue, onChange }: ClassNameInputProps) {
const [inputValue, setInputValue] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const [internalValue, setInternalValue] = useState(defaultValue || '');
const isControlled = value !== undefined;
const classNames = (isControlled ? value : internalValue).split(' ').filter(Boolean);
useEffect(() => {
if (isControlled) {
setInternalValue(value);
}
}, [isControlled, value]);
const isValidClassName = (className: string) => {
return /^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*$/.test(className);
};
const updateValue = (newClassNames: string[]) => {
const newValue = newClassNames.join(' ');
if (isControlled) {
onChange?.(newValue);
} else {
setInternalValue(newValue);
onChange?.(newValue);
}
};
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
addClassName(suggestions[highlightedIndex]);
} else {
addClassName(inputValue.trim());
}
} else if (event.key === 'ArrowDown') {
event.preventDefault();
setHighlightedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0));
} else if (event.key === 'ArrowUp') {
event.preventDefault();
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1));
} else if (event.key === 'Backspace' && inputValue === '' && classNames.length > 0) {
event.preventDefault();
const newClassNames = [...classNames];
newClassNames.pop();
updateValue(newClassNames);
}
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const input = event.target.value;
setInputValue(input);
if (input) {
const matchedSuggestions = tailwindClasses
.filter(
(className) =>
className.toLowerCase().includes(input.toLowerCase()) &&
!classNames.includes(className),
)
.slice(0, 10);
setSuggestions(matchedSuggestions);
setHighlightedIndex(-1);
} else {
setSuggestions([]);
}
};
const addClassName = (className: string) => {
if (className && !classNames.includes(className) && isValidClassName(className)) {
updateValue([...classNames, className]);
setInputValue('');
setSuggestions([]);
setHighlightedIndex(-1);
}
};
const removeClassName = (removedTag: string) => {
const newClassNames = classNames.filter((tag) => tag !== removedTag);
updateValue(newClassNames);
};
const handleSuggestionClick = (suggestion: string) => {
addClassName(suggestion);
};
const menu = (
<Menu>
{suggestions.map((suggestion, index) => (
<Menu.Item
key={index}
onClick={() => handleSuggestionClick(suggestion)}
className={index === highlightedIndex ? 'ant-dropdown-menu-item-active' : ''}
>
{suggestion}
</Menu.Item>
))}
</Menu>
);
return (
<Dropdown overlay={menu} visible={suggestions.length > 0} placement="bottomLeft">
<InputWrapper onClick={() => inputRef.current?.focus()}>
{classNames.map((className) => (
<StyledTag key={className} closable onClose={() => removeClassName(className)}>
{className}
</StyledTag>
))}
<Input
ref={inputRef}
type="text"
value={inputValue}
onKeyDown={handleInputKeyDown}
onChange={handleInputChange}
placeholder={classNames.length === 0 ? '输入 class 名称' : ''}
/>
</InputWrapper>
</Dropdown>
);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { createIcon } from './create-icon';
const DualOutlinedSvg = () => (
<svg
viewBox="0 0 1025 1024"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1em"
+2
View File
@@ -13,6 +13,7 @@ export * from './menu';
export * from './panel';
export * from './toggle-button';
export * from './input-code';
export * from './input-style-code';
export * from './input-list';
export * from './search';
export * from './json-view';
@@ -25,3 +26,4 @@ export * from './tag-select';
export * from './popover';
export * from './drag-panel';
export * from './context-action';
export * from './classname-input';
+1 -1
View File
@@ -174,7 +174,7 @@ export function InputCode({
);
}
function useInputCode({
export function useInputCode({
shape,
status,
showLineNumbers,
+99
View File
@@ -0,0 +1,99 @@
import React from 'react';
import { Box } from 'coral-system';
import CodeMirror from '@uiw/react-codemirror';
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 { jsxCSSproperties, jsxCSSvalues } from './lang/css-object';
import type { Completion } from '@codemirror/autocomplete';
import { InputCodeProps, useInputCode } from './input-code';
const eslintConfig = {
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
env: {
browser: true,
},
rules: {},
};
function completeProperties(from: number, cssOptions: Completion[] | readonly Completion[]) {
return {
from,
options: cssOptions,
validFor: /^[\w$]*$/,
};
}
/**
*
* @param scope 预测 CSS 的补全上下文
* @param customOptions 自定义补全选项列表
* @returns
*/
function buildAutoComplete() {
/**
* @see context https://codemirror.net/docs/ref/#autocomplete.CompletionContext
*/
return function (context: CompletionContext) {
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
if (nodeBefore.name === 'VariableName') {
// match css property name
return completeProperties(nodeBefore.from, jsxCSSproperties());
}
// match css property value
else if (nodeBefore.name === 'String') {
return completeProperties(nodeBefore.from + 1, jsxCSSvalues);
}
return null;
};
}
export type InputStyleCodeProps = Omit<
InputCodeProps,
'autoCompleteContext' | 'autoCompleteOptions'
>;
export function InputStyleCode({
shape = 'solid',
suffix,
status,
showLineNumbers,
showFoldGutter,
enableESLint = false,
...rest
}: InputStyleCodeProps) {
const extensions = [
javascript({ jsx: true }),
javascriptLanguage.data.of({
autocomplete: buildAutoComplete(),
}),
];
if (enableESLint) {
extensions.push(lintGutter(), linter(esLint(new eslint.Linter(), eslintConfig)));
}
const { rootStyle, codeSetup } = useInputCode({
shape,
status,
showLineNumbers,
showFoldGutter,
});
return (
<Box className="InputCode" display="flex" alignItems="center" overflow="hidden" {...rootStyle}>
<Box flex="1" overflow="auto">
<CodeMirror
extensions={extensions}
basicSetup={codeSetup}
placeholder={'//输入 css 代码'}
{...rest}
/>
</Box>
{suffix}
</Box>
);
}
+602
View File
@@ -0,0 +1,602 @@
/**
* @file: jsx css completion file
* @description: fork from https://github.com/codemirror/lang-css/blob/main/src/complete.ts
*/
import type { Completion } from '@codemirror/autocomplete';
function toCamelCase(input: string): string {
return input.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
let _properties: readonly Completion[] | null = null;
// inject global css properties
export function jsxCSSproperties() {
if (!_properties && typeof document === 'object' && document.body) {
const { style } = document.body;
const names = [];
const seen = new Set();
for (let prop in style)
if (prop !== 'cssText' && prop !== 'cssFloat') {
if (typeof style[prop] === 'string') {
if (/[A-Z]/.test(prop)) prop = prop.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
if (!seen.has(prop)) {
names.push(prop);
seen.add(prop);
}
}
}
_properties = names.sort().map((name) => ({ label: toCamelCase(name), type: 'variable' }));
}
return _properties || [];
}
export const jsxCSSvalues = [
'above',
'absolute',
'activeborder',
'additive',
'activecaption',
'after-white-space',
'ahead',
'alias',
'all',
'all-scroll',
'alphabetic',
'alternate',
'always',
'antialiased',
'appworkspace',
'asterisks',
'attr',
'auto',
'auto-flow',
'avoid',
'avoid-column',
'avoid-page',
'avoid-region',
'axis-pan',
'background',
'backwards',
'baseline',
'below',
'bidi-override',
'blink',
'block',
'block-axis',
'bold',
'bolder',
'border',
'border-box',
'both',
'bottom',
'break',
'break-all',
'break-word',
'bullets',
'button',
'button-bevel',
'buttonface',
'buttonhighlight',
'buttonshadow',
'buttontext',
'calc',
'capitalize',
'caps-lock-indicator',
'caption',
'captiontext',
'caret',
'cell',
'center',
'checkbox',
'circle',
'cjk-decimal',
'clear',
'clip',
'close-quote',
'col-resize',
'collapse',
'color',
'color-burn',
'color-dodge',
'column',
'column-reverse',
'compact',
'condensed',
'contain',
'content',
'contents',
'content-box',
'context-menu',
'continuous',
'copy',
'counter',
'counters',
'cover',
'crop',
'cross',
'crosshair',
'currentcolor',
'cursive',
'cyclic',
'darken',
'dashed',
'decimal',
'decimal-leading-zero',
'default',
'default-button',
'dense',
'destination-atop',
'destination-in',
'destination-out',
'destination-over',
'difference',
'disc',
'discard',
'disclosure-closed',
'disclosure-open',
'document',
'dot-dash',
'dot-dot-dash',
'dotted',
'double',
'down',
'e-resize',
'ease',
'ease-in',
'ease-in-out',
'ease-out',
'element',
'ellipse',
'ellipsis',
'embed',
'end',
'ethiopic-abegede-gez',
'ethiopic-halehame-aa-er',
'ethiopic-halehame-gez',
'ew-resize',
'exclusion',
'expanded',
'extends',
'extra-condensed',
'extra-expanded',
'fantasy',
'fast',
'fill',
'fill-box',
'fixed',
'flat',
'flex',
'flex-end',
'flex-start',
'footnotes',
'forwards',
'from',
'geometricPrecision',
'graytext',
'grid',
'groove',
'hand',
'hard-light',
'help',
'hidden',
'hide',
'higher',
'highlight',
'highlighttext',
'horizontal',
'hsl',
'hsla',
'hue',
'icon',
'ignore',
'inactiveborder',
'inactivecaption',
'inactivecaptiontext',
'infinite',
'infobackground',
'infotext',
'inherit',
'initial',
'inline',
'inline-axis',
'inline-block',
'inline-flex',
'inline-grid',
'inline-table',
'inset',
'inside',
'intrinsic',
'invert',
'italic',
'justify',
'keep-all',
'landscape',
'large',
'larger',
'left',
'level',
'lighter',
'lighten',
'line-through',
'linear',
'linear-gradient',
'lines',
'list-item',
'listbox',
'listitem',
'local',
'logical',
'loud',
'lower',
'lower-hexadecimal',
'lower-latin',
'lower-norwegian',
'lowercase',
'ltr',
'luminosity',
'manipulation',
'match',
'matrix',
'matrix3d',
'medium',
'menu',
'menutext',
'message-box',
'middle',
'min-intrinsic',
'mix',
'monospace',
'move',
'multiple',
'multiple_mask_images',
'multiply',
'n-resize',
'narrower',
'ne-resize',
'nesw-resize',
'no-close-quote',
'no-drop',
'no-open-quote',
'no-repeat',
'none',
'normal',
'not-allowed',
'nowrap',
'ns-resize',
'numbers',
'numeric',
'nw-resize',
'nwse-resize',
'oblique',
'opacity',
'open-quote',
'optimizeLegibility',
'optimizeSpeed',
'outset',
'outside',
'outside-shape',
'overlay',
'overline',
'padding',
'padding-box',
'painted',
'page',
'paused',
'perspective',
'pinch-zoom',
'plus-darker',
'plus-lighter',
'pointer',
'polygon',
'portrait',
'pre',
'pre-line',
'pre-wrap',
'preserve-3d',
'progress',
'push-button',
'radial-gradient',
'radio',
'read-only',
'read-write',
'read-write-plaintext-only',
'rectangle',
'region',
'relative',
'repeat',
'repeating-linear-gradient',
'repeating-radial-gradient',
'repeat-x',
'repeat-y',
'reset',
'reverse',
'rgb',
'rgba',
'ridge',
'right',
'rotate',
'rotate3d',
'rotateX',
'rotateY',
'rotateZ',
'round',
'row',
'row-resize',
'row-reverse',
'rtl',
'run-in',
'running',
's-resize',
'sans-serif',
'saturation',
'scale',
'scale3d',
'scaleX',
'scaleY',
'scaleZ',
'screen',
'scroll',
'scrollbar',
'scroll-position',
'se-resize',
'self-start',
'self-end',
'semi-condensed',
'semi-expanded',
'separate',
'serif',
'show',
'single',
'skew',
'skewX',
'skewY',
'skip-white-space',
'slide',
'slider-horizontal',
'slider-vertical',
'sliderthumb-horizontal',
'sliderthumb-vertical',
'slow',
'small',
'small-caps',
'small-caption',
'smaller',
'soft-light',
'solid',
'source-atop',
'source-in',
'source-out',
'source-over',
'space',
'space-around',
'space-between',
'space-evenly',
'spell-out',
'square',
'start',
'static',
'status-bar',
'stretch',
'stroke',
'stroke-box',
'sub',
'subpixel-antialiased',
'svg_masks',
'super',
'sw-resize',
'symbolic',
'symbols',
'system-ui',
'table',
'table-caption',
'table-cell',
'table-column',
'table-column-group',
'table-footer-group',
'table-header-group',
'table-row',
'table-row-group',
'text',
'text-bottom',
'text-top',
'textarea',
'textfield',
'thick',
'thin',
'threeddarkshadow',
'threedface',
'threedhighlight',
'threedlightshadow',
'threedshadow',
'to',
'top',
'transform',
'translate',
'translate3d',
'translateX',
'translateY',
'translateZ',
'transparent',
'ultra-condensed',
'ultra-expanded',
'underline',
'unidirectional-pan',
'unset',
'up',
'upper-latin',
'uppercase',
'url',
'var',
'vertical',
'vertical-text',
'view-box',
'visible',
'visibleFill',
'visiblePainted',
'visibleStroke',
'visual',
'w-resize',
'wait',
'wave',
'wider',
'window',
'windowframe',
'windowtext',
'words',
'wrap',
'wrap-reverse',
'x-large',
'x-small',
'xor',
'xx-large',
'xx-small',
]
.map((name) => ({ label: name, type: 'text' }))
.concat(
[
'aliceblue',
'antiquewhite',
'aqua',
'aquamarine',
'azure',
'beige',
'bisque',
'black',
'blanchedalmond',
'blue',
'blueviolet',
'brown',
'burlywood',
'cadetblue',
'chartreuse',
'chocolate',
'coral',
'cornflowerblue',
'cornsilk',
'crimson',
'cyan',
'darkblue',
'darkcyan',
'darkgoldenrod',
'darkgray',
'darkgreen',
'darkkhaki',
'darkmagenta',
'darkolivegreen',
'darkorange',
'darkorchid',
'darkred',
'darksalmon',
'darkseagreen',
'darkslateblue',
'darkslategray',
'darkturquoise',
'darkviolet',
'deeppink',
'deepskyblue',
'dimgray',
'dodgerblue',
'firebrick',
'floralwhite',
'forestgreen',
'fuchsia',
'gainsboro',
'ghostwhite',
'gold',
'goldenrod',
'gray',
'grey',
'green',
'greenyellow',
'honeydew',
'hotpink',
'indianred',
'indigo',
'ivory',
'khaki',
'lavender',
'lavenderblush',
'lawngreen',
'lemonchiffon',
'lightblue',
'lightcoral',
'lightcyan',
'lightgoldenrodyellow',
'lightgray',
'lightgreen',
'lightpink',
'lightsalmon',
'lightseagreen',
'lightskyblue',
'lightslategray',
'lightsteelblue',
'lightyellow',
'lime',
'limegreen',
'linen',
'magenta',
'maroon',
'mediumaquamarine',
'mediumblue',
'mediumorchid',
'mediumpurple',
'mediumseagreen',
'mediumslateblue',
'mediumspringgreen',
'mediumturquoise',
'mediumvioletred',
'midnightblue',
'mintcream',
'mistyrose',
'moccasin',
'navajowhite',
'navy',
'oldlace',
'olive',
'olivedrab',
'orange',
'orangered',
'orchid',
'palegoldenrod',
'palegreen',
'paleturquoise',
'palevioletred',
'papayawhip',
'peachpuff',
'peru',
'pink',
'plum',
'powderblue',
'purple',
'rebeccapurple',
'red',
'rosybrown',
'royalblue',
'saddlebrown',
'salmon',
'sandybrown',
'seagreen',
'seashell',
'sienna',
'silver',
'skyblue',
'slateblue',
'slategray',
'snow',
'springgreen',
'steelblue',
'tan',
'teal',
'thistle',
'tomato',
'turquoise',
'violet',
'wheat',
'white',
'whitesmoke',
'yellow',
'yellowgreen',
].map((name) => ({ label: name, type: 'text' })),
);
+3 -1
View File
@@ -12,6 +12,7 @@ const buttonStyle = css`
cursor: pointer;
color: var(--tango-colors-gray-50);
padding: 0 8px;
white-space: nowrap;
.anticon-down {
width: 10px;
@@ -201,7 +202,8 @@ export function ToggleButton(props: ToggleButtonProps) {
let btn = (
<Button css={buttonStyle} className={clazz} type="button" {...rest}>
<Box as="span">
{/* @ts-ignore textOverflow is available */}
<Box as="span" flex="1" overflow="hidden" textOverflow="ellipsis">
{icon && (
<Box as="span" className="buttonIcon" mr="s">
{icon}
-1
View File
@@ -10,7 +10,6 @@
"skipLibCheck": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"suppressImplicitAnyIndexErrors": true,
"ignoreDeprecations": "5.0",
"allowSyntheticDefaultImports": true,
"strictNullChecks": false,
+5699 -5273
View File
File diff suppressed because it is too large Load Diff