feat: add isError and errorMessage to File & add isAstSynced to Module (#190)

* feat: update file without sync ast

* feat: parse and display file errors

* fix: update

* fix: refactor toolbar

* fix: add activeViews config to toolbarItem

* fix: update icon
This commit is contained in:
Wells
2024-07-30 20:54:05 +08:00
committed by GitHub
parent 82bc5db4d8
commit 8dd289cd7d
25 changed files with 325 additions and 160 deletions
+5 -5
View File
@@ -113,8 +113,8 @@ export default function App() {
actions={
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="addPage" placement="left">
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
<Action
tooltip="添加页面"
shape="outline"
@@ -122,10 +122,10 @@ export default function App() {
onClick={() => setShowNewPageModal(true)}
/>
</Toolbar.Item>
<Toolbar.Item key="history" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator />
<Toolbar.Item placement="right">
<Space>
+4 -16
View File
@@ -3,7 +3,7 @@
*/
import generator, { GeneratorOptions } from '@babel/generator';
import * as t from '@babel/types';
import { logger, wrapCode } from '@music163/tango-helpers';
import { Dict, logger, wrapCode } from '@music163/tango-helpers';
import { formatCode } from '../string';
const defaultGeneratorOptions: GeneratorOptions = {
@@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
return code;
}
const bracketPattern = /^\(.+\)$/s;
/**
* 是否被 () 包裹
*
* @example ({ foo: 'foo' }) -> true
* @example { foo: 'foo' } -> false
*
* @param str 目标字符串
*/
function isWrappingWithBrackets(str: string) {
return bracketPattern.test(str);
}
const bracketPattern = /^\(.+\)$/;
/**
* 将表达式生成为块级代码
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
if (isWrappingExpression && bracketPattern.test(ret)) {
// 如果是对象,输出包含 ({}),则去掉首尾的括号
ret = ret.slice(1, -1);
}
@@ -195,7 +183,7 @@ export function node2value(node: t.Node, isWrapCode = true): any {
);
if (isSimpleObject) {
// simple object: { key1, key2, key3 }
ret = node.properties.reduce((prev, propertyNode) => {
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
if (propertyNode.type === 'ObjectProperty') {
const key = keyNode2value(propertyNode.key);
const value = node2value(propertyNode.value, isWrapCode);
+2 -6
View File
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
* @returns
*/
export function code2ast(code: string): t.File {
try {
return parse(code, babelParserConfig);
} catch (err) {
logger.error('[code2ast failed!]', err);
}
return parse(code, babelParserConfig);
}
/**
@@ -175,7 +171,7 @@ export function value2node(
* 将 js 普通对象解析为 t.Node
*/
export function object2node(
obj: object,
obj: Dict,
getValueNode: (value: any, key?: string) => t.Expression = value2node,
) {
if (!isPlainObject(obj)) {
+2 -2
View File
@@ -1009,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
});
}
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
@@ -1017,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
const configNode = path.node.arguments[0];
if (t.isObjectExpression(configNode)) {
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
const newPropertiesNodeMap = Object.keys(config).reduce<Dict>((properties, key) => {
const serviceConfig = config[key];
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
properties[key] = property;
+1 -1
View File
@@ -13,7 +13,7 @@ export class TangoComponentsEntryModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
+11
View File
@@ -24,6 +24,16 @@ export class TangoFile {
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
@@ -40,6 +50,7 @@ export class TangoFile {
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) {
+18 -2
View File
@@ -10,6 +10,7 @@ import {
IPageConfigData,
IImportSpecifierSourceData,
IImportSpecifierData,
IFileError,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module';
@@ -192,7 +193,18 @@ export interface IWorkspace {
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => 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;
@@ -298,8 +310,12 @@ export interface IWorkspace {
get bizComps(): string[];
get baseComps(): string[];
get localComps(): string[];
get fileErrors(): IFileError[];
/**
* 是否是合法的项目
* 是否是有效的项目
* - 包含 tango.config.json
* - 包含视图模块
* - 没有文件错误
*/
get isValid(): boolean;
}
+60 -18
View File
@@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile {
ast: t.File;
/**
* ast 是否与 code 保持同步
*/
isAstSynced: boolean;
/**
* 导入的依赖列表
*/
@@ -33,23 +38,53 @@ export class TangoModule extends TangoFile {
/**
* 基于最新的 ast 进行同步
* @param code 如果传入 code,则基于 code 进行同步
* @param isFormatCode 是否格式化代码
* @param refreshWorkspace 是否刷新 workspace
* @param isSyncAst 是否同步 ast
* @param isRefreshWorkspace 是否刷新 workspace
*/
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
this.lastModified = Date.now();
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isFormatCode);
try {
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isSyncAst);
}
if (isSyncAst) {
this._analysisAst();
}
this.isAstSynced = isSyncAst;
this.isError = false;
this.errorMessage = undefined;
this.workspace.onFilesChange([this.filename]);
if (isRefreshWorkspace) {
this.workspace.refresh([this.filename]);
}
} catch (err: any) {
this.isError = true;
this.errorMessage = err.message;
}
}
this._analysisAst();
this.workspace.onFilesChange([this.filename]);
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
/**
* 基于当前的代码重新生成 ast
*/
updateAst() {
if (!this.isAstSynced) {
try {
this.ast = code2ast(this._code);
this._analysisAst();
this.isAstSynced = true;
this.isError = false;
this.errorMessage = undefined;
} catch (err: any) {
this.isAstSynced = false;
this.isError = true;
this.errorMessage = err.message;
}
}
}
@@ -75,22 +110,26 @@ export class TangoModule extends TangoFile {
/**
* 基于输入的源码进行同步
* @param code 源码
* @param isFormatCode 是否格式化代码
* @param isSyncAst 是否同步 ast
* @returns
*/
_syncByCode(code: string, isFormatCode = true) {
_syncByCode(code: string, isSyncAst = true) {
if (code === this._code) {
return;
}
// 提前格式化代码
if (isFormatCode) {
try {
code = formatCode(code);
} catch (err) {
// err ignored, format code failed
}
this._code = code;
this._cleanCode = code;
this.ast = code2ast(code);
if (isSyncAst) {
this.ast = code2ast(code);
}
}
_analysisAst() {
@@ -105,14 +144,17 @@ export class TangoModule extends TangoFile {
export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, 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,
});
}
}
+5 -1
View File
@@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { logger } from '@music163/tango-helpers';
import {
traverseRouteFile,
addRouteToRouteFile,
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
_routes: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
logger.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
updateAst: action,
});
}
+3
View File
@@ -31,10 +31,13 @@ export class TangoStoreEntryModule extends TangoModule {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
+5 -1
View File
@@ -138,10 +138,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
@@ -320,7 +324,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
const newImportData = relatedImports.reduce((prev, name) => {
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
const proto = this.workspace.getPrototype(name);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source];
+34 -15
View File
@@ -25,10 +25,10 @@ import {
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node';
import { TangoJsModule } from './module';
import { TangoJsModule, TangoModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
@@ -151,11 +151,8 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
private copyTempNodes: TangoNode[];
/**
* 是否是合法的项目
*/
get isValid() {
return !!this.tangoConfigJson && !!this.activeViewModule;
return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0;
}
/**
@@ -223,6 +220,19 @@ export class Workspace extends EventTarget implements IWorkspace {
return Object.keys(this.componentsEntryModule?.exportList || {});
}
get fileErrors() {
const errors: IFileError[] = [];
this.files.forEach((file) => {
if (file.isError) {
errors.push({
filename: file.filename,
message: file.errorMessage,
});
}
});
return errors;
}
constructor(options?: IWorkspaceOptions) {
super();
this.history = new TangoHistory(this);
@@ -256,6 +266,8 @@ export class Workspace extends EventTarget implements IWorkspace {
activeViewFile: observable,
pages: computed,
bizComps: computed,
fileErrors: computed,
isValid: computed,
setActiveRoute: action,
setActiveFile: action,
addFile: action,
@@ -439,11 +451,15 @@ export class Workspace extends EventTarget implements IWorkspace {
);
}
updateFile(filename: string, code: string, shouldFormatCode = false) {
updateFile(filename: string, code: string, isSyncAst = true) {
const file = this.getFile(filename);
file.update(code);
if (file instanceof TangoModule) {
file.update(code, isSyncAst);
} else {
file.update(code);
}
const shouldFormat = shouldFormatCode ?? this.projectConfig?.designerConfig?.autoFormatCode;
const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode;
if (shouldFormat && file instanceof TangoViewModule) {
file.removeUnusedImportSpecifiers().update();
}
@@ -455,12 +471,15 @@ export class Workspace extends EventTarget implements IWorkspace {
});
}
/**
* 删除工作区的文件
* @param filename
*/
syncFiles() {
this.files.forEach((file) => {
if (file instanceof TangoModule) {
file.updateAst();
}
});
}
removeFile(filename: string) {
// TODO: refactor visitFile to share this logic
if (this.files.get(filename)) {
// 如果是文件,直接删除
this.files.delete(filename);
@@ -516,7 +535,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns { [filename]: fileCode }
*/
listFiles() {
const ret = {};
const ret: Dict<string> = {};
this.files.forEach((file) => {
ret[file.filename] = file.cleanCode;
});
+5
View File
@@ -57,6 +57,11 @@ export interface IFileConfig {
type?: FileType;
}
export interface IFileError {
filename: string;
message: string;
}
/**
* 视图节点数据类型
*/
@@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { Box, css } from 'coral-system';
import { observer, useWorkspace } from '@music163/tango-context';
import { CloseOutlined } from '@ant-design/icons';
const errorMessageStyle = css`
padding: 24px;
white-space: pre-wrap;
`;
/**
* 文件错误提示浮层
*/
export const FileErrorsOverlay = observer(() => {
const [isVisible, setIsVisible] = useState<boolean>(true);
const workspace = useWorkspace();
const handleClose = () => {
setIsVisible(false);
};
if (workspace.fileErrors.length === 0) {
return null;
}
if (!isVisible) {
return null;
}
return (
<Box
className="WorkspaceErrorOverlay"
position="absolute"
top="0"
left="0"
right="0"
height="100%"
bg="rgba(244, 244, 244, 0.9)"
color="red"
zIndex={1999}
>
<Box position="relative" p="4">
<CloseOutlined
style={{
position: 'absolute',
top: 12,
right: 12,
color: '#000',
}}
onClick={handleClose}
/>
<Box css={errorMessageStyle}>
<Box fontSize="20px" mb="l">
</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>
</Box>
))}
</Box>
</Box>
</Box>
);
});
@@ -4,3 +4,4 @@ export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';
export * from './components-popover';
export * from './file-errors-overlay';
+5 -33
View File
@@ -2,8 +2,6 @@ import React, { useRef, useEffect, useCallback } from 'react';
import { Box } from 'coral-system';
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { isValidCode } from '@music163/tango-core';
import { Modal } from 'antd';
const ideConfig = {
// disableFileOps: {
@@ -21,14 +19,9 @@ const ideConfig = {
// disableSetting: true,
};
export interface CodeEditorProps extends Partial<MultiEditorProps> {
/**
* 是否自动清除未使用的导入
*/
autoRemoveUnusedImports?: boolean;
}
export type CodeEditorProps = Partial<MultiEditorProps>;
export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEditorProps) => {
export const CodeEditor = observer((props: CodeEditorProps) => {
const editorRef = useRef(null);
const workspace = useWorkspace();
const designer = useDesigner();
@@ -47,26 +40,9 @@ export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEd
const fileSave = useCallback(
(path: string, value: string) => {
if (!isJsFile(path)) {
// 非 js 文件直接保存
workspace.updateFile(path, value, autoRemoveUnusedImports);
return;
}
// js 文件需要先检查语法,只有语法正确才会保存
if (isValidCode(value)) {
workspace.updateFile(path, value, autoRemoveUnusedImports);
} else {
Modal.confirm({
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
onOk: () => {
editorRef.current?.refresh(files, activeFile);
},
onCancel: () => {},
});
}
workspace.updateFile(path, value, false);
},
[workspace, autoRemoveUnusedImports, activeFile, files],
[workspace],
);
const handleFileChange = useCallback(
@@ -123,12 +99,8 @@ export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEd
defaultPath={activeFile}
defaultTheme="GithubLightDefault"
defaultFiles={files}
{...rest}
{...props}
/>
</Box>
);
});
function isJsFile(path: string) {
return /.jsx?$/.test(path);
}
+28 -18
View File
@@ -3,7 +3,7 @@ import cx from 'classnames';
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
import { Badge } from 'antd';
import { ReactComponentProps } from '@music163/tango-helpers';
import { observer, useDesigner } from '@music163/tango-context';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { ResizableBox } from './resizable-box';
import { getWidget } from '../widgets';
@@ -19,15 +19,22 @@ const sidebarStyle = css`
list-style: none;
user-select: none;
transition: all 0.3s ease;
&.disabled {
pointer-events: none;
}
}
.SidebarPanelBarListItem {
color: var(--tango-colors-text2);
&.active {
background-color: var(--tango-colors-custom-sidebarItemActiveBg);
color: var(--tango-colors-brand);
background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2);
}
&:hover {
background-color: var(--tango-colors-custom-sidebarItemHoverBg);
background-color: var(--tango-colors-custom-sidebarItemHoverBg, --tango-colors-fill2);
}
}
`;
@@ -76,6 +83,7 @@ export interface SidebarPanelItemProps
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
const designer = useDesigner();
const workspace = useWorkspace();
const items = useMemo(() => {
const ret: Record<string, SidebarPanelItemProps> = {};
@@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
return ret;
}, [children]);
if (designer.activeView !== 'design') {
return null;
}
return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
<Box as="ul" flex="0" p="0" m="0" textAlign="center" className="SidebarPanelBarList">
<Box
as="ul"
flex="0"
p="0"
m="0"
textAlign="center"
className={cx('SidebarPanelBarList', {
disabled: workspace.fileErrors.length > 0,
})}
>
{Object.keys(items).map((key) => {
const item = items[key];
const isActive = key === designer.activeSidebarPanel;
@@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
icon={item.icon}
label={item.label}
showBadge={item.showBadge}
isActive={isActive}
/>
</li>
);
@@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({
export const Sidebar = observer(BaseSidebarPanel);
interface SidebarPanelBarItemProps {
/**
* 是否选中
*/
isActive?: boolean;
/**
* 侧边栏图标
*/
@@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps {
};
}
function SidebarPanelBarItem({
isActive,
icon: iconProp,
label,
showBadge,
}: SidebarPanelBarItemProps) {
const color = isActive ? 'brand' : 'text.body';
function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) {
let icon = (
<Text fontSize="24px" lineHeight={1} color={color}>
<Text fontSize="24px" lineHeight={1}>
{iconProp}
</Text>
);
@@ -241,7 +251,7 @@ function SidebarPanelBarItem({
py="12px"
>
{icon}
<Text fontSize="12px" mt="s" color={color}>
<Text fontSize="12px" mt="s">
{label}
</Text>
</Box>
+3 -2
View File
@@ -5,11 +5,12 @@ import { observer, useWorkspace } from '@music163/tango-context';
export const HistoryTool = observer(() => {
const workspace = useWorkspace();
const disabled = !workspace.isValid;
return (
<Box display="flex" columnGap="m">
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldBack || workspace.history.index === 0}
disabled={disabled || !workspace.history.couldBack || workspace.history.index === 0}
tooltip="撤销"
onClick={() => {
workspace.history.back();
@@ -20,7 +21,7 @@ export const HistoryTool = observer(() => {
</ToggleButton>
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldForward}
disabled={disabled || !workspace.history.couldForward}
tooltip="重做"
onClick={() => {
workspace.history.forward();
+2 -15
View File
@@ -1,6 +1,5 @@
import React, { useCallback } from 'react';
import React from 'react';
import { Group } from 'coral-system';
import { Modal } from 'antd';
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { BorderOutlined } from '@ant-design/icons';
@@ -9,23 +8,13 @@ export const ModeSwitchTool = observer(() => {
const workspace = useWorkspace();
const designer = useDesigner();
const activeFileCheck = useCallback(() => {
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}, [workspace]);
return (
<Group attached>
<ToggleButton
shape="ghost"
selected={designer.activeView === 'design'}
onClick={() => {
workspace.syncFiles(); // 保证 ast 与 code 是同步的
designer.setActiveView('design');
}}
tooltip="设计视图"
@@ -38,7 +27,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}}
tooltip="源码视图"
>
@@ -50,7 +38,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('dual'); // 切换到双屏视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}}
tooltip="双屏视图"
>
+3 -1
View File
@@ -1,13 +1,15 @@
import React from 'react';
import { ToggleButton } from '@music163/tango-ui';
import { EyeOutlined } from '@ant-design/icons';
import { observer, useDesigner } from '@music163/tango-context';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
export const PreviewTool = observer(() => {
const designer = useDesigner();
const workspace = useWorkspace();
return (
<ToggleButton
disabled={!workspace.isValid}
shape="ghost"
selected={designer.isPreview}
onClick={() => {
+46 -16
View File
@@ -1,6 +1,8 @@
import React, { useMemo } from 'react';
import { Box, Group } from 'coral-system';
import { ReactComponentProps } from '@music163/tango-helpers';
import { DesignerViewType } from '@music163/tango-core';
import { observer, useDesigner } from '@music163/tango-context';
import { getWidget } from '../widgets';
export interface ToolbarProps {
@@ -15,25 +17,23 @@ export function Toolbar({ children }: ToolbarProps) {
let prevPlacement: string;
React.Children.forEach(children, (child: React.ReactElement, index) => {
let fallbackNode;
const childProps = child.props;
let fallbackChildren;
if (child.key) {
const Widget = getWidget(['toolbar', child.key].join('.'));
if (Widget) {
fallbackNode = React.createElement(Widget, child.props.widgetProps);
fallbackChildren = <Widget {...childProps.widgetProps} />;
}
}
let node = child.props.children ?? fallbackNode ?? null;
if (!node) {
node = child; // separator
}
const key = child.key || index;
node = (
<div key={key} className="ToolbarPanelItem" data-key={key}>
{node}
</div>
);
const placement = child.props.placement || prevPlacement || 'center';
const node = React.cloneElement(child, {
key,
'data-key': key,
children: childProps.children || fallbackChildren,
});
const placement = childProps.placement || prevPlacement || 'center';
switch (placement) {
case 'left':
left.push(node);
@@ -68,17 +68,47 @@ export function Toolbar({ children }: ToolbarProps) {
}
export interface ToolbarItemProps extends ReactComponentProps {
/**
* 类型
*/
type?: 'divider';
placement?: 'left' | 'center' | 'right';
children?: React.ReactElement;
/**
* 如果 key 匹配到内置组件的话,传递给子节点的属性
*/
widgetProps?: object;
/**
* 工具箱展示的视图
*/
activeViews?: DesignerViewType[];
}
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
return <div>{children}</div>;
}
const ToolbarItem = observer(
({
activeViews = ['code', 'design', 'dual'],
type,
placement,
widgetProps,
children,
...rest
}: ToolbarItemProps) => {
const designer = useDesigner();
if (!activeViews.includes(designer.activeView)) {
return null;
}
if (type === 'divider') {
return <Separator />;
}
return (
<div className="ToolbarPanelItem" {...rest}>
{children}
</div>
);
},
);
function Separator() {
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
+1 -1
View File
@@ -30,7 +30,7 @@ import {
const widgets = {};
export function registerWidget(key: string, component: React.ComponentType<any>) {
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) {
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-Z]+$/.test(key)) {
throw new Error(
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
);
+9 -7
View File
@@ -1,35 +1,37 @@
import React from 'react';
import { Box } from 'coral-system';
import { Box, HTMLCoralProps } from 'coral-system';
import { observer, useDesigner } from '@music163/tango-context';
import { DesignerViewType } from '@music163/tango-core';
import { ComponentsPopover } from './components';
import cx from 'classnames';
import { ComponentsPopover, FileErrorsOverlay } from './components';
export interface WorkspaceViewProps {
export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
/**
* 视图面板模式,对应 Workspace 的模式
*/
mode?: DesignerViewType;
children: React.ReactNode;
}
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
const { mode = 'design', children } = props;
const { mode = 'design', children, className, ...rest } = props;
const designer = useDesigner();
const display = mode !== designer.activeView ? 'none' : 'block';
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕
// 移动端模式小屏幕适配,可能会溢出屏幕
const overflow =
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
return (
<Box
className={`ViewPanel ${mode}`}
className={cx('ViewPanel', mode, className)}
display={designer.activeView === 'dual' ? 'block' : display}
flex="1"
overflow={overflow}
position="relative"
{...rest}
>
{children}
{/* 添加组件弹层 */}
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
{mode === 'design' && <FileErrorsOverlay />}
</Box>
);
});
@@ -8,6 +8,7 @@ const PackageOutlinedSvg = () => (
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
>
<path d="M915.2 284.8c0-3.2 0-3.2-3.2-6.4 0-3.2-3.2-3.2-6.4-6.4-9.6-12.8-22.4-25.6-38.4-35.2l-297.6-169.6c-35.2-22.4-80-22.4-118.4 0L313.6 147.2c-6.4 3.2-12.8 6.4-16 12.8L153.6 240c-12.8 9.6-25.6 22.4-38.4 35.2 0 3.2-3.2 3.2-3.2 6.4s0 3.2-3.2 6.4c-6.4 16-12.8 35.2-12.8 54.4v342.4c0 41.6 22.4 80 57.6 102.4l297.6 169.6c12.8 6.4 28.8 12.8 41.6 16 3.2 3.2 9.6 3.2 16 3.2s9.6-3.2 16-3.2c16-3.2 28.8-6.4 41.6-16l297.6-169.6c35.2-22.4 57.6-60.8 57.6-102.4V342.4c6.4-19.2 0-38.4-6.4-57.6z m-428.8-160c9.6-3.2 16-6.4 25.6-6.4 9.6 0 19.2 3.2 25.6 6.4L832 291.2l-128 73.6-316.8-182.4 99.2-57.6z m25.6 352L192 291.2l128-73.6 316.8 182.4-124.8 76.8zM185.6 729.6c-16-9.6-25.6-28.8-25.6-48V345.6l320 185.6V896l-294.4-166.4z m652.8 0L544 896V531.2l320-185.6v336c0 19.2-9.6 38.4-25.6 48z" />
</svg>