Compare commits

...

2 Commits

Author SHA1 Message Date
wwsun 7999f604fd chore(release): publish
- @music163/tango-context@0.2.0
 - @music163/tango-core@0.2.0
 - @music163/tango-designer@0.3.0
 - @music163/tango-sandbox@0.1.5
 - @music163/tango-setting-form@0.2.2
2023-09-21 13:59:44 +08:00
Wells a7df29c3de feat: support multiple service modules & refactor workspace
* feat: support multiple service modules

* style: add memo

* fix: refactor module

* fix: refactor workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-09-21 13:57:04 +08:00
27 changed files with 979 additions and 969 deletions
+15 -1
View File
@@ -44,7 +44,7 @@ const tangoConfigJson = {
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.1.3',
version: '0.2.0',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
@@ -175,6 +175,7 @@ export default defineStore({
const serviceCode = `
import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({
get: {
@@ -201,6 +202,18 @@ export default defineServices({
});
`;
const subServiceCode = `
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
},
}, {
namespace: 'sub',
});
`;
const lessCode = `
body {
font-size: 12px;
@@ -232,6 +245,7 @@ export const sampleFiles = [
{ filename: '/src/stores/app.js', code: storeApp },
{ filename: '/src/stores/counter.js', code: storeCounter },
{ filename: '/src/services/index.js', code: serviceCode },
{ filename: '/src/services/sub.js', code: subServiceCode },
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
];
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-context@0.1.4...@music163/tango-context@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-context@0.1.3...@music163/tango-context@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-context
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "0.1.4",
"version": "0.2.0",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,7 +31,7 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^0.1.4",
"@music163/tango-core": "^0.2.0",
"@music163/tango-helpers": "^0.1.3",
"mobx-react-lite": "4.0.2"
},
+12 -5
View File
@@ -53,7 +53,7 @@ export const useWorkspaceData = () => {
const storeVariables: any[] = []; // 模型中的所有变量
const serviceVariables: any[] = []; // 服务中的所有变量
workspace.listStoreModules?.().forEach((file) => {
Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`;
const states = file.states.map((item) => ({
title: item.name,
@@ -91,11 +91,17 @@ export const useWorkspaceData = () => {
});
});
Object.keys(workspace.serviceModule?.serviceFunctions || {}).forEach((key) => {
Object.values(workspace.serviceModules).forEach((file) => {
const prefix = file.name !== 'index' ? `services.${file.name}` : 'services';
serviceVariables.push({
title: key,
key: `services.${key}`,
type: 'function',
title: file.name,
key: prefix,
selectable: false,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
})),
});
});
@@ -113,6 +119,7 @@ export const useWorkspaceData = () => {
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
storeVariables,
serviceVariables,
expressionVariables: [
buildVariableOptions('数据模型', 'stores', storeVariables),
buildVariableOptions('服务函数', 'services', serviceVariables),
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-core@0.1.4...@music163/tango-core@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-core@0.1.3...@music163/tango-core@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-core
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "0.1.4",
"version": "0.2.0",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
+9 -33
View File
@@ -11,14 +11,9 @@ import {
StringOrNumber,
Dict,
parseDndId,
upperCamelCase
upperCamelCase,
} from '@music163/tango-helpers';
import {
keyNode2value,
jsxAttributeValueNode2value,
node2value,
node2code,
} from './generate';
import { keyNode2value, jsxAttributeValueNode2value, node2value, node2code } from './generate';
import {
value2jsxAttributeValueNode,
value2jsxChildrenValueNode,
@@ -957,42 +952,22 @@ export function updateServiceConfigToServiceFile(
return ast;
}
function visitObjectProperties(
node: t.ObjectExpression,
visitor: (propName: string, node: t.ObjectProperty) => void,
) {
for (const propNode of node.properties) {
if (t.isObjectProperty(propNode)) {
const propName = keyNode2value(propNode.key);
visitor?.(propName as string, propNode);
}
}
}
export function updateBaseConfigToServiceFile(ast: t.File, configName: string, configValue: any) {
export function updateBaseConfigToServiceFile(ast: t.File, config: object) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) {
switch (path.node.arguments.length) {
case 1:
path.node.arguments.push(value2node({ [configName]: configValue }));
// 没有 baseConfig,直接创建新的参数即可
path.node.arguments.push(value2node(config));
break;
case 2: {
// 已存在 baseConfig,需要进行参数的合并
const baseConfigNode = path.node.arguments[1];
if (t.isObjectExpression(baseConfigNode)) {
let found = false;
visitObjectProperties(baseConfigNode, (propName, propNode) => {
if (propName === configName) {
found = true;
propNode.value = value2node(configValue);
}
});
if (!found) {
baseConfigNode.properties.push(
t.objectProperty(t.identifier(configName), value2node(configValue)),
);
}
const baseConfig = node2value(baseConfigNode);
path.node.arguments[1] = value2node({ ...baseConfig, ...config });
}
break;
}
@@ -1020,6 +995,7 @@ export function traverseServiceFile(ast: t.File) {
if (path.node.arguments[1]) {
const config = node2value(path.node.arguments[1]);
baseConfig.encryptFetch = !!config.encryptFetch;
baseConfig.namespace = config.namespace || 'index';
}
}
path.stop();
+1 -1
View File
@@ -35,7 +35,7 @@ export function inferFileType(filename: string): FileType {
}
// FIXME: 暂时写死,后续要支持多 services 文件解析
if (/\/services\/index.js$/.test(filename)) {
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
+4
View File
@@ -7,5 +7,9 @@ export * from './drag-source';
export * from './history';
export * from './file';
export * from './module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './node';
export * from './interfaces';
+20 -45
View File
@@ -9,11 +9,12 @@ import {
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IProjectData,
IServiceFunctionPayload,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule, TangoServiceModule, TangoStoreModule } from './module';
import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
export interface IViewFile {
readonly workspace: IWorkspace;
@@ -25,6 +26,9 @@ export interface IViewFile {
*/
importMap?: Dict<{ package: string; isDefault?: boolean }>;
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
getNode: (targetNodeId: string) => IViewNode;
@@ -139,7 +143,8 @@ export interface IWorkspace {
tangoConfigJson: TangoJsonFile;
routeModule?: TangoRouteModule;
serviceModule?: TangoServiceModule;
storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void;
@@ -150,11 +155,6 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/**
* 获取项目数据
*/
getProjectData?: () => IProjectData;
/**
* 查询节点
* @param id 节点 ID
@@ -162,73 +162,54 @@ export interface IWorkspace {
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addFiles: (files: IFileConfig[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addViewPage: (name: string, code: string) => void;
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
dropNode: () => void;
insertToNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
replaceNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
addBlock?: (files: object, name: string) => void;
generateBlockFilesBySelectedNode?: () => Record<string, string>;
removeServiceFunction?: (serviceName: string) => void;
addServiceFunction?: (payload: IServiceFunctionPayload | IServiceFunctionPayload[]) => void;
updateServiceFunction?: (payload: any) => void;
updateServiceBaseConfig?: (name: string, value: any) => void;
listStoreModules?: () => TangoStoreModule[];
getServiceFunction?: (serviceKey: string) => object;
removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
addStoreModule?: (storeName: string, code: string) => void;
removeStoreModule?: (storeName: string) => void;
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// TODO: 是否需要,和 addFile 有啥区别 ???
addViewPage: (name: string, code: string) => void;
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
@@ -258,14 +239,8 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
get activeViewModule(): IViewFile;
// TODO: -> getStoreModules
get storeModules(): TangoStoreModule[];
// TODO: -> getPages
get pages(): any[];
// TODO: getBizComps
get bizComps(): string[];
// TODO: getBaseComps
get baseComps(): string[];
// TODO: getBlocks
get blocks(): any[];
}
+7 -645
View File
@@ -1,60 +1,17 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, ComponentPrototypeType, Dict, isNil, logger } from '@music163/tango-helpers';
import {
code2ast,
ast2code,
traverseRouteFile,
traverseViewFile,
traverseServiceFile,
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
updateServiceConfigToServiceFile,
removeJSXElement,
insertSiblingAfterJSXElement,
getModuleNameByFilename,
addRouteToRouteFile,
removeRouteFromRouteFile,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
deleteServiceConfigFromServiceFile,
replaceJSXElement,
formatCode,
getImportDeclarationPayloadByPrototype,
addStoreState,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
replaceRootJSXElementChildren,
updateRouteToRouteFile,
updateBaseConfigToServiceFile,
IdGenerator,
updateJSXAttributes,
} from '../helpers';
import { TangoNode } from './node';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import { code2ast, ast2code, formatCode } from '../helpers';
import { TangoFile } from './file';
import {
IRouteData,
IFileConfig,
IClassPropertyNodeData,
IServiceFunctionPayload,
ITangoViewNodeData,
IStorePropertyData,
IImportDeclarationPayload,
InsertChildPositionType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
/**
* 模块实现规范
* JS 模块实现规范
* - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法
*/
class TangoModule extends TangoFile {
export class TangoModule extends TangoFile {
ast: t.File;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
@@ -132,598 +89,3 @@ export class TangoJsModule extends TangoModule {
});
}
}
/**
* 入口配置模块
*/
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,
stores: computed,
code: computed,
cleanCode: computed,
update: 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;
}
}
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<{ package: string; isDefault?: boolean }>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
package: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { package: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
export function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 模块名
*/
name: string;
_serviceFunctions: Dict;
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(optionName: string, optionValue: any) {
this.ast = updateBaseConfigToServiceFile(this.ast, optionName, optionValue);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { JSXElement, SourceLocation } from '@babel/types';
import { cloneJSXElementWithoutTrackingData, getJSXElementAttributes } from '../helpers';
import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './module';
import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
+88
View File
@@ -0,0 +1,88 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseRouteFile,
addRouteToRouteFile,
removeRouteFromRouteFile,
updateRouteToRouteFile,
} from '../helpers';
import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
+100
View File
@@ -0,0 +1,100 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, Dict, logger } from '@music163/tango-helpers';
import {
traverseServiceFile,
updateServiceConfigToServiceFile,
getModuleNameByFilename,
deleteServiceConfigFromServiceFile,
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig, IServiceFunctionPayload } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 模块名
*/
name: string;
_serviceFunctions: Dict;
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
this.name = baseConfig.namespace;
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(data: object) {
this.ast = updateBaseConfigToServiceFile(this.ast, data);
return this;
}
}
+129
View File
@@ -0,0 +1,129 @@
import { action, computed, makeObservable, observable, toJS } 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,
stores: computed,
code: computed,
cleanCode: computed,
update: 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;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+382
View File
@@ -0,0 +1,382 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers';
import {
ast2code,
traverseViewFile,
removeJSXElement,
insertSiblingAfterJSXElement,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
replaceJSXElement,
getImportDeclarationPayloadByPrototype,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren,
IdGenerator,
updateJSXAttributes,
queryXFormItemFields,
} from '../helpers';
import { TangoNode } from './node';
import {
IFileConfig,
ITangoViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<{ package: string; isDefault?: boolean }>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 弹窗列表
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* 表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
package: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { package: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
+82 -157
View File
@@ -16,20 +16,12 @@ import {
getJSXElementChildrenNames,
namesToImportDeclarations,
getBlockNameByFilename,
queryXFormItemFields,
getPrivilegeCode,
} from '../helpers';
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node';
import {
TangoStoreEntryModule,
TangoRouteModule,
TangoServiceModule,
TangoStoreModule,
TangoViewModule,
TangoJsModule,
} from './module';
import { TangoJsModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import {
@@ -37,11 +29,14 @@ import {
FileType,
ITangoConfigPackages,
IPageConfigData,
IProjectData,
IServiceFunctionPayload,
} from '../types';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module';
export interface IWorkspaceOptions {
/**
@@ -96,14 +91,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 本地区块 { [blockName]: filePath }
* FIXME: 修正类型 Record<string, TangoBlockModule>
*/
localBlocks: Record<string, string>;
/**
* service 模块
*/
serviceModule: TangoServiceModule;
/**
* 路由配置模块
*/
@@ -114,6 +105,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
storeEntryModule: TangoStoreEntryModule;
storeModules: Record<string, TangoStoreModule> = {};
serviceModules: Record<string, TangoServiceModule> = {};
/**
* package.json 文件
*/
@@ -161,19 +156,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return this.files.get(this.activeViewFile) as TangoViewModule;
}
/**
* store 模块列表
*/
get storeModules() {
const mods: TangoStoreModule[] = [];
this.files.forEach((file) => {
if (file instanceof TangoStoreModule) {
mods.push(file);
}
});
return mods;
}
/**
* 获取页面列表
*/
@@ -270,36 +252,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return name as ComponentPrototypeType;
}
getProjectData() {
const pages = this.pages.map((item) => {
const filename = this.getFilenameByRoutePath(item.path);
return {
title: item.name,
path: item.path,
filename,
includes: {
variables: (this.getFile(filename) as TangoViewModule).variables,
},
};
});
const stores = this.storeModules.reduce((acc, item) => {
acc[item.name] = { filename: item.filename };
return acc;
}, {});
const services = {
index: {
filename: this.serviceModule?.filename,
functions: Object.keys(this.serviceModule.serviceFunctions),
},
};
const data: IProjectData = {
pages,
stores,
services,
};
return data;
}
/**
* 设置当前路由
* @param routePath 路由路径
@@ -395,10 +347,11 @@ export class Workspace extends EventTarget implements IWorkspace {
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModule = module; // 仅支持单个 service 文件
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
@@ -524,24 +477,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return ret;
}
listStoreModules() {
return Array.from(this.files.values()).filter(
(file) => file instanceof TangoStoreModule,
) as TangoStoreModule[];
}
getStoreModuleByName(storeName: string) {
let mod;
const files = this.files.values();
for (const file of files) {
if (file instanceof TangoStoreModule && file.name === storeName) {
mod = file;
break;
}
}
return mod;
}
/**
* 删除视图模块
* @param route 路由名称
@@ -641,45 +576,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
/**
* TODO: 仅获取当前视图即可
* 获取当前视图的弹窗列表
* @returns
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.activeViewModule?.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* TODO: 获取当前视图的表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.activeViewModule?.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 应用代码初始化完成
*/
@@ -696,34 +592,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
/**
* 添加区块文件
* @param files 文件
* @param name 区块文件名
*/
addBlock(files: Object, name: string) {
const blockFiles = {};
Object.keys(files).forEach((path) => {
if (!path.includes('/src')) return;
blockFiles[path.replace('/src/', '')] = files[path];
});
this.addFiles(
Object.keys(blockFiles).map((filepath) => ({
filename: `/src/blocks/${name}/${filepath}`,
code: blockFiles[filepath],
})),
);
const { dependencies } = JSON.parse(files['/package.json']);
Object.keys(dependencies).forEach((pkg) => {
this.packageJson?.setValue('dependencies', (deps) => {
if (!deps[pkg]) {
deps[pkg] = dependencies[pkg];
}
return deps;
});
});
}
/**
* 添加新的模型文件
* @param name 模型名
@@ -752,7 +620,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.getStoreModuleByName(storeName).addState(stateName, initValue).update();
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
@@ -761,7 +629,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.getStoreModuleByName(storeName).removeState(stateName).update();
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
@@ -772,26 +640,47 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [type, storeName, stateName] = variablePath.split('.');
this.getStoreModuleByName(storeName).updateState(stateName, code).update();
const [, storeName, stateName] = variablePath.split('.');
this.storeModules[storeName]?.updateState(stateName, code).update();
}
}
/**
* 获取服务函数的详情
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 更新服务函数
*/
updateServiceFunction(payload: any) {
this.serviceModule.updateServiceFunction(payload).update();
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(payload: IServiceFunctionPayload | IServiceFunctionPayload[]) {
addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) {
this.serviceModule.addServiceFunctions(payload).update();
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else {
this.serviceModule.addServiceFunction(payload).update();
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
}
}
@@ -799,15 +688,15 @@ export class Workspace extends EventTarget implements IWorkspace {
* 删除服务函数
* @param name
*/
removeServiceFunction(name: string) {
this.serviceModule.deleteServiceFunction(name).update();
removeServiceFunction(name: string, moduleName = 'index') {
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(name: string, value: any) {
this.serviceModule.updateBaseConfig(name, value).update();
updateServiceBaseConfig(config: object, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
/**
@@ -1295,4 +1184,40 @@ export class Workspace extends EventTarget implements IWorkspace {
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
}
}
/**
* 解析 serviceKey
* @param serviceKey
* @returns
*
* @example services.list => { moduleName: 'index', name: 'list' }
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
* @example foo => undefined
*/
private parseServiceKey(serviceKey: string) {
const parts = serviceKey.split('.');
if (parts[0] !== 'services') {
return {};
}
let moduleName = 'index';
let name = '';
switch (parts.length) {
case 2: {
name = parts[1];
break;
}
case 3: {
moduleName = parts[1];
name = parts[2];
break;
}
default:
break;
}
return {
moduleName,
name,
};
}
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.1...@music163/tango-designer@0.3.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.0...@music163/tango-designer@0.2.1) (2023-09-18)
**Note:** Version bump only for package @music163/tango-designer
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "0.2.1",
"version": "0.3.0",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,11 +33,11 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0",
"@music163/tango-context": "^0.1.4",
"@music163/tango-core": "^0.1.4",
"@music163/tango-context": "^0.2.0",
"@music163/tango-core": "^0.2.0",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-sandbox": "^0.1.4",
"@music163/tango-setting-form": "^0.2.1",
"@music163/tango-sandbox": "^0.1.5",
"@music163/tango-setting-form": "^0.2.2",
"@music163/tango-ui": "^0.1.4",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
@@ -352,13 +352,15 @@ interface VariableTreeProps {
onSelect?: (data: IVariableTreeNode) => void;
onAdd?: (data: IVariableTreeNode) => void;
onRemove?: (data: IVariableTreeNode) => void;
deletable?: boolean;
}
function VariableTree({
export function VariableTree({
dataSource = [],
onSelect = noop,
onAdd = noop,
onRemove = noop,
deletable = false,
}: VariableTreeProps) {
return (
<Box className="VariableTree" css={varTreeStyle}>
@@ -370,12 +372,15 @@ function VariableTree({
onSelect(detail.node);
}}
titleRender={(node) => {
if (node.showAddChildIcon || node.showRemoveIcon) {
// 只有叶子结点支持删除
const showRemoveIcon = (!node.children && node.showDeleteIcon) ?? deletable;
const showAddIcon = node.showAddChildIcon ?? false;
if (showRemoveIcon || showAddIcon) {
return (
<Box display="flex" justifyContent="space-between" alignItems="center" pr="l">
<Text>{node.title}</Text>
<Box>
{node.showAddChildIcon && (
{showAddIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
@@ -388,7 +393,7 @@ function VariableTree({
/>
</Tooltip>
)}
{node.showRemoveIcon && (
{showRemoveIcon && (
<Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
onConfirm={() => onRemove(node)}
@@ -43,7 +43,7 @@ export function EventSetter(props: EventSetterProps) {
const { actionVariables, routeOptions } = useWorkspaceData();
const workspace = useWorkspace();
const modalOptions = workspace.listModals() || [];
const modalOptions = workspace.activeViewModule.listModals() || [];
const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => {
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useMemo } from 'react';
import { observer, useWorkspace } from '@music163/tango-context';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { Box, css } from 'coral-system';
import { Button, FormProps, Empty, Space, Dropdown } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { Form, InputCode, Menu, Panel, JsonView, Search } from '@music163/tango-ui';
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import {
isNil,
isVariableString,
@@ -11,8 +11,10 @@ import {
isValidFunctionCode,
logger,
code2object,
filterTreeData,
} from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { VariableTree } from '../../components';
/*
* 服务函数的操作类型
@@ -39,7 +41,7 @@ enum ServiceFunctionMethodType {
* requestType
* headers: Content-Type
*/
enum ESFHTTRequestType {
enum DataServiceRequestType {
'application/json' = 'json',
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
}
@@ -65,11 +67,11 @@ export const getApiDefaultName = (url: string) =>
// 方法名以数字开头,添加 api 前缀
.replace(/^\d/, (str) => `api${str}`);
type DataServiceViewProps = {
interface DataServiceViewProps {
onAdd?: (values: Record<string, string> | Array<Record<string, string>>) => void;
onUpdate?: (values: Record<string, string>) => void;
onDelete?: (values: Record<string, string>) => void;
};
}
// http 方法类型
const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
@@ -78,9 +80,9 @@ const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
}));
// requestType 类型
const requestTypes = Object.keys(ESFHTTRequestType).map((key) => ({
const requestTypes = Object.keys(DataServiceRequestType).map((key) => ({
label: key,
value: ESFHTTRequestType[key],
value: DataServiceRequestType[key],
}));
const detailFormStyle = css`
@@ -90,31 +92,32 @@ const detailFormStyle = css`
`;
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
const [serviceKey, setServiceKey] = useState<string>();
const [serviceData, setServiceData] = useState<any>();
const [keyword, setKeyword] = useState<string>();
const [visible, setVisible] = useState(null);
const workspace = useWorkspace();
const sandbox = useSandboxQuery();
const { serviceVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const serviceFunctions = serviceVariables.reduce((acc, cur) => {
acc = acc.concat(cur.children.map((item: any) => item.key));
return acc;
}, []);
const dataSource = useMemo(() => {
if (!keyword) {
return serviceVariables;
}
return filterTreeData(
serviceVariables,
(leaf) => leaf.title.includes(keyword),
'children',
true,
);
}, [serviceVariables, keyword]);
const serviceMap = workspace.serviceModule.serviceFunctions;
const serviceItems = useMemo(() => {
if (!serviceMap) {
return [];
}
let list = Object.keys(serviceMap).map((key) => ({
key,
label: key,
note: serviceMap[key].app || 'http',
deletable: true,
deletableConfirm: '确定删除此服务函数吗?',
}));
if (keyword) {
list = list.filter((item) => item.label.includes(keyword));
}
return list;
}, [serviceMap, keyword]);
const serviceData = serviceMap?.[serviceKey];
const isAddMode = !serviceKey;
const isAddMode = serviceData && !serviceData.name;
return (
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
@@ -136,52 +139,48 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
<Button
block
onClick={() => {
setServiceKey(undefined);
setVisible('detail');
setServiceData({});
}}
>
</Button>
<Search placeholder="搜索服务函数" onChange={setKeyword} />
</Box>
<Menu
activeKey={serviceKey}
items={serviceItems}
onItemClick={(key) => {
setServiceKey(key);
setVisible('detail');
<VariableTree
dataSource={dataSource}
deletable
onSelect={(item) => {
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
}}
onDelete={(key) => {
const values = {
name: key,
...workspace.serviceModule.serviceFunctions[key],
};
workspace.removeServiceFunction(key);
onDelete && onDelete(values);
setVisible(null);
onRemove={(item) => {
workspace.removeServiceFunction(item.key);
onDelete?.(item);
}}
/>
</Panel>
</Box>
<Box width="65%" overflow="auto" css={detailFormStyle}>
{visible === 'detail' && (
{serviceData && (
<>
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
<ServiceDetailForm
key={serviceKey || 'createSF'}
serviceKeys={Object.keys(serviceMap)}
key={serviceData.key || 'createSF'}
serviceModules={serviceModules}
serviceKeys={serviceFunctions}
initialValues={
isAddMode
? {}
? {
moduleName: 'index',
}
: {
name: serviceKey,
name: serviceData.name,
moduleName: serviceData.moduleName,
method: 'get',
...serviceData,
...serviceData.config,
}
}
onCancel={() => {
setServiceKey(undefined);
setVisible(null);
setServiceData(undefined);
}}
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
function shapeServiceValues(val: any) {
@@ -194,28 +193,27 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
return shapeValues;
}
// 移除掉不必要的属性
const data = shapeServiceValues(values);
const { moduleName, ...data } = shapeServiceValues(values);
if (mode === ServiceFunctionOperationModeType.ADD) {
workspace.addServiceFunction(data);
workspace.addServiceFunction(data, moduleName);
onAdd && onAdd(values);
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
workspace.updateServiceFunction(data);
workspace.updateServiceFunction(data, moduleName);
onUpdate && onUpdate(values);
}
setServiceKey(values.name);
}}
/>
</Panel>
{serviceKey ? (
{serviceData?.key ? (
<ServiceFunctionPreview
key={serviceKey}
key={serviceData.key}
appContext={sandbox?.window['tango']}
functionName={serviceKey}
functionName={serviceData.key}
/>
) : null}
</>
)}
{!visible && (
{!serviceData && (
<Box py="xxl">
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
</Box>
@@ -227,12 +225,14 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
interface ServiceDetailFormProps extends FormProps {
serviceKeys?: string[];
serviceModules?: any[];
onCancel?: () => void;
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
}
function ServiceDetailForm({
serviceKeys = [],
serviceModules = [],
onCancel,
onSubmit,
initialValues,
@@ -274,6 +274,20 @@ function ServiceDetailForm({
}}
{...formProps}
>
<Form.Item
label="所属模块"
name="moduleName"
rules={[
{
required: true,
},
]}
component="select"
componentProps={{
options: serviceModules,
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法名"
name="name"
@@ -282,7 +296,9 @@ function ServiceDetailForm({
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceKeys.includes(value);
const isExist = serviceKeys.includes(
['services', form.getFieldValue('moduleName'), value].join('.'),
);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.4...@music163/tango-sandbox@0.1.5) (2023-09-21)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.3...@music163/tango-sandbox@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-sandbox
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "0.1.4",
"version": "0.1.5",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,7 +29,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.1.4",
"@music163/tango-core": "^0.2.0",
"@music163/tango-helpers": "^0.1.3",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.1...@music163/tango-setting-form@0.2.2) (2023-09-21)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.0...@music163/tango-setting-form@0.2.1) (2023-09-18)
### Bug Fixes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "0.2.1",
"version": "0.2.2",
"description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,7 +28,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.1.4",
"@music163/tango-core": "^0.2.0",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-ui": "^0.1.4",
"antd": "^4.24.2",
+4 -3
View File
@@ -7,11 +7,12 @@
"resolveJsonModule": true,
"strictFunctionTypes": true,
"paths": {
"@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-helpers": ["packages/helpers/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"],
"@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-context": ["packages/context/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-ui": ["packages/ui/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"]
}
}
}