feat: support local components (#78)

* docs: update

* feat: parse local components

* fix: update

* refactor: module exports to exportList

* fix: update workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
This commit is contained in:
Wells
2023-12-19 15:07:44 +08:00
committed by GitHub
parent f2919c1aa0
commit 6fc0498901
21 changed files with 1592 additions and 1974 deletions
+3 -3
View File
@@ -53,10 +53,10 @@ gantt
## 💻 Development
### Recommended Development Environment
### Environment
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### Development Quick Start
+2 -2
View File
@@ -55,8 +55,8 @@ gantt
### 推荐开发环境
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### 本地开发调试方法
+54 -7
View File
@@ -75,6 +75,18 @@ const tangoConfigJson = {
},
};
const helperCode = `
export function registerComponentPrototype(proto) {
if (!proto) return;
if (!window.localTangoComponentPrototypes) {
window.localTangoComponentPrototypes = {};
}
if (proto.name) {
window.localTangoComponentPrototypes[proto.name] = proto;
}
}
`;
const routesCode = `
import Index from "./pages/list";
@@ -143,7 +155,7 @@ import {
FormilyForm,
} from "@music163/antd";
import { Space } from '@music163/antd';
import { MyButton } from '../components/button';
import { LocalButton } from '../components';
class App extends React.Component {
render() {
@@ -153,7 +165,7 @@ class App extends React.Component {
</Section>
<Section>
<Space>
<MyButton />
<LocalButton />
<Button>button</Button>
<Input />
</Space>
@@ -162,18 +174,52 @@ class App extends React.Component {
);
}
}
export default definePage(App);
`;
const componentsButtonCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export function MyButton() {
return <button>my button</button>
export default function MyButton(props) {
return <button {...props}>my button</button>
}
registerComponentPrototype({
name: 'LocalButton',
title: 'Local Button',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'background', title: '背景色', setter: 'colorSetter' },
],
});
`;
const componentsPrototypeCode = ``;
const componentsInputCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyInput(props) {
return <input {...props} />;
}
registerComponentPrototype({
name: 'LocalInput',
title: 'Local Input',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'color', title: '文本色', setter: 'colorSetter' },
],
});
`;
const componentsEntryCode = `
export { default as LocalButton } from './button';
export { default as LocalInput } from './input';
`;
const storeApp = `
import { defineStore } from '@music163/tango-boot';
@@ -260,14 +306,15 @@ export const sampleFiles = [
{ filename: '/src/index.js', code: entryCode },
{ filename: '/src/pages/list.js', code: viewHomeCode },
{ filename: '/src/components/button.js', code: componentsButtonCode },
{ filename: '/src/components/prototype.js', code: componentsPrototypeCode },
{ filename: '/src/components/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode },
{ filename: '/src/routes.js', code: routesCode },
{ filename: '/src/stores/index.js', code: storeIndexCode },
{ 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() {}` },
{ filename: '/src/utils/index.js', code: helperCode },
];
export const genDefaultPage = (index: number) => ({
+3 -1
View File
@@ -33,7 +33,6 @@ const workspace = new Workspace({
// 2. 引擎初始化
const engine = createEngine({
workspace,
defaultActiveSidebarPanel: 'outline',
});
// @ts-ignore
@@ -121,6 +120,9 @@ export default function App() {
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
}
}
if (sandboxWindow.localTangoComponentPrototypes) {
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
}
setMenuLoading(false);
}
}}
+29 -1
View File
@@ -22,7 +22,7 @@ import {
code2expression,
object2node,
} from './parse';
import { isValidComponentName } from '../string';
import { getFullPath, isValidComponentName } from '../string';
import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type {
IRouteData,
@@ -32,6 +32,7 @@ import type {
IServiceFunctionPayload,
InsertChildPositionType,
IImportSpecifierData,
IExportSpecifierData,
} from '../../types';
import { IdGenerator } from '../id-generator';
@@ -1330,6 +1331,33 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
};
}
export function traverseComponentsEntryFile(ast: t.File, baseDir?: string) {
const exportMap: Record<string, IExportSpecifierData> = {};
traverse(ast, {
ExportNamedDeclaration(path) {
const node = path.node;
let source = node2value(node.source);
if (baseDir) {
// fix relative source path
source = getFullPath(baseDir, source);
}
node.specifiers.forEach((specifier) => {
if (t.isExportSpecifier(specifier)) {
const name = keyNode2value(specifier.exported) as string;
if (name) {
exportMap[name] = {
source,
exportedName: name,
};
}
}
});
},
});
return { ast, exportMap };
}
/**
* 解析导入语句
*/
+23 -12
View File
@@ -33,6 +33,10 @@ export function inferFileType(filename: string): FileType {
return FileType.JsonViewModule;
}
if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.ComponentsEntryModule;
}
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
@@ -49,10 +53,6 @@ export function inferFileType(filename: string): FileType {
return FileType.StoreModule;
}
if (/\/blocks\/[\w-]+\/index\.js/.test(filename)) {
return FileType.BlockEntryModule;
}
if (/\.jsx?$/.test(filename)) {
return FileType.Module;
}
@@ -170,24 +170,35 @@ export function getBlockNameByFilename(filename: string) {
}
/**
* FIXME: 有问题,需要优化下
* 基于 from 文件的地址计算 to 文件的相对引用路径
* @param from
* @param to
* 合并两个路径
* @param root
* @param filename
* @returns
*/
export function getRelativePath(from: string, to: string) {
const fromFolder = path.dirname(from);
return path.relative(fromFolder, to);
export function getFullPath(root: string, filename: string) {
return path.join(root, filename);
}
/**
* 计算 targetFile 在 sourceFile 中的相对引用路径
* @param sourceFile
* @param targetFile
* @returns
*/
export function getRelativePath(sourceFile: string, targetFile: string) {
sourceFile = path.dirname(sourceFile);
return path.relative(sourceFile, targetFile);
}
/**
* 判断给定字符串是否是文件路径
* @example ./pages/index.js -- yes
* @example ../pages/index.js -- yes
* @example ../components -- yes
* @example /src/pages/index.js -- yes
* @example @music163/tango-designer -- no
* @param str
*/
export function isFilepath(str: string) {
return /^(\.\.?\/|\/).*\.[a-z]+$/.test(str);
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
}
@@ -0,0 +1,40 @@
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';
/**
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
*/
export class TangoComponentsEntryModule extends TangoModule {
exportList: Record<string, IExportSpecifierData>;
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
exportList: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
const baseDir = path.dirname(this.filename);
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
this.exportList = exportMap;
Object.keys(this.exportList).forEach((key) => {
this.workspace.componentPrototypes.set(key, {
name: key,
exportType: 'namedExport',
package: baseDir,
type: 'element',
});
});
}
}
+1 -1
View File
@@ -266,5 +266,5 @@ export interface IWorkspace {
get pages(): any[];
get bizComps(): string[];
get baseComps(): string[];
get blocks(): any[];
get localComps(): string[];
}
+3 -3
View File
@@ -22,9 +22,9 @@ export class TangoModule extends TangoFile {
ast: t.File;
/**
* 导入语句
* 导入的依赖列表
*/
imports: ImportDeclarationDataType;
importList: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
@@ -95,7 +95,7 @@ export class TangoModule extends TangoFile {
_analysisAst() {
const { imports } = traverseFile(this.ast);
this.imports = imports;
this.importList = imports;
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ export class TangoServiceModule extends TangoModule {
_analysisAst() {
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
this.imports = imports;
this.importList = imports;
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
+3 -3
View File
@@ -159,7 +159,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.imports = imports;
this.importList = imports;
this.importMap = buildImportMap(imports);
this.variables = variables;
@@ -180,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 依赖列表
*/
listImportSources() {
return Object.keys(this.imports);
return Object.keys(this.importList);
}
/**
@@ -227,7 +227,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* @returns
*/
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
const existSpecifiers = this.imports[source];
const existSpecifiers = this.importList[source];
if (existSpecifiers) {
const insertedSpecifiers = newSpecifiers.filter((item) => {
return !existSpecifiers.find((existItem) => existItem.localName === item.localName);
+30 -47
View File
@@ -14,7 +14,6 @@ import {
isPathnameMatchRoute,
getJSXElementChildrenNames,
namesToImportDeclarations,
getBlockNameByFilename,
prototype2importDeclarationData,
} from '../helpers';
import { DropMethod } from './drop-target';
@@ -36,6 +35,7 @@ import { TangoRouteModule } from './route-module';
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module';
import { TangoComponentsEntryModule } from './component-module';
export interface IWorkspaceOptions {
/**
@@ -73,8 +73,14 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
files: Map<string, TangoFile>;
/**
* 组件配置
*/
componentPrototypes: Map<string, ComponentPrototypeType>;
/**
* 入口文件
*/
entry: string;
/**
@@ -92,12 +98,6 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
activeViewFile: string;
/**
* 本地区块 { [blockName]: filePath }
* TODO: 废弃这个,不再支持
*/
localBlocks: Record<string, string>;
/**
* 路由配置模块
*/
@@ -112,6 +112,8 @@ export class Workspace extends EventTarget implements IWorkspace {
serviceModules: Record<string, TangoServiceModule> = {};
componentsEntryModule: TangoComponentsEntryModule;
/**
* package.json 文件
*/
@@ -197,8 +199,8 @@ export class Workspace extends EventTarget implements IWorkspace {
return list;
}
get blocks() {
return Object.keys(this.localBlocks);
get localComps(): string[] {
return Object.keys(this.componentsEntryModule.exportList);
}
constructor(options?: IWorkspaceOptions) {
@@ -207,7 +209,6 @@ export class Workspace extends EventTarget implements IWorkspace {
this.selectSource = new SelectSource(this);
this.dragSource = new DragSource(this);
this.componentPrototypes = new Map();
this.localBlocks = {};
this.entry = options?.entry;
this.activeRoute = options?.defaultActiveRoute || '/';
this.activeFile = options?.entry;
@@ -233,8 +234,6 @@ export class Workspace extends EventTarget implements IWorkspace {
activeRoute: observable,
activeFile: observable,
activeViewFile: observable,
localBlocks: observable,
blocks: computed,
pages: computed,
bizComps: computed,
setActiveRoute: action,
@@ -330,6 +329,10 @@ export class Workspace extends EventTarget implements IWorkspace {
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.ComponentsEntryModule:
module = new TangoComponentsEntryModule(this, props);
this.componentsEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
@@ -351,19 +354,6 @@ export class Workspace extends EventTarget implements IWorkspace {
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
const prototype: ComponentPrototypeType = {
name: blockName,
exportType: 'defaultExport',
package: props.filename,
type: 'block',
};
this.localBlocks[blockName] = props.filename;
this.componentPrototypes.set(blockName, prototype);
module = new TangoViewModule(this, props);
break;
}
case FileType.Module:
module = new TangoJsModule(this, props);
break;
@@ -404,8 +394,22 @@ export class Workspace extends EventTarget implements IWorkspace {
this.storeEntryModule.addStore(storeName).update();
}
/**
* 添加视图文件
* @param viewName 文件名
* @param code 代码
*/
addViewFile(viewName: string, code: string) {
// TODO: implement it
const viewRoute = viewName.startsWith('/') ? viewName : `/${viewName}`;
const filename = `/src/pages/${viewName}.js`;
this.addFile(filename, code);
this.addRoute(
{
name: viewName,
path: viewRoute,
},
filename,
);
}
updateFile(filename: string, code: string, shouldFormatCode = false) {
@@ -504,27 +508,6 @@ export class Workspace extends EventTarget implements IWorkspace {
this.removeFile(filename);
}
/**
* 添加新的视图文件
* @deprecated 使用 addViewFile 代替
* FIXME: 重构这个逻辑
* @param route 视图名
* @param code 视图代码
*/
addViewPage(routeConfig: string | IPageConfigData, code: string) {
const config =
typeof routeConfig === 'string'
? {
name: routeConfig,
path: routeConfig.startsWith('/') ? routeConfig : `/${routeConfig}`,
}
: routeConfig;
const filename = getFilepath(config.path, '/src/pages', '.js');
this.addFile(filename, code);
this.addRoute(config, filename);
}
/**
* 添加新的路由
*/
+25 -2
View File
@@ -17,8 +17,13 @@ export enum FileType {
ComponentPrototypeModule = 'componentPrototypeModule',
// 组件运行调试入口文件,一般为 `/app.js`
ComponentDemoEntryModule = 'componentDemoEntryModule',
// 区块入口文件 FIXME: 是否移除
/**
* 本地组件目录的入口文件
*/
ComponentsEntryModule = 'componentsEntryModule',
/**
* @deprecated 已废弃
*/
BlockEntryModule = 'blockEntryModule',
// jsx 类型视图文件
@@ -81,6 +86,24 @@ export interface ITangoViewNodeData<T = JSXElement> {
children?: Array<ITangoViewNodeData<T>>;
}
/**
* 导出变量数据
*/
export interface IExportSpecifierData {
/**
* 来源
*/
source: string;
/**
* exported name
*/
exportedName: string;
/**
* local name
*/
localName?: string;
}
/**
* 导入变量的来源
*/
+7 -3
View File
@@ -129,8 +129,10 @@ describe('string helpers', () => {
expect(getRelativePath('/src/pages/index.js', '/src/blocks/sample-block/index.js')).toEqual(
'../blocks/sample-block/index.js',
);
// TODO: fix me
// expect(getRelativePath('/src/pages/', '/src/pages/index.js')).toEqual('./index.js');
expect(getRelativePath('/src/pages/index.js', '/src/components')).toEqual('../components');
expect(getRelativePath('/src/pages/index.js', '/src/components/input.js')).toEqual(
'../components/index.js',
);
});
it('getFilepath', () => {
@@ -143,7 +145,9 @@ describe('string helpers', () => {
expect(isFilepath('./pages/index.js')).toBeTruthy();
expect(isFilepath('../pages/index.js')).toBeTruthy();
expect(isFilepath('./pages/index.css')).toBeTruthy();
expect(isFilepath('/src/pages/index.js')).toBeTruthy();
expect(isFilepath('./src/pages/index.js')).toBeTruthy();
expect(isFilepath('./src/components')).toBeTruthy();
expect(isFilepath('../src/components')).toBeTruthy();
expect(isFilepath('path')).toBeFalsy();
expect(isFilepath('path-browserify')).toBeFalsy();
expect(isFilepath('@music/one')).toBeFalsy();
-1
View File
@@ -72,7 +72,6 @@ export function getElementData(
const bounding = getElementBoundingData(element, relativeContainer);
const data = getElementDataProps(element);
const dnd = parseDndId(data.dnd);
// FIXME: 是否改为按需执行,仅 dropTarget 需要 display 信息,且目前 display 信息并不准确
const display = getElementCSSDisplay(element);
return {
id: dnd.id,
@@ -13,7 +13,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import { Button, Empty, Spin, Popover } from 'antd';
import { getDragGhostElement } from '../helpers';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'block' | 'bizComp';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
type MenuValueType = Array<{ title: string; items: string[] }>;
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
@@ -26,10 +26,6 @@ export interface ComponentsPanelProps {
* 展示业务组件分类
*/
showBizComps?: boolean;
/**
* 展示基础组件分类
*/
showBlocks?: boolean;
/**
* 动态加载物料 loading,避免未加载完成用户点击空列表
*/
@@ -47,7 +43,7 @@ const localeMap = {
atom: '原子组件',
snippet: '组合',
bizComp: '业务组件',
block: '区块',
localComp: '本地组件',
};
const emptyMenuData: MenuDataType = {
@@ -78,19 +74,18 @@ export const ComponentsPanel = observer(
({
menuData = emptyMenuData,
showBizComps = true,
showBlocks = true,
getBizCompName = upperCamelCase,
loading = false,
}: ComponentsPanelProps) => {
const [keyword, setKeyword] = useState<string>('');
const allList = useFlatMenuData<MenuDataType>(menuData);
const workspace = useWorkspace();
const [bizCompData, blockData] = useMemo(
const [bizCompData, localCompData] = useMemo(
() => [
[{ title: '已安装业务组件', items: workspace.bizComps.map(getBizCompName) }],
[{ title: '项目区块', items: workspace.blocks }],
[{ title: '本地组件', items: workspace.localComps }],
],
[workspace.bizComps, workspace.blocks, getBizCompName],
[workspace.bizComps, workspace.localComps, getBizCompName],
);
const tabs = Object.keys(menuData).map((key) => ({
@@ -106,11 +101,12 @@ export const ComponentsPanel = observer(
children: <MaterialList type="bizComp" data={bizCompData} />,
});
}
if (showBlocks) {
if (localCompData.length) {
tabs.push({
key: 'block',
label: localeMap.block,
children: <MaterialList type="block" data={blockData} />,
key: 'localComps',
label: localeMap.localComp,
children: <MaterialList data={localCompData} />,
});
}
const contentNode =
@@ -145,7 +141,7 @@ interface MaterialListProps {
/**
* 物料类型
*/
type?: 'common' | 'bizComp' | 'block';
type?: 'common' | 'bizComp' | 'localComp';
}
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
@@ -175,7 +171,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
)}
<Grid
columns={type === 'block' ? 1 : 2}
columns={type === 'localComp' ? 1 : 2}
spacing="1px"
bg="background.normal"
padding="0"
@@ -185,9 +181,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
if (!prototype) {
logger.log(`<${item}> prototype not found!`);
}
return prototype ? (
<MaterialGrid key={item} data={prototype} type={type} />
) : null;
return prototype ? <MaterialGrid key={item} data={prototype} /> : null;
})}
{addBlock && <Box bg="white" />}
</Grid>
@@ -201,7 +195,6 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
interface MaterialProps {
data: ComponentPrototypeType;
type: 'common' | 'atom' | 'snippet' | 'bizComp' | 'block';
}
const StyledCommonGridItem = styled.div`
@@ -219,11 +212,6 @@ const StyledCommonGridItem = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
&[class~='block-item'] {
flex-direction: row;
justify-content: flex-start;
}
.material-icon {
font-size: 40px;
}
@@ -260,7 +248,7 @@ const StyledCommonGridItem = styled.div`
}
`;
function MaterialGrid({ data, type }: MaterialProps) {
function MaterialGrid({ data }: MaterialProps) {
const workspace = useWorkspace();
const handleDragStart = (e: React.DragEvent) => {
@@ -286,7 +274,6 @@ function MaterialGrid({ data, type }: MaterialProps) {
data-name={data.name}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
className={type === 'block' ? 'block-item' : null}
>
{icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
@@ -296,7 +283,7 @@ function MaterialGrid({ data, type }: MaterialProps) {
<Text fontSize="12px" lineHeight="1.5">
{data.title}
</Text>
<Text fontSize="12px" color={type === 'block' ? 'inherit' : 'gray.50'}>
<Text fontSize="12px" color="gray.50">
{data.name}
</Text>
{data.docs || data.help ? (
@@ -366,19 +366,19 @@ function RenderItem({
</Popconfirm>,
]
: baseNeedUpgrade
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
]
: [
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
{!isUndefined(baseNeedUpgrade) && '已是最新'}
</Text>,
]
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
]
: [
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
{!isUndefined(baseNeedUpgrade) && '已是最新'}
</Text>,
]
}
>
<List.Item.Meta
@@ -453,7 +453,7 @@ function AddDependencyModal({
const onFinishCallback = (name?: string) => {
name && message.success(`${name} 添加成功`);
// FIXME: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
// TIP: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
sandbox.reload();
off();
};
+1 -1
View File
@@ -43,7 +43,7 @@ describe('string', () => {
expect(isVariableString('{"hello"}')).toBeTruthy();
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
expect(isVariableString('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
// expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // FIXME: fix this case
// expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // TIP: failed
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
});
+4 -1
View File
@@ -109,7 +109,10 @@ export function createFormItem(options: IFormItemCreateOptions) {
let expProps = {};
// FIXME: 重新考虑这段代码的位置,外置这个逻辑
if (['expressionSetter', 'actionSetter', 'eventSetter'].includes(setter) || isVariable) {
if (
['expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(setter) ||
isVariable
) {
expProps = {
modalTitle: title,
modalTip: tip,
+26 -12
View File
@@ -1,8 +1,26 @@
import React, { useMemo } from 'react';
import React from 'react';
import styled from 'styled-components';
import cx from 'classnames';
import { Tabs as AntTabs, TabsProps as AntTabsProps } from 'antd';
const defaultTabBarStyle: React.CSSProperties = { paddingLeft: 12, paddingRight: 12, margin: 0 };
const centerTabBarStyle: React.CSSProperties = { margin: 0 };
const StyledTabs = styled(AntTabs)<any>`
.ant-tabs-nav {
padding-left: 12px;
padding-right: 12px;
margin: 0;
}
&.sticky .ant-tabs-nav {
position: sticky;
top: ${(props) => props.$stickyOffset};
z-index: 2;
background: #fff;
}
&.ant-tabs-centered .ant-tabs-nav {
margin: 0;
}
`;
export interface TabsProps extends AntTabsProps {
isTabBarSticky?: boolean;
@@ -13,22 +31,18 @@ export function Tabs({
centered,
isTabBarSticky = false,
tabBarStickyOffset = 0,
className,
...rest
}: TabsProps) {
const tabBarStyle = useMemo(() => {
let ret = centered ? centerTabBarStyle : defaultTabBarStyle;
if (isTabBarSticky) {
ret = { ...ret, position: 'sticky', top: tabBarStickyOffset, zIndex: 2, background: '#fff' };
}
return ret;
}, [centered, isTabBarSticky, tabBarStickyOffset]);
const classNames = cx(className, { sticky: isTabBarSticky });
return (
<AntTabs
<StyledTabs
size="small"
tabBarGutter={24}
centered={centered}
tabBarStyle={tabBarStyle}
className={classNames}
$stickyOffset={tabBarStickyOffset}
{...rest}
/>
);
+1308 -1831
View File
File diff suppressed because it is too large Load Diff