feat: added workspace api to support registration of multiple resources

This commit is contained in:
liujuping 2022-12-29 17:53:23 +08:00 committed by 林熠
parent 0a2427354b
commit dae09e3bcb
36 changed files with 720 additions and 311 deletions

View File

@ -11,15 +11,41 @@ sidebar_position: 12
低代码设计器窗口模型
## 变量
### id
窗口唯一 id
### title
窗口标题
### resourceName
窗口资源名字
## 方法签名
### importSchema(schema: IPublicTypeNodeSchema)
当前窗口导入 schema
### importSchema
当前窗口导入 schema, 会调用当前窗口对应资源的 import 钩子
```typescript
function importSchema(schema: IPublicTypeNodeSchema): void
```
相关类型:[IPublicTypeNodeSchema](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/node-schema.ts)
### changeViewType(viewName: string)
### changeViewType
修改当前窗口视图类型
### async save()
调用当前窗口视图保存钩子
```typescript
function changeViewType(viewName: string): void
```
### save
当前窗口的保存方法,会调用当前窗口对应资源的 save 钩子
```typescript
function save(): Promise(void)
```

View File

@ -21,6 +21,30 @@ sidebar_position: 12
当前设计器窗口模型
```typescript
get window(): IPublicModelWindow
```
关联模型 [IPublicModelWindow](./model/window)
### plugins
应用级别的插件注册
```typescript
get plugins(): IPublicApiPlugins
```
关联模型 [IPublicApiPlugins](./plugins)
### windows
当前设计器的编辑窗口
```typescript
get window(): IPublicModelWindow[]
```
关联模型 [IPublicModelWindow](./model/window)
## 方法签名
@ -34,3 +58,19 @@ registerResourceType(resourceName: string, resourceType: 'editor', options: IPub
```
相关类型:[IPublicResourceOptions](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/resource-options.ts)
### onChangeWindows
窗口新增/删除的事件
```typescript
function onChangeWindows(fn: () => void): void;
```
### onChangeActiveWindow
active 窗口变更事件
```typescript
function onChangeActiveWindow(fn: () => void): void;
```

View File

@ -0,0 +1,155 @@
import { IPublicTypeComponentAction, IPublicTypeMetadataTransducer } from '@alilc/lowcode-types';
import { engineConfig } from '@alilc/lowcode-editor-core';
import { intlNode } from './locale';
import {
IconLock,
IconUnlock,
IconRemove,
IconClone,
IconHidden,
} from './icons';
import { Node } from './document';
import { componentDefaults, legacyIssues } from './transducers';
export class ComponentActions {
actions: IPublicTypeComponentAction[] = [
{
name: 'remove',
content: {
icon: IconRemove,
title: intlNode('remove'),
/* istanbul ignore next */
action(node: Node) {
node.remove();
},
},
important: true,
},
{
name: 'hide',
content: {
icon: IconHidden,
title: intlNode('hide'),
/* istanbul ignore next */
action(node: Node) {
node.setVisible(false);
},
},
/* istanbul ignore next */
condition: (node: Node) => {
return node.componentMeta.isModal;
},
important: true,
},
{
name: 'copy',
content: {
icon: IconClone,
title: intlNode('copy'),
/* istanbul ignore next */
action(node: Node) {
// node.remove();
const { document: doc, parent, index } = node;
if (parent) {
const newNode = doc.insertNode(parent, node, index + 1, true);
newNode.select();
const { isRGL, rglNode } = node.getRGL();
if (isRGL) {
// 复制 layout 信息
let layout = rglNode.getPropValue('layout') || [];
let curLayout = layout.filter((item) => item.i === node.getPropValue('fieldId'));
if (curLayout && curLayout[0]) {
layout.push({
...curLayout[0],
i: newNode.getPropValue('fieldId'),
});
rglNode.setPropValue('layout', layout);
// 如果是磁贴块复制,则需要滚动到影响位置
setTimeout(() => newNode.document.simulator?.scrollToNode(newNode), 10);
}
}
}
},
},
important: true,
},
{
name: 'lock',
content: {
icon: IconLock, // 锁定 icon
title: intlNode('lock'),
/* istanbul ignore next */
action(node: Node) {
node.lock();
},
},
/* istanbul ignore next */
condition: (node: Node) => {
return engineConfig.get('enableCanvasLock', false) && node.isContainer() && !node.isLocked;
},
important: true,
},
{
name: 'unlock',
content: {
icon: IconUnlock, // 解锁 icon
title: intlNode('unlock'),
/* istanbul ignore next */
action(node: Node) {
node.lock(false);
},
},
/* istanbul ignore next */
condition: (node: Node) => {
return engineConfig.get('enableCanvasLock', false) && node.isContainer() && node.isLocked;
},
important: true,
},
];
constructor() {
this.registerMetadataTransducer(legacyIssues, 2, 'legacy-issues'); // should use a high level priority, eg: 2
this.registerMetadataTransducer(componentDefaults, 100, 'component-defaults');
}
removeBuiltinComponentAction(name: string) {
const i = this.actions.findIndex((action) => action.name === name);
if (i > -1) {
this.actions.splice(i, 1);
}
}
addBuiltinComponentAction(action: IPublicTypeComponentAction) {
this.actions.push(action);
}
modifyBuiltinComponentAction(
actionName: string,
handle: (action: IPublicTypeComponentAction) => void,
) {
const builtinAction = this.actions.find((action) => action.name === actionName);
if (builtinAction) {
handle(builtinAction);
}
}
private metadataTransducers: IPublicTypeMetadataTransducer[] = [];
registerMetadataTransducer(
transducer: IPublicTypeMetadataTransducer,
level = 100,
id?: string,
) {
transducer.level = level;
transducer.id = id;
const i = this.metadataTransducers.findIndex((item) => item.level != null && item.level > level);
if (i < 0) {
this.metadataTransducers.push(transducer);
} else {
this.metadataTransducers.splice(i, 0, transducer);
}
}
getRegisteredMetadataTransducers(): IPublicTypeMetadataTransducer[] {
return this.metadataTransducers;
}
}

View File

@ -4,7 +4,6 @@ import {
IPublicTypeNpmInfo,
IPublicTypeNodeData,
IPublicTypeNodeSchema,
IPublicTypeComponentAction,
IPublicTypeTitleContent,
IPublicTypeTransformedComponentMetadata,
IPublicTypeNestingFilter,
@ -15,20 +14,13 @@ import {
IPublicModelComponentMeta,
} from '@alilc/lowcode-types';
import { deprecate, isRegExp, isTitleConfig } from '@alilc/lowcode-utils';
import { computed, engineConfig, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import { componentDefaults, legacyIssues } from './transducers';
import { computed, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import { isNode, Node, INode } from './document';
import { Designer } from './designer';
import { intlNode } from './locale';
import {
IconLock,
IconUnlock,
IconContainer,
IconPage,
IconComponent,
IconRemove,
IconClone,
IconHidden,
} from './icons';
export function ensureAList(list?: string | string[]): string[] | null {
@ -272,7 +264,7 @@ export class ComponentMeta implements IComponentMeta {
}
private transformMetadata(metadta: IPublicTypeComponentMetadata): IPublicTypeTransformedComponentMetadata {
const result = getRegisteredMetadataTransducers().reduce((prevMetadata, current) => {
const result = this.designer.componentActions.getRegisteredMetadataTransducers().reduce((prevMetadata, current) => {
return current(prevMetadata);
}, preprocessMetadata(metadta));
@ -300,7 +292,7 @@ export class ComponentMeta implements IComponentMeta {
const disabled =
ensureAList(disableBehaviors) ||
(this.isRootComponent(false) ? ['copy', 'remove', 'lock', 'unlock'] : null);
actions = builtinComponentActions.concat(
actions = this.designer.componentActions.actions.concat(
this.designer.getGlobalComponentActions() || [],
actions || [],
);
@ -382,142 +374,3 @@ function preprocessMetadata(metadata: IPublicTypeComponentMetadata): IPublicType
};
}
const metadataTransducers: IPublicTypeMetadataTransducer[] = [];
export function registerMetadataTransducer(
transducer: IPublicTypeMetadataTransducer,
level = 100,
id?: string,
) {
transducer.level = level;
transducer.id = id;
const i = metadataTransducers.findIndex((item) => item.level != null && item.level > level);
if (i < 0) {
metadataTransducers.push(transducer);
} else {
metadataTransducers.splice(i, 0, transducer);
}
}
export function getRegisteredMetadataTransducers(): IPublicTypeMetadataTransducer[] {
return metadataTransducers;
}
const builtinComponentActions: IPublicTypeComponentAction[] = [
{
name: 'remove',
content: {
icon: IconRemove,
title: intlNode('remove'),
/* istanbul ignore next */
action(node: Node) {
node.remove();
},
},
important: true,
},
{
name: 'hide',
content: {
icon: IconHidden,
title: intlNode('hide'),
/* istanbul ignore next */
action(node: Node) {
node.setVisible(false);
},
},
/* istanbul ignore next */
condition: (node: Node) => {
return node.componentMeta.isModal;
},
important: true,
},
{
name: 'copy',
content: {
icon: IconClone,
title: intlNode('copy'),
/* istanbul ignore next */
action(node: Node) {
// node.remove();
const { document: doc, parent, index } = node;
if (parent) {
const newNode = doc.insertNode(parent, node, index + 1, true);
newNode.select();
const { isRGL, rglNode } = node.getRGL();
if (isRGL) {
// 复制 layout 信息
let layout = rglNode.getPropValue('layout') || [];
let curLayout = layout.filter((item) => item.i === node.getPropValue('fieldId'));
if (curLayout && curLayout[0]) {
layout.push({
...curLayout[0],
i: newNode.getPropValue('fieldId'),
});
rglNode.setPropValue('layout', layout);
// 如果是磁贴块复制,则需要滚动到影响位置
setTimeout(() => newNode.document.simulator?.scrollToNode(newNode), 10);
}
}
}
},
},
important: true,
},
{
name: 'lock',
content: {
icon: IconLock, // 锁定 icon
title: intlNode('lock'),
/* istanbul ignore next */
action(node: Node) {
node.lock();
},
},
/* istanbul ignore next */
condition: (node: Node) => {
return engineConfig.get('enableCanvasLock', false) && node.isContainer() && !node.isLocked;
},
important: true,
},
{
name: 'unlock',
content: {
icon: IconUnlock, // 解锁 icon
title: intlNode('unlock'),
/* istanbul ignore next */
action(node: Node) {
node.lock(false);
},
},
/* istanbul ignore next */
condition: (node: Node) => {
return engineConfig.get('enableCanvasLock', false) && node.isContainer() && node.isLocked;
},
important: true,
},
];
export function removeBuiltinComponentAction(name: string) {
const i = builtinComponentActions.findIndex((action) => action.name === name);
if (i > -1) {
builtinComponentActions.splice(i, 1);
}
}
export function addBuiltinComponentAction(action: IPublicTypeComponentAction) {
builtinComponentActions.push(action);
}
export function modifyBuiltinComponentAction(
actionName: string,
handle: (action: IPublicTypeComponentAction) => void,
) {
const builtinAction = builtinComponentActions.find((action) => action.name === actionName);
if (builtinAction) {
handle(builtinAction);
}
}
registerMetadataTransducer(legacyIssues, 2, 'legacy-issues'); // should use a high level priority, eg: 2
registerMetadataTransducer(componentDefaults, 100, 'component-defaults');

View File

@ -32,6 +32,7 @@ import { OffsetObserver, createOffsetObserver } from './offset-observer';
import { focusing } from './focusing';
import { SettingTopEntry } from './setting';
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
import { ComponentActions } from '../component-actions';
const logger = new Logger({ level: 'warn', bizName: 'designer' });
@ -60,6 +61,8 @@ export interface DesignerProps {
export class Designer implements IDesigner {
readonly dragon = new Dragon(this);
readonly componentActions = new ComponentActions();
readonly activeTracker = new ActiveTracker();
readonly detecting = new Detecting();

View File

@ -2,7 +2,7 @@ import { IPublicTypeTitleContent, IPublicTypeSetterType, IPublicTypeDynamicSette
import { Transducer } from './utils';
import { SettingPropEntry } from './setting-prop-entry';
import { SettingEntry } from './setting-entry';
import { computed, obx, makeObservable, action } from '@alilc/lowcode-editor-core';
import { computed, obx, makeObservable, action, untracked } from '@alilc/lowcode-editor-core';
import { cloneDeep, isCustomView, isDynamicSetter } from '@alilc/lowcode-utils';
function getSettingFieldCollectorKey(parent: SettingEntry, config: IPublicTypeFieldConfig) {
@ -43,8 +43,10 @@ export class SettingField extends SettingPropEntry implements SettingEntry {
return null;
}
if (isDynamicSetter(this._setter)) {
const shellThis = this.internalToShellPropEntry();
return this._setter.call(shellThis, shellThis);
return untracked(() => {
const shellThis = this.internalToShellPropEntry();
return this._setter.call(shellThis, shellThis);
});
}
return this._setter;
}

View File

@ -4,7 +4,7 @@ import { Designer } from '../designer';
import { BuiltinSimulatorHostView } from '../builtin-simulator';
import './project.less';
class BuiltinLoading extends Component {
export class BuiltinLoading extends Component {
render() {
return (
<div id="engine-loading-wrapper">

View File

@ -30,6 +30,8 @@ export class Project implements IProject {
private _simulator?: ISimulatorHost;
private isRendererReady: boolean = false;
/**
*
*/
@ -318,6 +320,7 @@ export class Project implements IProject {
}
setRendererReady(renderer: any) {
this.isRendererReady = true;
this.emitter.emit('lowcode_engine_renderer_ready', renderer);
}
@ -328,7 +331,10 @@ export class Project implements IProject {
};
}
onRendererReady(fn: (args: any) => void): () => void {
onRendererReady(fn: () => void): () => void {
if (this.isRendererReady) {
fn();
}
this.emitter.on('lowcode_engine_renderer_ready', fn);
return () => {
this.emitter.removeListener('lowcode_engine_renderer_ready', fn);

View File

@ -1,5 +1,4 @@
import '../../fixtures/window';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import divMeta from '../../fixtures/component-metadata/div';
import div2Meta from '../../fixtures/component-metadata/div2';
@ -19,22 +18,18 @@ import page2Meta from '../../fixtures/component-metadata/page2';
import {
ComponentMeta,
isComponentMeta,
removeBuiltinComponentAction,
addBuiltinComponentAction,
modifyBuiltinComponentAction,
ensureAList,
buildFilter,
registerMetadataTransducer,
getRegisteredMetadataTransducers,
} from '../../../src/component-meta';
import { componentDefaults } from '../../../src/transducers';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
const { ComponentActions } = require('../../../src/component-actions');
return {
getGlobalComponentActions: () => [],
componentActions: new ComponentActions(),
};
}),
};
@ -126,12 +121,12 @@ describe('组件元数据处理', () => {
expect(meta.availableActions[1].name).toBe('hide');
expect(meta.availableActions[2].name).toBe('copy');
removeBuiltinComponentAction('remove');
designer.componentActions.removeBuiltinComponentAction('remove');
expect(meta.availableActions).toHaveLength(4);
expect(meta.availableActions[0].name).toBe('hide');
expect(meta.availableActions[1].name).toBe('copy');
addBuiltinComponentAction({
designer.componentActions.addBuiltinComponentAction({
name: 'new',
content: {
action() {},
@ -227,17 +222,17 @@ describe('帮助函数', () => {
});
it('registerMetadataTransducer', () => {
expect(getRegisteredMetadataTransducers()).toHaveLength(2);
expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(2);
// 插入到 legacy-issues 和 component-defaults 的中间
registerMetadataTransducer((metadata) => metadata, 3, 'noop');
expect(getRegisteredMetadataTransducers()).toHaveLength(3);
designer.componentActions.registerMetadataTransducer((metadata) => metadata, 3, 'noop');
expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(3);
registerMetadataTransducer((metadata) => metadata);
expect(getRegisteredMetadataTransducers()).toHaveLength(4);
designer.componentActions.registerMetadataTransducer((metadata) => metadata);
expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(4);
});
it('modifyBuiltinComponentAction', () => {
modifyBuiltinComponentAction('copy', (action) => {
designer.componentActions.modifyBuiltinComponentAction('copy', (action) => {
expect(action.name).toBe('copy');
});
});

View File

@ -69,7 +69,7 @@ export class SettingsPrimaryPane extends Component<{ engineEditor: Editor; confi
}
const workspace = globalContext.get('workspace');
const editor = workspace.isActive ? workspace.window.editor : globalContext.get('editor');
const editor = this.props.engineEditor;
const designer = editor.get('designer');
const current = designer?.currentSelection?.getNodes()?.[0];
let node: Node | null = settings.first;

View File

@ -138,6 +138,16 @@ body {
display: flex;
flex-direction: column;
background-color: #edeff3;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
&.active {
z-index: 999;
}
.lc-workbench {

View File

@ -1,15 +1,23 @@
import { registerMetadataTransducer } from '@alilc/lowcode-designer';
import parseJSFunc from './transducers/parse-func';
import parseProps from './transducers/parse-props';
import addonCombine from './transducers/addon-combine';
import { IPublicModelPluginContext } from '@alilc/lowcode-types';
export const registerDefaults = () => {
// parseFunc
registerMetadataTransducer(parseJSFunc, 1, 'parse-func');
export const registerDefaults = (ctx: IPublicModelPluginContext) => {
const { material } = ctx;
return {
init() {
// parseFunc
material.registerMetadataTransducer(parseJSFunc, 1, 'parse-func');
// parseProps
registerMetadataTransducer(parseProps, 5, 'parse-props');
// parseProps
material.registerMetadataTransducer(parseProps, 5, 'parse-props');
// addon/platform custom
registerMetadataTransducer(addonCombine, 10, 'combine-props');
// addon/platform custom
material.registerMetadataTransducer(addonCombine, 10, 'combine-props');
},
};
};
registerDefaults.pluginName = '___register_defaults___';

View File

@ -50,6 +50,8 @@ export class Skeleton {
readonly topArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
readonly subTopArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
readonly leftFixedArea: Area<PanelConfig, Panel>;
@ -88,6 +90,17 @@ export class Skeleton {
},
false,
);
this.subTopArea = new Area(
this,
'subTopArea',
(config) => {
if (isWidget(config)) {
return config;
}
return this.createWidget(config);
},
false,
);
this.toolbar = new Area(
this,
'toolbar',
@ -389,6 +402,8 @@ export class Skeleton {
case 'topArea':
case 'top':
return this.topArea.add(parsedConfig as PanelDockConfig);
case 'subTopArea':
return this.subTopArea.add(parsedConfig as PanelDockConfig);
case 'toolbar':
return this.toolbar.add(parsedConfig as PanelDockConfig);
case 'mainArea':

View File

@ -59,8 +59,6 @@ export * from './modules/skeleton-types';
export * from './modules/designer-types';
export * from './modules/lowcode-types';
registerDefaults();
async function registryInnerPlugin(designer: Designer, editor: Editor, plugins: Plugins) {
// 注册一批内置插件
await plugins.register(OutlinePlugin, {}, { autoInit: true });
@ -68,6 +66,7 @@ async function registryInnerPlugin(designer: Designer, editor: Editor, plugins:
await plugins.register(setterRegistry, {}, { autoInit: true });
await plugins.register(defaultPanelRegistry(editor));
await plugins.register(builtinHotkey);
await plugins.register(registerDefaults);
}
const innerWorkspace = new InnerWorkspace(registryInnerPlugin, shellModelFactory);
@ -82,6 +81,7 @@ editor.set('skeleton' as any, innerSkeleton);
const designer = new Designer({ editor, shellModelFactory });
editor.set('designer' as any, designer);
const { project: innerProject } = designer;
const innerHotkey = new InnerHotkey();
@ -195,6 +195,7 @@ export async function init(
engineContainer,
);
innerWorkspace.setActive(true);
await innerWorkspace.plugins.init(pluginPreference);
return;
}

View File

@ -1,11 +1,6 @@
import { Editor, globalContext } from '@alilc/lowcode-editor-core';
import {
Designer,
registerMetadataTransducer,
getRegisteredMetadataTransducers,
addBuiltinComponentAction,
removeBuiltinComponentAction,
modifyBuiltinComponentAction,
isComponentMeta,
} from '@alilc/lowcode-designer';
import { IPublicTypeAssetsJson } from '@alilc/lowcode-utils';
@ -85,20 +80,20 @@ export class Material implements IPublicApiMaterial {
* @param level
* @param id
*/
registerMetadataTransducer(
registerMetadataTransducer = (
transducer: IPublicTypeMetadataTransducer,
level?: number,
id?: string | undefined,
) {
registerMetadataTransducer(transducer, level, id);
}
) => {
this[designerSymbol].componentActions.registerMetadataTransducer(transducer, level, id);
};
/**
*
* @returns
*/
getRegisteredMetadataTransducers() {
return getRegisteredMetadataTransducers();
return this[designerSymbol].componentActions.getRegisteredMetadataTransducers();
}
/**
@ -147,7 +142,7 @@ export class Material implements IPublicApiMaterial {
* @param action
*/
addBuiltinComponentAction(action: IPublicTypeComponentAction) {
addBuiltinComponentAction(action);
this[designerSymbol].componentActions.addBuiltinComponentAction(action);
}
/**
@ -155,7 +150,7 @@ export class Material implements IPublicApiMaterial {
* @param name
*/
removeBuiltinComponentAction(name: string) {
removeBuiltinComponentAction(name);
this[designerSymbol].componentActions.removeBuiltinComponentAction(name);
}
/**
@ -164,7 +159,7 @@ export class Material implements IPublicApiMaterial {
* @param handle
*/
modifyBuiltinComponentAction(actionName: string, handle: (action: IPublicTypeComponentAction) => void) {
modifyBuiltinComponentAction(actionName, handle);
this[designerSymbol].componentActions.modifyBuiltinComponentAction(actionName, handle);
}
/**

View File

@ -18,14 +18,12 @@ import {
import { DocumentModel } from '../model/document-model';
import { SimulatorHost } from './simulator-host';
import { editorSymbol, projectSymbol, simulatorHostSymbol, simulatorRendererSymbol, documentSymbol } from '../symbols';
import { editorSymbol, projectSymbol, simulatorHostSymbol, documentSymbol } from '../symbols';
const innerProjectSymbol = Symbol('innerProject');
export class Project implements IPublicApiProject {
private readonly [editorSymbol]: IPublicModelEditor;
private readonly [innerProjectSymbol]: InnerProject;
private [simulatorHostSymbol]: BuiltinSimulatorHost;
private [simulatorRendererSymbol]: any;
get [projectSymbol](): InnerProject {
if (this.workspaceMode) {
return this[innerProjectSymbol];
@ -38,9 +36,12 @@ export class Project implements IPublicApiProject {
return this[innerProjectSymbol];
}
get [editorSymbol](): IPublicModelEditor {
return this[projectSymbol]?.designer.editor;
}
constructor(project: InnerProject, public workspaceMode: boolean = false) {
this[innerProjectSymbol] = project;
this[editorSymbol] = project?.designer.editor;
}
static create(project: InnerProject) {
@ -201,13 +202,9 @@ export class Project implements IPublicApiProject {
* project ready
*/
onSimulatorRendererReady(fn: () => void): IPublicTypeDisposable {
const offFn = this[projectSymbol].onRendererReady((renderer: any) => {
this[simulatorRendererSymbol] = renderer;
const offFn = this[projectSymbol].onRendererReady(() => {
fn();
});
if (this[simulatorRendererSymbol]) {
fn();
}
return offFn;
}

View File

@ -1,5 +1,6 @@
import { IPublicApiWorkspace } from '@alilc/lowcode-types';
import { Workspace as InnerWorkSpace } from '@alilc/lowcode-workspace';
import { Plugins } from '@alilc/lowcode-shell';
import { Window } from '../model/window';
import { workspaceSymbol } from '../symbols';
@ -21,4 +22,36 @@ export class Workspace implements IPublicApiWorkspace {
registerResourceType(resourceName: string, resourceType: 'editor', options: any): void {
this[workspaceSymbol].registerResourceType(resourceName, resourceType, options);
}
openEditorWindow(resourceName: string, title: string, viewType?: string) {
this[workspaceSymbol].openEditorWindow(resourceName, title, viewType);
}
openEditorWindowById(id: string) {
this[workspaceSymbol].openEditorWindowById(id);
}
removeEditorWindow(resourceName: string, title: string) {
this[workspaceSymbol].removeEditorWindow(resourceName, title);
}
removeEditorWindowById(id: string) {
this[workspaceSymbol].removeEditorWindowById(id);
}
get plugins() {
return new Plugins(this[workspaceSymbol].plugins, true);
}
get windows() {
return this[workspaceSymbol].windows.map(d => new Window(d));
}
onChangeWindows(fn: () => void) {
return this[workspaceSymbol].onChangeWindows(fn);
}
onChangeActiveWindow(fn: () => void) {
return this[workspaceSymbol].onChangeActiveWindow(fn);
}
}

View File

@ -5,6 +5,22 @@ import { EditorWindow } from '@alilc/lowcode-workspace';
export class Window implements IPublicModelWindow {
private readonly [windowSymbol]: EditorWindow;
get id() {
return this[windowSymbol].id;
}
get title() {
return this[windowSymbol].title;
}
get icon() {
return this[windowSymbol].icon;
}
get resourceName() {
return this[windowSymbol].resourceName;
}
constructor(editorWindow: EditorWindow) {
this[windowSymbol] = editorWindow;
}

View File

@ -21,7 +21,6 @@ export const dragonSymbol = Symbol('dragon');
export const componentMetaSymbol = Symbol('componentMeta');
export const dropLocationSymbol = Symbol('dropLocation');
export const simulatorHostSymbol = Symbol('simulatorHost');
export const simulatorRendererSymbol = Symbol('simulatorRenderer');
export const dragObjectSymbol = Symbol('dragObject');
export const locateEventSymbol = Symbol('locateEvent');
export const designerCabinSymbol = Symbol('designerCabin');

View File

@ -1,5 +1,6 @@
import { IPublicModelWindow } from '../model';
import { IPublicResourceOptions } from '../type';
import { IPublicApiPlugins } from '@alilc/lowcode-types';
export interface IPublicApiWorkspace {
/** 是否启用 workspace 模式 */
@ -10,4 +11,21 @@ export interface IPublicApiWorkspace {
/** 注册资源 */
registerResourceType(resourceName: string, resourceType: 'editor', options: IPublicResourceOptions): void;
/** 打开视图窗口 */
openEditorWindow(resourceName: string, title: string, viewType?: string): void;
/** 移除窗口 */
removeEditorWindow(resourceName: string, title: string): void;
plugins: IPublicApiPlugins;
/** 当前设计器的编辑窗口 */
windows: IPublicModelWindow[];
/** 窗口新增/删除的事件 */
onChangeWindows: (fn: () => void) => void;
/** active 窗口变更事件 */
onChangeActiveWindow: (fn: () => void) => void;
}

View File

@ -9,4 +9,13 @@ export interface IPublicModelWindow {
/** 调用当前窗口视图保存钩子 */
save(): Promise<any>;
/** 窗口 id */
id: string;
/** 窗口标题 */
title?: string;
/** 窗口资源名字 */
resourceName?: string;
}

View File

@ -1,7 +1,7 @@
export interface IPublicViewFunctions {
/** 视图初始化 */
/** 视图初始化钩子 */
init?: () => Promise<void>;
/** 资源保存时调用视图的钩子 */
/** 资源保存时,会调用视图的钩子 */
save?: () => Promise<void>;
}
@ -20,6 +20,9 @@ export interface IPublicResourceOptions {
/** 资源描述 */
description?: string;
/** 资源 icon 标识 */
icon?: React.ReactElement;
/** 默认视图类型 */
defaultViewType: string;
@ -35,4 +38,7 @@ export interface IPublicResourceOptions {
import?: (schema: any) => Promise<{
[viewName: string]: any;
}>;
/** 默认标题 */
defaultTitle?: string;
}

View File

@ -2,7 +2,7 @@
*
*/
export type IPublicTypeWidgetConfigArea = 'leftArea' | 'left' | 'rightArea' |
'right' | 'topArea' | 'top' |
'right' | 'topArea' | 'subTopArea' | 'top' |
'toolbar' | 'mainArea' | 'main' |
'center' | 'centerArea' | 'bottomArea' |
'bottom' | 'leftFixedArea' |

View File

@ -33,7 +33,7 @@ import {
IPublicTypePluginMeta,
} from '@alilc/lowcode-types';
import { getLogger } from '@alilc/lowcode-utils';
import { Workspace as InnerWorkspace } from './index';
import { Workspace as InnerWorkspace } from './workspace';
import { EditorWindow } from './editor-window/context';
export class BasicContext {
@ -51,7 +51,7 @@ export class BasicContext {
designer: Designer;
registerInnerPlugins: () => Promise<void>;
innerSetters: InnerSetters;
innerSkeleton: any;
innerSkeleton: InnerSkeleton;
innerHotkey: InnerHotkey;
innerPlugins: LowCodePluginManager;
canvas: Canvas;
@ -65,7 +65,7 @@ export class BasicContext {
const designer: Designer = new Designer({
editor,
viewName,
shellModelFactory: innerWorkspace.shellModelFactory,
shellModelFactory: innerWorkspace?.shellModelFactory,
});
editor.set('designer' as any, designer);
@ -132,7 +132,7 @@ export class BasicContext {
// 注册一批内置插件
this.registerInnerPlugins = async function registerPlugins() {
await innerWorkspace.registryInnerPlugin(designer, editor, plugins);
await innerWorkspace?.registryInnerPlugin(designer, editor, plugins);
};
}
}

View File

@ -1,7 +1,7 @@
import { makeObservable, obx } from '@alilc/lowcode-editor-core';
import { IPublicEditorView, IPublicViewFunctions } from '@alilc/lowcode-types';
import { flow } from 'mobx';
import { Workspace as InnerWorkspace } from '../';
import { Workspace as InnerWorkspace } from '../workspace';
import { BasicContext } from '../base-context';
import { EditorWindow } from '../editor-window/context';
import { getWebviewPlugin } from '../inner-plugins/webview';

View File

@ -1,14 +1,15 @@
import { observer } from '@alilc/lowcode-editor-core';
import { BuiltinLoading } from '@alilc/lowcode-designer';
import { engineConfig, observer } from '@alilc/lowcode-editor-core';
import {
Workbench,
} from '@alilc/lowcode-editor-skeleton';
import { Component } from 'react';
import { PureComponent } from 'react';
import { Context } from './context';
export * from '../base-context';
@observer
export class EditorView extends Component<{
export class EditorView extends PureComponent<{
editorView: Context;
active: boolean;
}, any> {
@ -17,7 +18,8 @@ export class EditorView extends Component<{
const editorView = this.props.editorView;
const skeleton = editorView.innerSkeleton;
if (!editorView.isInit) {
return null;
const Loading = engineConfig.get('loadingComponent', BuiltinLoading);
return <Loading />;
}
return (

View File

@ -1,12 +1,21 @@
import { uniqueId } from '@alilc/lowcode-utils';
import { makeObservable, obx } from '@alilc/lowcode-editor-core';
import { Context } from '../editor-view/context';
import { Workspace } from '..';
import { Workspace } from '../workspace';
import { Resource } from '../resource';
export class EditorWindow {
constructor(readonly resource: Resource, readonly workspace: Workspace) {
id: string = uniqueId('window');
icon: React.ReactElement | undefined;
constructor(readonly resource: Resource, readonly workspace: Workspace, public title: string | undefined = '') {
makeObservable(this);
this.init();
this.icon = resource.icon;
}
get resourceName(): string {
return this.resource.options.name;
}
async importSchema(schema: any) {

View File

@ -1,28 +1,35 @@
import { Component } from 'react';
import { PureComponent } from 'react';
import { EditorView } from '../editor-view/view';
import { observer } from '@alilc/lowcode-editor-core';
import { engineConfig, observer } from '@alilc/lowcode-editor-core';
import { EditorWindow } from './context';
import { BuiltinLoading } from '@alilc/lowcode-designer';
@observer
export class EditorWindowView extends Component<{
export class EditorWindowView extends PureComponent<{
editorWindow: EditorWindow;
active: boolean;
}, any> {
render() {
const { resource, editorView, editorViews } = this.props.editorWindow;
const { active } = this.props;
const { editorView, editorViews } = this.props.editorWindow;
if (!editorView) {
return null;
const Loading = engineConfig.get('loadingComponent', BuiltinLoading);
return (
<div className={`workspace-engine-main ${active ? 'active' : ''}`}>
<Loading />
</div>
);
}
return (
<div className="workspace-engine-main">
<div className={`workspace-engine-main ${active ? 'active' : ''}`}>
{
Array.from(editorViews.values()).map((editorView: any) => {
return (
<EditorView
resource={resource}
key={editorView.name}
active={editorView.active}
editorView={editorView}
defaultViewType
/>
);
})

View File

@ -1,70 +1 @@
import { Designer } from '@alilc/lowcode-designer';
import { Editor } from '@alilc/lowcode-editor-core';
import {
Skeleton as InnerSkeleton,
} from '@alilc/lowcode-editor-skeleton';
import { Plugins } from '@alilc/lowcode-shell';
import { IPublicResourceOptions } from '@alilc/lowcode-types';
import { EditorWindow } from './editor-window/context';
import { Resource } from './resource';
export { Resource } from './resource';
export * from './editor-window/context';
export * from './layouts/workbench';
export class Workspace {
readonly editor = new Editor();
readonly skeleton = new InnerSkeleton(this.editor);
constructor(
readonly registryInnerPlugin: (designer: Designer, editor: Editor, plugins: Plugins) => Promise<void>,
readonly shellModelFactory: any,
) {
if (this.defaultResource) {
this.window = new EditorWindow(this.defaultResource, this);
}
}
private _isActive = false;
get isActive() {
return this._isActive;
}
setActive(value: boolean) {
this._isActive = value;
}
editorWindows: [];
window: EditorWindow;
private resources: Map<string, Resource> = new Map();
registerResourceType(resourceName: string, resourceType: 'editor' | 'webview', options: IPublicResourceOptions): void {
if (resourceType === 'editor') {
const resource = new Resource(options);
this.resources.set(resourceName, resource);
if (!this.window) {
this.window = new EditorWindow(this.defaultResource, this);
}
}
}
get defaultResource() {
if (this.resources.size === 1) {
return this.resources.values().next().value;
}
return null;
}
removeResourceType(resourceName: string) {
if (this.resources.has(resourceName)) {
this.resources.delete(resourceName);
}
}
openEditorWindow() {}
}
export { Workspace } from './workspace';

View File

@ -7,6 +7,9 @@ import { Area } from '@alilc/lowcode-editor-skeleton';
export default class LeftArea extends Component<{ area: Area }> {
render() {
const { area } = this.props;
if (area.isEmpty()) {
return null;
}
return (
<div className={classNames('lc-left-area', {
'lc-area-visible': area.visible,

View File

@ -0,0 +1,67 @@
import { Component, Fragment } from 'react';
import classNames from 'classnames';
import { observer } from '@alilc/lowcode-editor-core';
import { Area } from '@alilc/lowcode-editor-skeleton';
@observer
export default class SubTopArea extends Component<{ area: Area; itemClassName?: string }> {
render() {
const { area, itemClassName } = this.props;
if (area.isEmpty()) {
return null;
}
return (
<div className={classNames('lc-sub-top-area engine-actionpane', {
'lc-area-visible': area.visible,
})}
>
<Contents area={area} itemClassName={itemClassName} />
</div>
);
}
}
@observer
class Contents extends Component<{ area: Area; itemClassName?: string }> {
render() {
const { area, itemClassName } = this.props;
const left: any[] = [];
const center: any[] = [];
const right: any[] = [];
area.container.items.slice().sort((a, b) => {
const index1 = a.config?.index || 0;
const index2 = b.config?.index || 0;
return index1 === index2 ? 0 : (index1 > index2 ? 1 : -1);
}).forEach(item => {
const content = (
<div className={itemClassName || ''} key={`top-area-${item.name}`}>
{item.content}
</div>
);
if (item.align === 'center') {
center.push(content);
} else if (item.align === 'left') {
left.push(content);
} else {
right.push(content);
}
});
let children = [];
if (left && left.length) {
children.push(<div className="lc-sub-top-area-left">{left}</div>);
}
if (center && center.length) {
children.push(<div className="lc-sub-top-area-center">{center}</div>);
}
if (right && right.length) {
children.push(<div className="lc-sub-top-area-right">{right}</div>);
}
return (
<Fragment>
{children}
</Fragment>
);
}
}

View File

@ -8,7 +8,7 @@ export default class TopArea extends Component<{ area: Area; itemClassName?: str
render() {
const { area, itemClassName } = this.props;
if (!area?.container?.items?.length) {
if (area.isEmpty()) {
return null;
}

View File

@ -138,7 +138,7 @@ body {
display: flex;
flex-direction: column;
background-color: #edeff3;
.lc-top-area {
.lc-top-area, .lc-sub-top-area {
height: var(--top-area-height);
background-color: var(--color-pane-background);
width: 100%;
@ -150,18 +150,18 @@ body {
display: flex;
}
.lc-top-area-left {
.lc-top-area-left, .lc-sub-top-area-left {
display: flex;
align-items: center;
}
.lc-top-area-center {
.lc-top-area-center, .lc-sub-top-area-center {
flex: 1;
display: flex;
justify-content: center;
margin: 0 8px;
}
.lc-top-area-right {
.lc-top-area-right, .lc-sub-top-area-right {
display: flex;
align-items: center;
> * {
@ -335,6 +335,7 @@ body {
display: flex;
flex-direction: column;
z-index: 10;
position: relative;
.lc-toolbar {
display: flex;
height: var(--toolbar-height);
@ -359,6 +360,12 @@ body {
}
}
}
.lc-workspace-workbench-window {
position: relative;
height: 100%;
}
.lc-right-area {
height: 100%;
width: var(--right-area-width);

View File

@ -11,10 +11,17 @@ import BottomArea from './bottom-area';
import './workbench.less';
import { SkeletonContext } from '../skeleton-context';
import { EditorConfig, PluginClassSet } from '@alilc/lowcode-types';
import { Workspace } from '..';
import { Workspace } from '../workspace';
import SubTopArea from './sub-top-area';
@observer
export class Workbench extends Component<{ workspace: Workspace; config?: EditorConfig; components?: PluginClassSet; className?: string; topAreaItemClassName?: string }> {
export class Workbench extends Component<{
workspace: Workspace;
config?: EditorConfig;
components?: PluginClassSet;
className?: string;
topAreaItemClassName?: string;
}> {
constructor(props: any) {
super(props);
const { config, components, workspace } = this.props;
@ -34,8 +41,20 @@ export class Workbench extends Component<{ workspace: Workspace; config?: Editor
<LeftFloatPane area={skeleton.leftFloatArea} />
<LeftFixedPane area={skeleton.leftFixedArea} />
<div className="lc-workspace-workbench-center">
{/* <Toolbar area={skeleton.toolbar} /> */}
<EditorWindowView editorWindow={workspace.window} />
<>
<SubTopArea area={skeleton.subTopArea} itemClassName={topAreaItemClassName} />
<div className="lc-workspace-workbench-window">
{
workspace.windows.map(d => (
<EditorWindowView
active={d.id === workspace.window.id}
editorWindow={d}
key={d.id}
/>
))
}
</div>
</>
<MainArea area={skeleton.mainArea} />
<BottomArea area={skeleton.bottomArea} />
</div>

View File

@ -19,6 +19,10 @@ export class Resource {
this.options.init(ctx);
}
get icon() {
return this.options.icon;
}
async import(schema: any) {
return await this.options.import?.(schema);
}
@ -38,4 +42,8 @@ export class Resource {
async save(value: any) {
return await this.options.save?.(value);
}
get title() {
return this.options.defaultTitle;
}
}

View File

@ -0,0 +1,169 @@
import { Designer } from '@alilc/lowcode-designer';
import { createModuleEventBus, Editor, IEventBus, makeObservable, obx } from '@alilc/lowcode-editor-core';
import { Plugins } from '@alilc/lowcode-shell';
import { IPublicApiWorkspace, IPublicResourceOptions } from '@alilc/lowcode-types';
import { BasicContext } from './base-context';
import { EditorWindow } from './editor-window/context';
import { Resource } from './resource';
export { Resource } from './resource';
export * from './editor-window/context';
export * from './layouts/workbench';
enum event {
ChangeWindow = 'change_window',
ChangeActiveWindow = 'change_active_window',
}
export class Workspace implements IPublicApiWorkspace {
private context: BasicContext;
private emitter: IEventBus = createModuleEventBus('workspace');
get skeleton() {
return this.context.innerSkeleton;
}
get plugins() {
return this.context.innerPlugins;
}
constructor(
readonly registryInnerPlugin: (designer: Designer, editor: Editor, plugins: Plugins) => Promise<void>,
readonly shellModelFactory: any,
) {
this.init();
makeObservable(this);
}
init() {
this.initWindow();
this.context = new BasicContext(this, '');
}
initWindow() {
if (!this.defaultResource) {
return;
}
const title = this.defaultResource.title;
this.window = new EditorWindow(this.defaultResource, this, title);
this.editorWindowMap.set(this.window.id, this.window);
this.windows.push(this.window);
this.emitChangeWindow();
this.emitChangeActiveWindow();
}
private _isActive = false;
get isActive() {
return this._isActive;
}
setActive(value: boolean) {
this._isActive = value;
}
windows: EditorWindow[] = [];
editorWindowMap: Map<string, EditorWindow> = new Map<string, EditorWindow>();
@obx.ref window: EditorWindow;
private resources: Map<string, Resource> = new Map();
async registerResourceType(resourceName: string, resourceType: 'editor' | 'webview', options: IPublicResourceOptions): Promise<void> {
if (resourceType === 'editor') {
const resource = new Resource(options);
this.resources.set(resourceName, resource);
if (!this.window && this.defaultResource) {
this.initWindow();
}
}
}
get defaultResource(): Resource | null {
if (this.resources.size > 1) {
return this.resources.values().next().value;
}
return null;
}
removeResourceType(resourceName: string) {
if (this.resources.has(resourceName)) {
this.resources.delete(resourceName);
}
}
removeEditorWindowById(id: string) {
const index = this.windows.findIndex(d => (d.id === id));
this.remove(index);
}
private remove(index: number) {
const window = this.windows[index];
this.windows = this.windows.splice(index - 1, 1);
if (this.window === window) {
this.window = this.windows[index] || this.windows[index + 1] || this.windows[index - 1];
this.emitChangeActiveWindow();
}
this.emitChangeWindow();
}
removeEditorWindow(resourceName: string, title: string) {
const index = this.windows.findIndex(d => (d.resourceName === resourceName && d.title));
this.remove(index);
}
openEditorWindowById(id: string) {
const window = this.editorWindowMap.get(id);
if (window) {
this.window = window;
this.emitChangeActiveWindow();
}
}
openEditorWindow(resourceName: string, title: string, viewType?: string) {
const resource = this.resources.get(resourceName);
if (!resource) {
console.error(`${resourceName} is not available`);
return;
}
const filterWindows = this.windows.filter(d => (d.resourceName === resourceName && d.title == title));
if (filterWindows && filterWindows.length) {
this.window = filterWindows[0];
this.emitChangeActiveWindow();
return;
}
this.window = new EditorWindow(resource, this, title);
this.windows.push(this.window);
this.editorWindowMap.set(this.window.id, this.window);
this.emitChangeWindow();
this.emitChangeActiveWindow();
}
onChangeWindows(fn: () => void) {
this.emitter.on(event.ChangeWindow, fn);
return () => {
this.emitter.removeListener(event.ChangeWindow, fn);
};
}
emitChangeWindow() {
this.emitter.emit(event.ChangeWindow);
}
emitChangeActiveWindow() {
this.emitter.emit(event.ChangeActiveWindow);
}
onChangeActiveWindow(fn: () => void) {
this.emitter.on(event.ChangeActiveWindow, fn);
return () => {
this.emitter.removeListener(event.ChangeActiveWindow, fn);
};
}
}