mirror of
https://github.com/NetEase/tango.git
synced 2026-09-05 23:48:19 +08:00
Compare commits
14 Commits
hotfix/071
...
@music163/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55bf417ddf | ||
|
|
ca56edd9ae | ||
|
|
65149f1326 | ||
|
|
939466648d | ||
|
|
8fe57333a0 | ||
|
|
269e304d68 | ||
|
|
8c80255f81 | ||
|
|
359aa0b148 | ||
|
|
51ca4a72ce | ||
|
|
b83d965955 | ||
|
|
a1eea02f94 | ||
|
|
0d0479e3e5 | ||
|
|
67d602a3ec | ||
|
|
8dd289cd7d |
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Group } from 'coral-system';
|
||||
import { Box, Group, Text } from 'coral-system';
|
||||
import { Avatar, Space, Switch } from 'antd';
|
||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
@@ -66,12 +66,12 @@ export function Logo() {
|
||||
export function ProjectDetail() {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" columnGap="l">
|
||||
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
|
||||
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
|
||||
community-test
|
||||
</Box>
|
||||
<Box className="BranchName" as="code" fontSize="14px">
|
||||
</Text>
|
||||
<Text className="BranchName" as="code" fontSize="14px" truncated>
|
||||
<BranchesOutlined /> feature/list
|
||||
</Box>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ import { OutButton } from "../components";
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={111}>
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0" />
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
LiveCodeEditor,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
@@ -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>
|
||||
@@ -188,7 +188,7 @@ export default function App() {
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="code">
|
||||
<LiveCodeEditor />
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
"test:watch": "jest --watch",
|
||||
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
||||
"publish": "lerna publish",
|
||||
"ver": "lerna version --no-private",
|
||||
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
|
||||
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
|
||||
"ver": "lerna version --no-private --conventional-commits",
|
||||
"release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
|
||||
"release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
|
||||
"up": "yarn upgrade-interactive --latest",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.7](https://github.com/netease/tango/compare/@music163/tango-context@1.1.6...@music163/tango-context@1.1.7) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.6](https://github.com/netease/tango/compare/@music163/tango-context@1.1.5...@music163/tango-context@1.1.6) (2024-08-01)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.8",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -29,10 +29,6 @@ export const useDesigner = () => {
|
||||
return useTangoEngine()?.engine.designer;
|
||||
};
|
||||
|
||||
export const useEditorState = () => {
|
||||
return useTangoEngine()?.engine.workspace.editorState;
|
||||
};
|
||||
|
||||
export const useWorkspaceData = () => {
|
||||
const ctx = useTangoEngine();
|
||||
const workspace = useWorkspace();
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- view module \_nodesTree set init value ([#194](https://github.com/netease/tango/issues/194)) ([269e304](https://github.com/netease/tango/commit/269e304d685c4fcc80635b9d04220cda333b80c4))
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-core@1.4.0...@music163/tango-core@1.4.1) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-core@1.3.3...@music163/tango-core@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.2",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -35,7 +35,7 @@ export function isTangoVariable(name: string) {
|
||||
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
|
||||
}
|
||||
|
||||
const templatePattern = /^{(.+)}$/s;
|
||||
const templatePattern = /^{(.+)}$/;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||
|
||||
@@ -25,11 +25,6 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否被 () 包裹
|
||||
* @example ({ foo: 'foo' }) -> true
|
||||
* @example { foo: 'foo' } -> false
|
||||
*/
|
||||
const bracketPattern = /^\(.+\)$/;
|
||||
|
||||
/**
|
||||
|
||||
@@ -171,7 +171,7 @@ export function value2node(
|
||||
* 将 js 普通对象解析为 t.Node
|
||||
*/
|
||||
export function object2node(
|
||||
obj: Dict<any>,
|
||||
obj: Dict,
|
||||
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
||||
) {
|
||||
if (!isPlainObject(obj)) {
|
||||
|
||||
@@ -1017,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>
|
||||
const configNode = path.node.arguments[0];
|
||||
|
||||
if (t.isObjectExpression(configNode)) {
|
||||
const newPropertiesNodeMap: Dict = Object.keys(config).reduce<Dict>((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;
|
||||
|
||||
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
|
||||
names: string[],
|
||||
nameMap: Dict<IImportSpecifierSourceData>,
|
||||
) {
|
||||
const map = {};
|
||||
const map: Dict = {};
|
||||
names.forEach((name) => {
|
||||
const mod = nameMap[name];
|
||||
if (mod) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseDndId, uuid } from '@music163/tango-helpers';
|
||||
import { Dict, parseDndId, uuid } from '@music163/tango-helpers';
|
||||
|
||||
// { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
|
||||
export function deepCloneNode(obj: any, component?: string): any {
|
||||
function deepCloneObject() {
|
||||
const target = {};
|
||||
const target: Dict = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
target[key] = deepCloneNode(obj[key], component);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
|
||||
export type SimulatorNameType = 'desktop' | 'phone';
|
||||
|
||||
@@ -185,7 +185,6 @@ export class Designer {
|
||||
// 默认激活的视图
|
||||
if (defaultActiveView) {
|
||||
this.setActiveView(defaultActiveView);
|
||||
this.workspace.setMode(defaultActiveView === 'design' ? 'design' : 'code');
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
@@ -239,24 +238,14 @@ export class Designer {
|
||||
}
|
||||
|
||||
setActiveView(view: DesignerViewType) {
|
||||
const prevView = this._activeView;
|
||||
if (view === prevView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._activeView = view;
|
||||
if (view === 'dual') {
|
||||
this.setActiveSidebarPanel('');
|
||||
this.toggleRightPanel(false);
|
||||
this.toggleIsPreview(true);
|
||||
this.workspace.setMode('code');
|
||||
} else if (view === 'design') {
|
||||
this.toggleIsPreview(false);
|
||||
this.workspace.setMode('design');
|
||||
} else if (view === 'code') {
|
||||
this.workspace.setMode('code');
|
||||
}
|
||||
|
||||
this._activeView = view;
|
||||
}
|
||||
|
||||
setActiveSidebarPanel(panel: string) {
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { FileType, IFileConfig } from '../types';
|
||||
|
||||
interface EditorActionPayload {
|
||||
filename: string;
|
||||
newFilename?: string;
|
||||
code: string;
|
||||
type?: FileType;
|
||||
}
|
||||
|
||||
type AddFilePayload = Omit<EditorActionPayload, 'newFileName'>;
|
||||
type DeleteFilePayload = Pick<EditorActionPayload, 'filename'>;
|
||||
type RenameFilePayload = Pick<EditorActionPayload, 'filename' | 'newFilename'>;
|
||||
type UpdateFilePayload = Pick<EditorActionPayload, 'filename' | 'code'>;
|
||||
|
||||
interface EditorStateConfig {
|
||||
defaultActiveFile: string;
|
||||
files: IFileConfig[];
|
||||
}
|
||||
|
||||
export class EditorState {
|
||||
_files: Map<string, Pick<IFileConfig, 'code' | 'type'>> = new Map();
|
||||
|
||||
_activeFile: string;
|
||||
|
||||
_history: EditorHistory;
|
||||
|
||||
get activeFile() {
|
||||
return this._activeFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件是否存在变更记录
|
||||
*/
|
||||
get isFilesChanged() {
|
||||
return this._history.size > 1;
|
||||
}
|
||||
|
||||
constructor(options?: EditorStateConfig) {
|
||||
this._history = new EditorHistory();
|
||||
this._activeFile = options?.defaultActiveFile;
|
||||
this.addFiles(options?.files);
|
||||
|
||||
makeObservable(this, {
|
||||
_files: observable,
|
||||
_activeFile: observable,
|
||||
activeFile: computed,
|
||||
setActiveFile: action,
|
||||
updateFile: action,
|
||||
addFile: action,
|
||||
addFiles: action,
|
||||
deleteFile: action,
|
||||
deleteFolder: action,
|
||||
_renameFile: action,
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._files.clear();
|
||||
this._history.clear();
|
||||
}
|
||||
|
||||
setActiveFile(filename: string) {
|
||||
this._activeFile = filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文件列表数组
|
||||
* @returns
|
||||
*/
|
||||
listFileData() {
|
||||
const list: IFileConfig[] = [];
|
||||
this._files.forEach((value, filename) => {
|
||||
list.push({
|
||||
...value,
|
||||
filename,
|
||||
});
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文件列表对象
|
||||
* @returns 文件列表对象 { [filename]: code }
|
||||
*/
|
||||
getFiles(): Record<string, string> {
|
||||
const fileCodes: Record<string, string> = {};
|
||||
this._files.forEach((fileData, filename) => {
|
||||
fileCodes[filename] = fileData.code;
|
||||
});
|
||||
return fileCodes;
|
||||
}
|
||||
|
||||
addFile({ filename, ...rest }: AddFilePayload) {
|
||||
this._files.set(filename, rest);
|
||||
|
||||
this._history.push({
|
||||
message: 'addFile',
|
||||
data: [
|
||||
{
|
||||
filename,
|
||||
...rest,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
addFiles(fileList: AddFilePayload[]) {
|
||||
if (!fileList || !fileList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
fileList.forEach(({ filename, ...rest }) => {
|
||||
this._files.set(filename, rest);
|
||||
});
|
||||
|
||||
this._history.push({
|
||||
message: 'addFiles',
|
||||
data: [...fileList],
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile({ filename }: DeleteFilePayload) {
|
||||
const file = this._files.get(filename);
|
||||
this._files.delete(filename);
|
||||
|
||||
this._history.push({
|
||||
message: 'deleteFile',
|
||||
data: [
|
||||
{
|
||||
filename,
|
||||
...file,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
deleteFolder(payload: DeleteFilePayload) {
|
||||
const deleteFiles: AddFilePayload[] = [];
|
||||
const fileNames = Object.keys(this._files);
|
||||
for (const filename of fileNames) {
|
||||
if (filename.startsWith(payload.filename)) {
|
||||
const file = this._files.get(filename);
|
||||
deleteFiles.push({
|
||||
filename,
|
||||
...file,
|
||||
});
|
||||
this._files.delete(filename);
|
||||
}
|
||||
}
|
||||
|
||||
this._history.push({
|
||||
message: 'deleteFolder',
|
||||
data: deleteFiles,
|
||||
});
|
||||
}
|
||||
|
||||
_renameFile({ filename, newFilename }: RenameFilePayload) {
|
||||
const fileData = this._files.get(filename);
|
||||
this._files.set(newFilename, { ...fileData });
|
||||
this._files.delete(filename);
|
||||
}
|
||||
|
||||
renameFile(payload: RenameFilePayload) {
|
||||
this._renameFile(payload);
|
||||
|
||||
this._history.push({
|
||||
message: 'renameFile',
|
||||
data: [payload],
|
||||
});
|
||||
}
|
||||
|
||||
renameFolder({ filename: oldFolderName, newFilename: newFolderName }: RenameFilePayload) {
|
||||
const fileNames = Object.keys(this._files);
|
||||
for (const filename of fileNames) {
|
||||
if (filename.startsWith(oldFolderName)) {
|
||||
const newFilename = filename.replace(oldFolderName, newFolderName);
|
||||
this._renameFile({ filename, newFilename });
|
||||
}
|
||||
}
|
||||
|
||||
this._history.push({
|
||||
message: 'renameFolder',
|
||||
data: [
|
||||
{
|
||||
filename: oldFolderName,
|
||||
newFilename: newFolderName,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
updateFile({ filename, code }: UpdateFilePayload) {
|
||||
const fileData = this._files.get(filename);
|
||||
if (fileData.code === code) {
|
||||
// 代码未发生变化
|
||||
return;
|
||||
}
|
||||
|
||||
fileData.code = code;
|
||||
this._files.set(filename, fileData);
|
||||
|
||||
this._history.push({
|
||||
message: 'updateFile',
|
||||
data: [
|
||||
{
|
||||
filename,
|
||||
code: fileData.code,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface EditorHistoryRecord {
|
||||
data: Array<Partial<AddFilePayload>>;
|
||||
time: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 实现上需要要 TangoHistory 合并
|
||||
*/
|
||||
class EditorHistory {
|
||||
_records: EditorHistoryRecord[] = [];
|
||||
_index = 0;
|
||||
_maxSize = 100;
|
||||
|
||||
get size() {
|
||||
return this._records.length;
|
||||
}
|
||||
|
||||
push(payload: Omit<EditorHistoryRecord, 'time'>) {
|
||||
if (this._index < this.size - 1) {
|
||||
this._records = this._records.slice(0, this._index + 1);
|
||||
}
|
||||
this._index = this.size;
|
||||
this._records.push({
|
||||
time: Date.now(),
|
||||
...payload,
|
||||
});
|
||||
|
||||
if (this.size > this._maxSize) {
|
||||
this._records.splice(0, this.size - this._maxSize);
|
||||
this._index = this.size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._records = [];
|
||||
this._index = 0;
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ export class TangoFile {
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件是否存在错误
|
||||
* 文件解析是否出错
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件的错误消息
|
||||
* 文件解析错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
import { EditorState } from './editor-state';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
@@ -138,26 +137,16 @@ export interface IViewNode {
|
||||
get loc(): unknown;
|
||||
}
|
||||
|
||||
export type WorkspaceMode = 'design' | 'code';
|
||||
|
||||
export interface IWorkspace {
|
||||
history: TangoHistory;
|
||||
selectSource: SelectSource;
|
||||
dragSource: DragSource;
|
||||
editorState: EditorState;
|
||||
/**
|
||||
* 工作区模式: design-设计模式, code-源码模式
|
||||
*/
|
||||
mode: WorkspaceMode;
|
||||
|
||||
files: Map<string, TangoFile>;
|
||||
componentPrototypes: Map<string, IComponentPrototype>;
|
||||
|
||||
entry: string;
|
||||
/**
|
||||
* @deprecated 已废弃,使用 editor.activeFile 代替
|
||||
*/
|
||||
activeFile?: string;
|
||||
activeFile: string;
|
||||
activeViewFile: string;
|
||||
activeRoute: string;
|
||||
|
||||
@@ -185,16 +174,8 @@ export interface IWorkspace {
|
||||
ready: () => void;
|
||||
refresh: (names: string[]) => void;
|
||||
|
||||
setMode: (mode: WorkspaceMode) => void;
|
||||
|
||||
setActiveRoute: (path: string) => void;
|
||||
|
||||
/**
|
||||
* @deprecated 已废弃,使用 editor.setActiveFile 代替
|
||||
* @param filename
|
||||
* @returns
|
||||
*/
|
||||
setActiveFile?: (filename: string) => void;
|
||||
setActiveFile: (filename: string) => void;
|
||||
|
||||
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
|
||||
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
|
||||
@@ -208,14 +189,23 @@ export interface IWorkspace {
|
||||
addViewFile: (viewName: string, code: string) => void;
|
||||
|
||||
removeFile: (filename: string) => void;
|
||||
clearFiles: () => void;
|
||||
|
||||
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;
|
||||
|
||||
listFileData: () => IFileConfig[];
|
||||
listFiles: () => Record<string, string>;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
|
||||
@@ -320,12 +310,12 @@ export interface IWorkspace {
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
/**
|
||||
* 文件错误列表
|
||||
*/
|
||||
get fileErrors(): IFileError[];
|
||||
/**
|
||||
* 是否是合法的项目
|
||||
* 是否是有效的项目
|
||||
* - 包含 tango.config.json
|
||||
* - 包含视图模块
|
||||
* - 没有文件错误
|
||||
*/
|
||||
get isValid(): boolean;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces';
|
||||
export class TangoModule extends TangoFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* ast 是否与 code 保持同步
|
||||
*/
|
||||
isAstSynced: boolean;
|
||||
|
||||
/**
|
||||
* 导入的依赖列表
|
||||
*/
|
||||
@@ -33,26 +38,29 @@ 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();
|
||||
|
||||
try {
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isFormatCode);
|
||||
this._syncByCode(code, isSyncAst);
|
||||
}
|
||||
this._analysisAst();
|
||||
|
||||
if (isSyncAst) {
|
||||
this._analysisAst();
|
||||
}
|
||||
|
||||
this.isAstSynced = isSyncAst;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
|
||||
if (refreshWorkspace) {
|
||||
if (isRefreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -61,6 +69,25 @@ export class TangoModule extends TangoFile {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前的代码重新生成 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
|
||||
this.ast = addImportDeclaration(this.ast, source, specifiers);
|
||||
return this;
|
||||
@@ -83,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() {
|
||||
@@ -113,7 +144,7 @@ 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,
|
||||
@@ -123,6 +154,7 @@ export class TangoJsModule extends TangoModule {
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export class TangoRouteModule extends TangoModule {
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ export class TangoServiceModule extends TangoModule {
|
||||
cleanCode: computed,
|
||||
code: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export class TangoStoreEntryModule extends TangoModule {
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
*/
|
||||
export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
// 解析为树结构的 jsxNodes 数组
|
||||
_nodesTree: ITangoViewNodeData[];
|
||||
_nodesTree: ITangoViewNodeData[] = [];
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
@@ -145,6 +145,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
cleanCode: computed,
|
||||
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ 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, WorkspaceMode } from './interfaces';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
@@ -37,7 +37,6 @@ import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
import { EditorState } from './editor-state';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -66,27 +65,9 @@ export interface IWorkspaceOptions {
|
||||
* 工作区
|
||||
*/
|
||||
export class Workspace extends EventTarget implements IWorkspace {
|
||||
/**
|
||||
* 历史记录
|
||||
*/
|
||||
history: TangoHistory;
|
||||
/**
|
||||
* 选中源
|
||||
*/
|
||||
selectSource: SelectSource;
|
||||
/**
|
||||
* 拖拽源
|
||||
*/
|
||||
dragSource: DragSource;
|
||||
/**
|
||||
* 编辑器状态
|
||||
*/
|
||||
editorState: EditorState;
|
||||
|
||||
/**
|
||||
* 工作区模式
|
||||
*/
|
||||
mode: WorkspaceMode;
|
||||
|
||||
/**
|
||||
* 工作区的文件列表
|
||||
@@ -108,6 +89,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeRoute: string;
|
||||
|
||||
/**
|
||||
* 当前选中的文件
|
||||
*/
|
||||
activeFile: string;
|
||||
|
||||
/**
|
||||
* 当前选中的视图文件
|
||||
*/
|
||||
@@ -165,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,16 +221,16 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
|
||||
get fileErrors() {
|
||||
const list: IFileError[] = [];
|
||||
const errors: IFileError[] = [];
|
||||
this.files.forEach((file) => {
|
||||
if (file.isError) {
|
||||
list.push({
|
||||
errors.push({
|
||||
filename: file.filename,
|
||||
errorMessage: file.errorMessage,
|
||||
message: file.errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
return list;
|
||||
return errors;
|
||||
}
|
||||
|
||||
constructor(options?: IWorkspaceOptions) {
|
||||
@@ -255,14 +238,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
this.history = new TangoHistory(this);
|
||||
this.selectSource = new SelectSource(this);
|
||||
this.dragSource = new DragSource(this);
|
||||
this.editorState = new EditorState({
|
||||
defaultActiveFile: options?.entry,
|
||||
files: options?.files,
|
||||
});
|
||||
this.mode = 'design';
|
||||
this.componentPrototypes = new Map();
|
||||
this.entry = options?.entry;
|
||||
this.activeRoute = options?.defaultActiveRoute || '/';
|
||||
this.activeFile = options?.entry;
|
||||
this.activeViewFile = '';
|
||||
this.files = new Map();
|
||||
this.isReady = false;
|
||||
@@ -281,50 +260,21 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
mode: observable,
|
||||
files: observable,
|
||||
activeRoute: observable,
|
||||
activeFile: observable,
|
||||
activeViewFile: observable,
|
||||
pages: computed,
|
||||
bizComps: computed,
|
||||
fileErrors: computed,
|
||||
isValid: computed,
|
||||
setActiveRoute: action,
|
||||
setActiveFile: action,
|
||||
addFile: action,
|
||||
removeFile: action,
|
||||
setMode: action,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工作区模式
|
||||
*/
|
||||
setMode(mode: WorkspaceMode) {
|
||||
const prevMode = this.mode;
|
||||
if (prevMode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mode = mode;
|
||||
|
||||
if (mode === 'design') {
|
||||
// 从源码切换到设计模式
|
||||
if (this.editorState.isFilesChanged) {
|
||||
this.clearFiles();
|
||||
this.addFiles(this.editorState.listFileData());
|
||||
}
|
||||
} else {
|
||||
// 从设计切换到源码模式
|
||||
|
||||
if (this.fileErrors.length) {
|
||||
// 工作区文件存在语法错误,不同步
|
||||
return;
|
||||
}
|
||||
|
||||
this.editorState.clear();
|
||||
this.editorState.addFiles(this.listFileData());
|
||||
}
|
||||
}
|
||||
|
||||
getPrototype(name: string | IComponentPrototype) {
|
||||
if (isString(name)) {
|
||||
return this.componentPrototypes.get(name);
|
||||
@@ -345,6 +295,17 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
this.setActiveViewFile(routePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前选中的文件
|
||||
* @param filename
|
||||
*/
|
||||
setActiveFile(filename: string, isViewFile = false) {
|
||||
this.activeFile = filename;
|
||||
if (isViewFile) {
|
||||
this.activeViewFile = filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前的路由计算当前的视图模块
|
||||
*/
|
||||
@@ -360,7 +321,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
}
|
||||
if (filename) {
|
||||
this.editorState.setActiveFile(filename);
|
||||
this.setActiveFile(filename, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,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();
|
||||
}
|
||||
@@ -506,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);
|
||||
@@ -526,10 +494,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
clearFiles() {
|
||||
this.files.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
* @param oldFilename
|
||||
@@ -578,22 +542,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表(FileData 参数形式)
|
||||
* @returns Array<{ filename, code, type }>
|
||||
*/
|
||||
listFileData() {
|
||||
const ret: IFileConfig[] = [];
|
||||
this.files.forEach((file) => {
|
||||
ret.push({
|
||||
filename: file.filename,
|
||||
code: file.cleanCode,
|
||||
type: file.type,
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除视图模块
|
||||
* @param route 路由名称
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface IFileConfig {
|
||||
|
||||
export interface IFileError {
|
||||
filename: string;
|
||||
errorMessage: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('expression2code: arrowFunctionExpression', () => {
|
||||
expect(expression2code(code2expression('() => {\n\n}\n'))).toEqual('() => {\n\n}');
|
||||
expect(expression2code(code2expression('() => {}'))).toEqual('() => {}');
|
||||
});
|
||||
|
||||
it('expression2code: memberExpression', () => {
|
||||
|
||||
@@ -3,6 +3,35 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.2...@music163/tango-designer@1.4.3) (2024-08-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- file errors overlay support click to error file & optimize ui ([#197](https://github.com/netease/tango/issues/197)) ([ca56edd](https://github.com/netease/tango/commit/ca56edd9ae201dbeb84b03ad390a09085433a257))
|
||||
- route matching without query string & toggle button wrapping ([#195](https://github.com/netease/tango/issues/195)) ([9394666](https://github.com/netease/tango/commit/939466648d40331800f15e886256a0789b7e7623))
|
||||
- update mobile simulator ui ([#196](https://github.com/netease/tango/issues/196)) ([65149f1](https://github.com/netease/tango/commit/65149f1326eaf78e8a27222993c3cacc674cd378))
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.1...@music163/tango-designer@1.4.2) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.0...@music163/tango-designer@1.4.1) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
- show right panel when design view select node & optimize editor css ([#193](https://github.com/netease/tango/issues/193)) ([b83d965](https://github.com/netease/tango/commit/b83d965955c8d33ed8d37063482c7abe079e62b1))
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.3...@music163/tango-designer@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.3",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.5",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-sandbox": "^1.0.8",
|
||||
"@music163/tango-setting-form": "^1.2.8",
|
||||
"@music163/tango-ui": "^1.3.3",
|
||||
"@music163/tango-context": "^1.1.8",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-sandbox": "^1.0.11",
|
||||
"@music163/tango-setting-form": "^1.2.12",
|
||||
"@music163/tango-ui": "^1.4.2",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
133
packages/designer/src/components/file-errors-overlay.tsx
Normal file
133
packages/designer/src/components/file-errors-overlay.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, css, Text } from 'coral-system';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
const errorMessageStyle = css`
|
||||
padding: 35px;
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
color: #f45755;
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
background-color: #171717;
|
||||
border-radius: 8px;
|
||||
.top-bar {
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
background-color: #f45755;
|
||||
}
|
||||
.WorkspaceErrorOverlayItem {
|
||||
font-size: 18px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
.WorkspaceErrorOverlayItemFilename {
|
||||
color: #48cacb;
|
||||
text-decoration: underline;
|
||||
letter-spacing: 1px;
|
||||
&:hover {
|
||||
color: #51b1ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件错误提示浮层
|
||||
*/
|
||||
export const FileErrorsOverlay = observer(() => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleOpenErrorFile = (fileName: string) => {
|
||||
workspace.setActiveFile(fileName);
|
||||
designer.setActiveView('code');
|
||||
};
|
||||
|
||||
if (workspace.fileErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlay"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
height="100%"
|
||||
bg="rgb(84, 84, 84)"
|
||||
padding="30px"
|
||||
color="red"
|
||||
overflow="auto"
|
||||
zIndex={1999}
|
||||
>
|
||||
<Box position="relative" p="4" boxShadow="0 0 10px rgba(0, 0, 0, 0.5)">
|
||||
<CloseOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 24,
|
||||
right: 12,
|
||||
color: '#fff',
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Box css={errorMessageStyle}>
|
||||
<Box className="top-bar" />
|
||||
<Box fontSize="18px" mb="l">
|
||||
<Text letterSpacing=".5px" color="#c29ef5">
|
||||
[plugin: @babel/parser]
|
||||
</Text>
|
||||
<Text marginLeft={'8px'} color="#f45755">
|
||||
SyntaxError in {workspace.fileErrors.length} files,Click file to modify the error
|
||||
code
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
{workspace.fileErrors.map((fileError, index) => (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlayItem"
|
||||
key={fileError.filename}
|
||||
borderBottom={index === workspace.fileErrors.length - 1 ? 'none' : 'dashed'}
|
||||
borderBottomColor="line2"
|
||||
py="l"
|
||||
>
|
||||
<Box
|
||||
className="WorkspaceErrorOverlayItemFilename"
|
||||
fontWeight="bold"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handleOpenErrorFile(fileError.filename)}
|
||||
>
|
||||
{fileError.filename}
|
||||
</Box>
|
||||
<Box
|
||||
as="code"
|
||||
display="block"
|
||||
color="#daa15e"
|
||||
fontSize="16px"
|
||||
fontWeight={'normal'}
|
||||
>
|
||||
{fileError.message}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
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: 12px;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件错误提示浮层
|
||||
*/
|
||||
export const FilesErrorOverlay = 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}>
|
||||
{workspace.fileErrors.map((fileError) => (
|
||||
<Box key={fileError.filename}>
|
||||
<Box fontWeight="bold">{fileError.filename}</Box>
|
||||
<Box as="code" display="block">
|
||||
{fileError.errorMessage}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -4,4 +4,4 @@ export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
export * from './components-popover';
|
||||
export * from './files-error-overlay';
|
||||
export * from './file-errors-overlay';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { Dict, isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface AddServiceFormProps extends FormProps {
|
||||
initialValues?: object;
|
||||
initialValues?: Dict;
|
||||
onSubmit: (values: object) => void;
|
||||
onCancel: () => void;
|
||||
serviceModules: object[];
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
/**
|
||||
* 快捷键
|
||||
*/
|
||||
export class Hotkey {
|
||||
private readonly hotkeyMap = {};
|
||||
private readonly hotkeyMap: Dict = {};
|
||||
|
||||
constructor(hotkeys: Record<string, Function>) {
|
||||
Object.keys(hotkeys).forEach((hotkey) => {
|
||||
|
||||
@@ -137,6 +137,8 @@ export function useDnd({
|
||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||
if (data && data.id) {
|
||||
selectSource.select(data);
|
||||
// 画布选中节点时,自动唤起 right-panel
|
||||
designer.toggleRightPanel(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { Box } from 'coral-system';
|
||||
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
||||
import { observer, useDesigner, useEditorState, useWorkspace } from '@music163/tango-context';
|
||||
import { LiveEditorSandbox } from './sandbox';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
const ideConfig = {
|
||||
// disableFileOps: {
|
||||
@@ -20,22 +19,14 @@ const ideConfig = {
|
||||
// disableSetting: true,
|
||||
};
|
||||
|
||||
export interface CodeEditorProps extends Partial<MultiEditorProps> {
|
||||
/**
|
||||
* 是否自动清除未使用的导入
|
||||
* @deprecated 已废弃,不再支持
|
||||
*/
|
||||
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();
|
||||
const editorState = useEditorState();
|
||||
const files = editorState.getFiles();
|
||||
|
||||
const activeFile = editorState.activeFile;
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
@@ -49,48 +40,48 @@ export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEd
|
||||
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
editorState.updateFile({ filename: path, code: value });
|
||||
workspace.updateFile(path, value, false);
|
||||
},
|
||||
[editorState],
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
editorState.addFile({ filename: info.path, code: info.value });
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
editorState.deleteFile({ filename: info.path });
|
||||
break;
|
||||
case 'deleteFolder':
|
||||
editorState.deleteFolder({ filename: info.path });
|
||||
workspace.removeFile(info.path);
|
||||
break;
|
||||
case 'renameFile':
|
||||
editorState.renameFile({ filename: info.path, newFilename: info.newpath });
|
||||
workspace.renameFile(info.path, info.newpath);
|
||||
workspace.setActiveFile(info.newpath);
|
||||
break;
|
||||
case 'renameFolder':
|
||||
editorState.renameFolder({ filename: info.path, newFilename: info.newpath });
|
||||
workspace.renameFolder(info.path, info.newpath);
|
||||
break;
|
||||
case 'addFolder':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[editorState],
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
editorState.setActiveFile(path);
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[editorState],
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const borderStyle =
|
||||
designer.activeView === 'dual'
|
||||
? {
|
||||
borderLeft: 'solid 1px var(--tango-colors-line2)',
|
||||
borderRight: 'solid 1px var(--tango-colors-line2)',
|
||||
}
|
||||
: {};
|
||||
|
||||
@@ -109,38 +100,8 @@ export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEd
|
||||
defaultPath={activeFile}
|
||||
defaultTheme="GithubLightDefault"
|
||||
defaultFiles={files}
|
||||
{...rest}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
const liveEditorWrapperStyle = css`
|
||||
.LiveCodeEditorLeft,
|
||||
.LiveCodeEditorRight {
|
||||
overflow: hidden;
|
||||
transition: flex 100ms ease-in-out 0s;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LiveCodeEditor = observer(() => {
|
||||
const designer = useDesigner();
|
||||
return (
|
||||
<Box display="flex" className="LiveCodeEditor" height="100%" css={liveEditorWrapperStyle}>
|
||||
<Box
|
||||
flex={designer.activeView === 'dual' ? '55 1 0px' : '100 1 0px'}
|
||||
className="LiveCodeEditorLeft"
|
||||
>
|
||||
<CodeEditor />
|
||||
</Box>
|
||||
<Box
|
||||
flex={designer.activeView === 'dual' ? '45 1 0px' : '0 1 0px'}
|
||||
borderLeft="solid"
|
||||
borderLeftColor="line2"
|
||||
className="LiveCodeEditorRight"
|
||||
>
|
||||
<LiveEditorSandbox />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ const navigatorStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export interface NavigatorProps {
|
||||
interface NavigatorProps {
|
||||
disabled?: boolean;
|
||||
startRoute?: string;
|
||||
onBack?: () => void;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
|
||||
import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { Dict, getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
|
||||
import { Simulator, Viewport } from '../simulator';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { DndQuery, useDnd } from '../dnd';
|
||||
import { Navigator, NavigatorProps } from './navigator';
|
||||
import { Navigator } from './navigator';
|
||||
import { SelectionToolsProps } from '../simulator/selection';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
|
||||
|
||||
@@ -18,7 +18,6 @@ interface ISandboxEventHandlerConfig {
|
||||
}
|
||||
|
||||
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
|
||||
files?: CodeSandboxProps['files'];
|
||||
isPreview?: boolean;
|
||||
selectionTools?: SelectionToolsProps['actions'];
|
||||
/**
|
||||
@@ -55,7 +54,6 @@ function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||
}
|
||||
|
||||
function useSandbox({
|
||||
files: filesProp,
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
filesFormatter,
|
||||
@@ -90,29 +88,14 @@ function useSandbox({
|
||||
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
|
||||
});
|
||||
|
||||
let files = filesProp;
|
||||
if (!files) {
|
||||
files = Array.from(workspace.files.keys()).reduce<CodeSandboxProps['files']>(
|
||||
(prev, filename) => {
|
||||
const code = workspace.getFile(filename).code;
|
||||
prev[filename] = { code };
|
||||
return prev;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
// fix tangoConfigJson
|
||||
const tangoConfigJson = files['/tango.config.json'];
|
||||
if (tangoConfigJson) {
|
||||
files['/tango.config.json'] = {
|
||||
code: mergeTangoConfigJson(tangoConfigJson.code, {
|
||||
isPreview,
|
||||
injectScript,
|
||||
formatter: configFormatter,
|
||||
}),
|
||||
};
|
||||
}
|
||||
let files = Array.from(workspace.files.keys()).reduce<Dict>((prev, filename) => {
|
||||
let code = workspace.getFile(filename).code;
|
||||
if (filename === '/tango.config.json') {
|
||||
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
|
||||
}
|
||||
prev[filename] = { code };
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
files = fixSandboxFiles(files, workspace.entry);
|
||||
if (filesFormatter) {
|
||||
@@ -320,60 +303,16 @@ export const CombinedSandbox = observer(
|
||||
},
|
||||
);
|
||||
|
||||
const FALLBACK_SANDBOX_BUNDLER_URL = 'https://codesandbox.fn.netease.com';
|
||||
|
||||
interface UseSandboxNavigatorProps {
|
||||
sandbox?: any;
|
||||
bundlerUrl?: string;
|
||||
}
|
||||
|
||||
function useSandboxNavigator({ sandbox, bundlerUrl }: UseSandboxNavigatorProps): NavigatorProps {
|
||||
const workspace = useWorkspace();
|
||||
return {
|
||||
disabled: !sandbox?.iframe,
|
||||
startRoute: workspace.activeRoute,
|
||||
onInputEnter: (newUrl) => {
|
||||
if (newUrl?.split('?')[0] === workspace.activeRoute) {
|
||||
// 只是输入了路由参数,默认为调试模式,直接更新沙箱内的路由地址
|
||||
sandbox?.updateRoute(newUrl);
|
||||
} else {
|
||||
// 如果是输入了其他路由,则直接跳转
|
||||
workspace.setActiveRoute(newUrl);
|
||||
}
|
||||
},
|
||||
onBack: () => {
|
||||
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlback' });
|
||||
},
|
||||
onForward: () => {
|
||||
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlforward' });
|
||||
},
|
||||
onRefresh: () => {
|
||||
if (!sandbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!canAccessIFrame(sandbox.iframe) ||
|
||||
sandbox.iframe.contentWindow.origin !== sandbox.props.bundlerURL
|
||||
) {
|
||||
sandbox.iframe.src = `${bundlerUrl}${workspace.activeRoute}`;
|
||||
} else {
|
||||
sandbox.manager.iframeProtocol.dispatch({ type: 'refresh' });
|
||||
}
|
||||
workspace.selectSource.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const Sandbox = observer(
|
||||
({
|
||||
isPreview: isPreviewProp,
|
||||
selectionTools,
|
||||
bundlerURL = FALLBACK_SANDBOX_BUNDLER_URL,
|
||||
bundlerURL,
|
||||
mode = 'combined',
|
||||
navigatorExtra,
|
||||
...props
|
||||
}: SandboxProps) => {
|
||||
const bundlerUrl = bundlerURL ?? 'https://codesandbox.fn.netease.com';
|
||||
const lastScrollTopRef = useRef<number>();
|
||||
const sandboxRef = useRef<CodeSandbox>(null);
|
||||
const combinedSandboxRef = useRef<CombinedSandboxRef>(null);
|
||||
@@ -381,6 +320,13 @@ export const Sandbox = observer(
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
let sandbox = sandboxRef.current;
|
||||
if (mode === 'combined') {
|
||||
sandbox = designer.isPreview
|
||||
? combinedSandboxRef.current?.previewSandbox
|
||||
: combinedSandboxRef.current?.designSandbox;
|
||||
}
|
||||
|
||||
const onViewChange = (data: any, { isActive }: ISandboxEventHandlerConfig) => {
|
||||
if (isActive) {
|
||||
navigatorRef.current.changeRelativeUrl(data?.pathname + data?.search);
|
||||
@@ -394,24 +340,51 @@ export const Sandbox = observer(
|
||||
}
|
||||
};
|
||||
|
||||
let sandbox = sandboxRef.current;
|
||||
if (mode === 'combined') {
|
||||
sandbox = designer.isPreview
|
||||
? combinedSandboxRef.current?.previewSandbox
|
||||
: combinedSandboxRef.current?.designSandbox;
|
||||
}
|
||||
const navigatorProps = useSandboxNavigator({ sandbox, bundlerUrl: bundlerURL });
|
||||
|
||||
return (
|
||||
<Box className="SandboxContainer" height="100%">
|
||||
<Navigator ref={navigatorRef} extra={navigatorExtra} {...navigatorProps} />
|
||||
<Navigator
|
||||
ref={navigatorRef}
|
||||
disabled={!sandbox?.iframe}
|
||||
startRoute={workspace.activeRoute}
|
||||
onInputEnter={(newUrl) => {
|
||||
if (newUrl?.split('?')[0] === workspace.activeRoute) {
|
||||
// 只是输入了路由参数,默认为调试模式,直接更新沙箱内的路由地址
|
||||
sandbox?.updateRoute(newUrl);
|
||||
} else {
|
||||
// 如果是输入了其他路由,则直接跳转
|
||||
workspace.setActiveRoute(newUrl);
|
||||
}
|
||||
}}
|
||||
onBack={() => {
|
||||
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlback' });
|
||||
}}
|
||||
onForward={() => {
|
||||
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlforward' });
|
||||
}}
|
||||
onRefresh={() => {
|
||||
if (!sandbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!canAccessIFrame(sandbox.iframe) ||
|
||||
sandbox.iframe.contentWindow.origin !== sandbox.props.bundlerURL
|
||||
) {
|
||||
sandbox.iframe.src = `${bundlerUrl}${workspace.activeRoute}`;
|
||||
} else {
|
||||
sandbox.manager.iframeProtocol.dispatch({ type: 'refresh' });
|
||||
}
|
||||
workspace.selectSource.clear();
|
||||
}}
|
||||
extra={navigatorExtra}
|
||||
/>
|
||||
<Simulator>
|
||||
<Viewport selectionTools={selectionTools}>
|
||||
{mode === 'single' && (
|
||||
<DesignSandbox
|
||||
ref={sandboxRef}
|
||||
template="create-react-app"
|
||||
bundlerURL={bundlerURL}
|
||||
bundlerURL={bundlerUrl}
|
||||
entry={workspace.entry}
|
||||
onViewChange={onViewChange}
|
||||
onMessage={onMessage}
|
||||
@@ -424,7 +397,7 @@ export const Sandbox = observer(
|
||||
<CombinedSandbox
|
||||
ref={combinedSandboxRef}
|
||||
template="create-react-app"
|
||||
bundlerURL={bundlerURL}
|
||||
bundlerURL={bundlerUrl}
|
||||
entry={workspace.entry}
|
||||
onViewChange={onViewChange}
|
||||
onMessage={onMessage}
|
||||
@@ -438,54 +411,6 @@ export const Sandbox = observer(
|
||||
},
|
||||
);
|
||||
|
||||
export const LiveEditorSandbox = observer(
|
||||
({ bundlerURL = FALLBACK_SANDBOX_BUNDLER_URL, navigatorExtra, ...props }: SandboxProps) => {
|
||||
const sandboxRef = useRef<CodeSandbox>(null);
|
||||
const workspace = useWorkspace();
|
||||
const { display, ...sandboxProps } = useSandbox({
|
||||
files: normalizeFiles(workspace.editorState.getFiles()),
|
||||
isPreview: true,
|
||||
sandboxType: 'preview',
|
||||
});
|
||||
const navigatorProps = useSandboxNavigator({
|
||||
sandbox: sandboxRef.current,
|
||||
bundlerUrl: bundlerURL,
|
||||
});
|
||||
return (
|
||||
<Box className="LiveEditorSandbox" height="100%">
|
||||
<Navigator extra={navigatorExtra} {...navigatorProps} />
|
||||
<Simulator>
|
||||
<CodeSandbox
|
||||
ref={sandboxRef}
|
||||
iframeId="live-editor-sandbox"
|
||||
template="create-react-app"
|
||||
bundlerURL={bundlerURL}
|
||||
entry={workspace.entry}
|
||||
startRoute={workspace.activeRoute}
|
||||
{...sandboxProps}
|
||||
{...props}
|
||||
/>
|
||||
</Simulator>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function normalizeFiles(files: Record<string, any>) {
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result: CodeSandboxProps['files'] = {};
|
||||
Object.keys(files).forEach((filename) => {
|
||||
const file = files[filename];
|
||||
result[filename] = {
|
||||
code: typeof file === 'string' ? file : file.code,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// 兼容 tango.config.json,转成 sandbox.config.json
|
||||
function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/index.js') {
|
||||
if (files['/tango.config.json']) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AutoComplete } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
@@ -141,7 +141,7 @@ export function EventSetter(props: EventSetterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
const handlerMap: Dict = {
|
||||
[EventAction.OpenModal]: 'openModal',
|
||||
[EventAction.CloseModal]: 'closeModal',
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
|
||||
@@ -182,7 +182,7 @@ export function ExpressionPopover({
|
||||
}));
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
const evaluateContext: any = sandbox.window;
|
||||
|
||||
const handleExpInputChange = (val: string) => {
|
||||
setExp(val?.trim());
|
||||
@@ -253,7 +253,7 @@ export function ExpressionPopover({
|
||||
height="100%"
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
appContext={evaluateContext['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
|
||||
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
|
||||
import { Form, Input, Button, Popover } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { DragBox } from '../components';
|
||||
|
||||
function copyWithoutUndefined(source: Record<string, any>) {
|
||||
const ret = {};
|
||||
function copyWithoutUndefined(source: Dict) {
|
||||
const ret: Dict = {};
|
||||
for (const key in source) {
|
||||
if (source[key] !== undefined) {
|
||||
ret[key] = source[key];
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ModelSetter({
|
||||
}: FormItemComponentProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const { modelVariables } = useWorkspaceData();
|
||||
const evaluateContext = useSandboxQuery().window || {};
|
||||
const evaluateContext: any = useSandboxQuery().window || {};
|
||||
const workspace = useWorkspace();
|
||||
const definedVariables = useMemo(() => {
|
||||
const list: string[] = [];
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
interface IRenderOption {
|
||||
label: string;
|
||||
@@ -37,7 +38,7 @@ export function RenderSetter({
|
||||
}, [value]);
|
||||
|
||||
const optionsMap = useMemo(() => {
|
||||
return options.reduce((prev, cur) => {
|
||||
return options.reduce<Dict>((prev, cur) => {
|
||||
prev[cur.value] = cur;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Input, Radio } from 'antd';
|
||||
import { isValidUrl } from '@music163/tango-helpers';
|
||||
import { Dict, isValidUrl } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspaceData } from '@music163/tango-context';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
|
||||
const routerModeMap: Dict = {
|
||||
link: '链接',
|
||||
router: '路由',
|
||||
both: '',
|
||||
};
|
||||
|
||||
export function RouterSetter(props: FormItemComponentProps) {
|
||||
const [type, setType] = useState<'input' | 'select'>(() => {
|
||||
return isValidUrl(props.value) ? 'input' : 'select';
|
||||
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
|
||||
)}
|
||||
{type === 'input' && (
|
||||
<Input
|
||||
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`}
|
||||
placeholder={`请输入任意${routerModeMap[displayType]}地址`}
|
||||
{...props}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
||||
import { Panel } from '@music163/tango-ui';
|
||||
import { clone, parseDndId } from '@music163/tango-helpers';
|
||||
import { clone, Dict, parseDndId } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { registerBuiltinSetters } from './setters';
|
||||
|
||||
@@ -34,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
||||
}
|
||||
|
||||
const getSettingValue = (attributes: Record<string, string>) => {
|
||||
const ret = {};
|
||||
const ret: Dict = {};
|
||||
const keys = Object.keys(attributes);
|
||||
for (const key of keys) {
|
||||
if (/^data-/.test(key)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
|
||||
import { Box, Grid, Text } from 'coral-system';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
Dict,
|
||||
IComponentPrototype,
|
||||
MenuDataType,
|
||||
MenuValueType,
|
||||
@@ -66,7 +67,7 @@ export interface ComponentsPanelProps {
|
||||
layout?: 'grid' | 'line';
|
||||
}
|
||||
|
||||
const localeMap = {
|
||||
const localeMap: Dict = {
|
||||
common: '基础组件',
|
||||
atom: '原子组件',
|
||||
snippet: '组合',
|
||||
@@ -124,7 +125,7 @@ export const ComponentsPanel = observer(
|
||||
const tabs = Object.keys(menuData).map((key) => ({
|
||||
key,
|
||||
label: localeMap[key],
|
||||
children: <MaterialList data={menuData[key]} />,
|
||||
children: <MaterialList data={(menuData as any)[key]} />,
|
||||
}));
|
||||
|
||||
if (showBizComps) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const DataSourceView = observer(() => {
|
||||
<Box className="ServiceFunctionList" p="m">
|
||||
<VariableTree
|
||||
dataSource={serviceVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
appContext={(sandbox?.window as any)['tango']}
|
||||
serviceModules={serviceModules}
|
||||
onRemoveService={(variableKey) => {
|
||||
workspace.removeServiceFunction(variableKey);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { Input, Switch, Form, Button, message, FormProps } from 'antd';
|
||||
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
export default observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
@@ -63,7 +64,7 @@ function ProxyManage({
|
||||
initialValues: initialValuesProp = {},
|
||||
onSave,
|
||||
}: {
|
||||
initialValues?: object;
|
||||
initialValues?: Dict;
|
||||
onSave?: (values: any) => void;
|
||||
}) {
|
||||
const initialValues = useMemo(() => {
|
||||
@@ -84,7 +85,7 @@ function ProxyManage({
|
||||
{...proxyManageFormStyle}
|
||||
initialValues={initialValues}
|
||||
onFinish={(values) => {
|
||||
const proxy = values.proxy.reduce((prev: object, { path, ...rest }: any) => {
|
||||
const proxy = values.proxy.reduce((prev: any, { path, ...rest }: any) => {
|
||||
prev[path] = rest;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { isFunction, isObject, pick } from '@music163/tango-helpers';
|
||||
import { Dict, isFunction, isObject, pick } from '@music163/tango-helpers';
|
||||
import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
|
||||
function processTango(obj: any = {}, index = 0) {
|
||||
const ret = {};
|
||||
function processTango(obj: Dict = {}, index = 0) {
|
||||
const ret: Dict = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const val = obj[key];
|
||||
if (index > 2) {
|
||||
@@ -25,7 +25,7 @@ function processTango(obj: any = {}, index = 0) {
|
||||
export const StateTree = observer(() => {
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
|
||||
const tangoContext = pick((sandboxQuery.window as any)['tango'] || {}, [
|
||||
'stores',
|
||||
'pageStore',
|
||||
'services',
|
||||
|
||||
@@ -19,7 +19,7 @@ const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralP
|
||||
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
|
||||
${barStyle &&
|
||||
Object.keys(barStyle)
|
||||
.map((key) => `${key}: ${barStyle[key]};`)
|
||||
.map((key) => `${key}: ${(barStyle as any)[key]};`)
|
||||
.join('')}
|
||||
&:hover,
|
||||
&:active {
|
||||
|
||||
@@ -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> = {};
|
||||
@@ -93,13 +101,22 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
|
||||
}, [children]);
|
||||
|
||||
if (designer.activeView !== 'design') {
|
||||
return <></>;
|
||||
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;
|
||||
@@ -116,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
showBadge={item.showBadge}
|
||||
isActive={isActive}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -187,10 +203,6 @@ BaseSidebarPanel.Item = function ({
|
||||
export const Sidebar = observer(BaseSidebarPanel);
|
||||
|
||||
interface SidebarPanelBarItemProps {
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
isActive?: boolean;
|
||||
/**
|
||||
* 侧边栏图标
|
||||
*/
|
||||
@@ -216,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>
|
||||
);
|
||||
@@ -245,7 +251,7 @@ function SidebarPanelBarItem({
|
||||
py="12px"
|
||||
>
|
||||
{icon}
|
||||
<Text fontSize="12px" mt="s" color={color}>
|
||||
<Text fontSize="12px" mt="s">
|
||||
{label}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -25,22 +25,45 @@ function DesktopSimulator({ children }: HTMLCoralProps<'div'>) {
|
||||
}
|
||||
|
||||
const mobileStyle = css`
|
||||
--device-viewport-width: 375px;
|
||||
--device-viewport-height: 812px;
|
||||
--device-frame-width: 13px;
|
||||
background-image: url(https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/21897838692/b1c5/2665/56db/10558c1bb2030d55ceabbcc74431de99.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
box-shadow: 0 2px 8px rgb(2 8 20 / 10%), 0 8px 16px rgb(2 8 20 / 8%);
|
||||
border-radius: 62px;
|
||||
overflow: hidden;
|
||||
--device-viewport-width: 360px;
|
||||
--device-viewport-height: 640px;
|
||||
position: relative;
|
||||
margin: auto;
|
||||
border: 16px black solid;
|
||||
border-top-width: 60px;
|
||||
border-bottom-width: 60px;
|
||||
border-radius: 36px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #333;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -65px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #333;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.MobileSimulatorDeviceFrame {
|
||||
border-radius: 50px;
|
||||
width: var(--device-viewport-width);
|
||||
height: var(--device-viewport-height);
|
||||
margin: var(--device-frame-width);
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Group } from 'coral-system';
|
||||
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { BorderOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ModeSwitchTool = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
return (
|
||||
<Group attached>
|
||||
<Group attached whiteSpace="nowrap">
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
selected={designer.activeView === 'design'}
|
||||
onClick={() => {
|
||||
workspace.syncFiles(); // 保证 ast 与 code 是同步的
|
||||
designer.setActiveView('design');
|
||||
}}
|
||||
tooltip="设计视图"
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -47,11 +47,14 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const current = optionMap[value];
|
||||
const pathname = value?.split?.('?')[0];
|
||||
const current = optionMap[pathname];
|
||||
return (
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
minWidth={100}
|
||||
maxWidth={200}
|
||||
textAlign="left"
|
||||
dropdownProps={{
|
||||
open: visible,
|
||||
onOpenChange: setVisible,
|
||||
|
||||
@@ -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 {
|
||||
@@ -17,28 +19,19 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
React.Children.forEach(children, (child: React.ReactElement, index) => {
|
||||
const childProps = child.props;
|
||||
|
||||
let fallbackNode;
|
||||
let fallbackChildren;
|
||||
if (child.key) {
|
||||
const Widget = getWidget(['toolbar', child.key].join('.'));
|
||||
if (Widget) {
|
||||
fallbackNode = (
|
||||
<ToolbarItem>
|
||||
<Widget {...childProps.widgetProps} />
|
||||
</ToolbarItem>
|
||||
);
|
||||
fallbackChildren = <Widget {...childProps.widgetProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
let node;
|
||||
if (childProps.children) {
|
||||
// 自定义子节点
|
||||
node = child;
|
||||
} else {
|
||||
// 预注册的子节点
|
||||
node = fallbackNode || child;
|
||||
}
|
||||
const key = child.key || index;
|
||||
node = React.cloneElement(node, { key, 'data-key': key });
|
||||
const node = React.cloneElement(child, {
|
||||
key,
|
||||
'data-key': key,
|
||||
children: childProps.children || fallbackChildren,
|
||||
});
|
||||
|
||||
const placement = childProps.placement || prevPlacement || 'center';
|
||||
switch (placement) {
|
||||
@@ -85,18 +78,37 @@ export interface ToolbarItemProps extends ReactComponentProps {
|
||||
* 如果 key 匹配到内置组件的话,传递给子节点的属性
|
||||
*/
|
||||
widgetProps?: object;
|
||||
/**
|
||||
* 工具箱展示的视图
|
||||
*/
|
||||
activeViews?: DesignerViewType[];
|
||||
}
|
||||
|
||||
function ToolbarItem({ type, placement, widgetProps, children, ...rest }: ToolbarItemProps) {
|
||||
if (type === 'divider') {
|
||||
return <Separator />;
|
||||
}
|
||||
return (
|
||||
<div className="ToolbarPanelItem" {...rest}>
|
||||
{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,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { DesignerViewType } from '@music163/tango-core';
|
||||
import { ComponentsPopover, FilesErrorOverlay } from './components';
|
||||
import cx from 'classnames';
|
||||
import { ComponentsPopover, FileErrorsOverlay } from './components';
|
||||
|
||||
export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
|
||||
/**
|
||||
@@ -15,27 +15,23 @@ export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
|
||||
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
||||
const { mode = 'design', children, className, ...rest } = props;
|
||||
const designer = useDesigner();
|
||||
let activeView = designer.activeView;
|
||||
if (designer.activeView === 'dual') {
|
||||
activeView = 'code';
|
||||
}
|
||||
const display = mode !== activeView ? 'none' : 'block';
|
||||
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕
|
||||
const display = mode !== designer.activeView ? 'none' : 'block';
|
||||
// 移动端模式小屏幕适配,可能会溢出屏幕
|
||||
const overflow =
|
||||
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
|
||||
return (
|
||||
<Box
|
||||
className={cx('ViewPanel', mode, className)}
|
||||
display={activeView === mode ? 'block' : display}
|
||||
overflow={overflow}
|
||||
display={designer.activeView === 'dual' ? 'block' : display}
|
||||
flex="1"
|
||||
overflow={overflow}
|
||||
position="relative"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{/* 添加组件弹层 */}
|
||||
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
||||
{mode === 'design' && <FilesErrorOverlay />}
|
||||
{designer.activeView === 'design' && <FileErrorsOverlay />}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.1...@music163/tango-helpers@1.2.2) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.0...@music163/tango-helpers@1.2.1) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.1...@music163/tango-helpers@1.2.0) (2024-06-03)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.2",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -43,7 +43,7 @@ export function filterTreeData<T = unknown>(
|
||||
childrenProp = 'children',
|
||||
onlyLeaf = false,
|
||||
) {
|
||||
const reducer = (result: T[], current: T) => {
|
||||
const reducer = (result: T[], current: any) => {
|
||||
if (current[childrenProp]) {
|
||||
const newChildren = current[childrenProp].reduce(reducer, []);
|
||||
if (newChildren.length) {
|
||||
@@ -70,7 +70,7 @@ export function mapTreeData<T = unknown>(
|
||||
mapper: (node: T) => any,
|
||||
childrenProp = 'children',
|
||||
) {
|
||||
return treeData.map((node) => {
|
||||
return treeData.map((node: any) => {
|
||||
const newNode = mapper(node);
|
||||
if (node[childrenProp]) {
|
||||
newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function isValidFunctionCode(str: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const templatePattern = /^{{(.+)}}$/s;
|
||||
const templatePattern = /^{{([\s\S]+)}}$/;
|
||||
|
||||
/**
|
||||
* 判断给定代码是否被双花括号包裹
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import get from 'lodash.get';
|
||||
import set from 'lodash.set';
|
||||
import { Dict } from '../types';
|
||||
|
||||
/**
|
||||
* 合并 target 和 source 对象,source 对象的优先级更高,如存在重名,覆盖 target 中的 key
|
||||
@@ -45,7 +46,7 @@ export function clone(obj: any, withUndefined = true) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const target = {};
|
||||
const target: Dict = {};
|
||||
for (const key in obj) {
|
||||
if (withUndefined) {
|
||||
target[key] = obj[key];
|
||||
@@ -65,7 +66,7 @@ export function omit(obj: any, keys: string[]) {
|
||||
}
|
||||
|
||||
export function pick(obj: any, keys: string[]) {
|
||||
const target = {};
|
||||
const target: Dict = {};
|
||||
for (const key of keys) {
|
||||
target[key] = obj[key];
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function withDnd(options: IWithDndOptions<any>) {
|
||||
const override = options.overrideProps || {};
|
||||
const renderFooter = options.renderFooter || renderEmpty;
|
||||
|
||||
const Component = forwardRef<unknown, P & DraggableComponentProps>((props, refProp) => {
|
||||
const Component = forwardRef<unknown, P & DraggableComponentProps>((props: any, refProp) => {
|
||||
const dndProps = {
|
||||
[SLOT.id]: props.tid, // id 作为唯一标记
|
||||
[SLOT.dnd]: props[SLOT.dnd], // dnd 作为追踪标记
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { warning } from '../helpers';
|
||||
import { Dict } from '../types';
|
||||
|
||||
type ListStoreOptionsType<T = object> = {
|
||||
type ListStoreOptionsType<T extends Dict = Dict> = {
|
||||
data: T[];
|
||||
keyProp?: string;
|
||||
labelProp?: string;
|
||||
childrenProp?: string;
|
||||
};
|
||||
|
||||
export class ListStore<T = object> {
|
||||
export class ListStore<T extends Dict = Dict> {
|
||||
private nodeMap: Map<string, T>;
|
||||
private keyProp: string;
|
||||
private childrenProp: string;
|
||||
|
||||
13
packages/helpers/tests/code-helper.test.ts
Normal file
13
packages/helpers/tests/code-helper.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { isWrappedCode } from '../src/helpers';
|
||||
|
||||
describe('helpers/codeHelper', () => {
|
||||
it('isWrappedCode', () => {
|
||||
expect(isWrappedCode('{{[]}}')).toBeTruthy(); // true
|
||||
expect(isWrappedCode('{{{}}}')).toBeTruthy(); // true
|
||||
expect(isWrappedCode('{{this.foo}}')).toBeTruthy(); // true
|
||||
expect(isWrappedCode('{{123}}')).toBeTruthy(); // true
|
||||
expect(isWrappedCode('{{() => {}}}')).toBeTruthy(); // true
|
||||
expect(isWrappedCode('{{<Button>button</Button>}}')).toBeTruthy(); // true
|
||||
expect(isWrappedCode('{{<>\n<Button>hello</Button>\n</>}}')).toBeTruthy(); // true
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.11](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.10...@music163/tango-sandbox@1.0.11) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.10](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.9...@music163/tango-sandbox@1.0.10) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.9](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.8...@music163/tango-sandbox@1.0.9) (2024-08-01)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.8](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.7...@music163/tango-sandbox@1.0.8) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.11",
|
||||
"description": "sandbox of tango apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.12](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.11...@music163/tango-setting-form@1.2.12) (2024-08-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.11](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.10...@music163/tango-setting-form@1.2.11) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.10](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.9...@music163/tango-setting-form@1.2.10) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
|
||||
## [1.2.9](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.8...@music163/tango-setting-form@1.2.9) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
## [1.2.8](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.7...@music163/tango-setting-form@1.2.8) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.12",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,9 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-ui": "^1.3.3",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-ui": "^1.4.2",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
noop,
|
||||
filterTreeData,
|
||||
mapTreeData,
|
||||
Dict,
|
||||
} from '@music163/tango-helpers';
|
||||
import { Action, Search, Tabs } from '@music163/tango-ui';
|
||||
import { SettingFormItem, register } from './form-item';
|
||||
@@ -24,7 +25,7 @@ function normalizeComponentProps(
|
||||
props: IComponentPrototype['props'],
|
||||
groupOptions: IFormTabsGroupOption[],
|
||||
) {
|
||||
const groups: Record<string, IComponentProp[]> = groupOptions.reduce((prev, cur) => {
|
||||
const groups: Record<string, IComponentProp[]> = groupOptions.reduce<Dict>((prev, cur) => {
|
||||
prev[cur.value] = [];
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@@ -34,7 +34,7 @@ export function TextSetter({
|
||||
|
||||
const autoSize = {
|
||||
minRows: 2,
|
||||
maxRows: 5,
|
||||
maxRows: 6,
|
||||
};
|
||||
|
||||
export function TextAreaSetter({
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.1...@music163/tango-ui@1.4.2) (2024-08-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- route matching without query string & toggle button wrapping ([#195](https://github.com/netease/tango/issues/195)) ([9394666](https://github.com/netease/tango/commit/939466648d40331800f15e886256a0789b7e7623))
|
||||
- update mobile simulator ui ([#196](https://github.com/netease/tango/issues/196)) ([65149f1](https://github.com/netease/tango/commit/65149f1326eaf78e8a27222993c3cacc674cd378))
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.0...@music163/tango-ui@1.4.1) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.3...@music163/tango-ui@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.2...@music163/tango-ui@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.2",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lint": "^6.7.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@uiw/react-codemirror": "^4.22.0",
|
||||
"antd": "^4.24.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Menu, Space } from 'antd';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { isApplePlatform } from '@music163/tango-helpers';
|
||||
import { Dict, isApplePlatform } from '@music163/tango-helpers';
|
||||
|
||||
const contextActionStyle = css`
|
||||
display: flex;
|
||||
@@ -46,7 +46,7 @@ export function ContextAction({
|
||||
if (!hotkey) {
|
||||
return null;
|
||||
}
|
||||
const keyMap = isApplePlatform()
|
||||
const keyMap: Dict = isApplePlatform()
|
||||
? {
|
||||
command: '⌘',
|
||||
meta: '⌘',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createIcon } from './create-icon';
|
||||
|
||||
const DualOutlinedSvg = () => (
|
||||
<svg
|
||||
viewBox="0 0 1025 1024"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,6 +12,7 @@ const buttonStyle = css`
|
||||
cursor: pointer;
|
||||
color: var(--tango-colors-gray-50);
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
.anticon-down {
|
||||
width: 10px;
|
||||
@@ -201,7 +202,8 @@ export function ToggleButton(props: ToggleButtonProps) {
|
||||
|
||||
let btn = (
|
||||
<Button css={buttonStyle} className={clazz} type="button" {...rest}>
|
||||
<Box as="span">
|
||||
{/* @ts-ignore textOverflow is available */}
|
||||
<Box as="span" flex="1" overflow="hidden" textOverflow="ellipsis">
|
||||
{icon && (
|
||||
<Box as="span" className="buttonIcon" mr="s">
|
||||
{icon}
|
||||
|
||||
Reference in New Issue
Block a user