Merge branch 'develop' into release/1.0.14-beta

This commit is contained in:
liujuping 2022-08-24 14:10:09 +08:00
commit d7237d7502
26 changed files with 4471 additions and 187 deletions

View File

@ -9,7 +9,7 @@ const jestConfig = {
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['**/document/node/node.test.ts'],
// testMatch: ['**/node-children.test.ts'],
// testMatch: ['**/history/history.test.ts'],
// testMatch: ['**/plugin/plugin-manager.test.ts'],
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
@ -31,6 +31,7 @@ const jestConfig = {
'!src/builtin-simulator/live-editing/live-editing.ts',
'!src/designer/offset-observer.ts',
'!src/designer/clipboard.ts',
'!src/designer/scroller.ts',
'!src/builtin-simulator/host.ts',
'!**/node_modules/**',
'!**/vendor/**',

View File

@ -2,35 +2,7 @@ import { EventEmitter } from 'events';
import { ISimulatorHost } from '../../simulator';
import { Designer, Point } from '../../designer';
import { cursor } from '@alilc/lowcode-utils';
// import Cursor from './cursor';
// import Pages from './pages';
function makeEventsHandler(
boostEvent: MouseEvent | DragEvent,
sensors: ISimulatorHost[],
): (fn: (sdoc: Document) => void) => void {
const topDoc = window.document;
const sourceDoc = boostEvent.view?.document || topDoc;
// TODO: optimize this logic, reduce listener
// const boostPrevented = boostEvent.defaultPrevented;
const docs = new Set<Document>();
// if (boostPrevented || isDragEvent(boostEvent)) {
docs.add(topDoc);
// }
docs.add(sourceDoc);
// if (sourceDoc !== topDoc || isDragEvent(boostEvent)) {
sensors.forEach(sim => {
const sdoc = sim.contentDocument;
if (sdoc) {
docs.add(sdoc);
}
});
// }
return (handle: (sdoc: Document) => void) => {
docs.forEach(doc => handle(doc));
};
}
import { makeEventsHandler } from '../../utils/misc';
// 拖动缩放
export default class DragResizeEngine {
@ -73,6 +45,7 @@ export default class DragResizeEngine {
const masterSensors = this.getMasterSensors();
/* istanbul ignore next */
const createResizeEvent = (e: MouseEvent | DragEvent): Point => {
const sourceDocument = e.view?.document;

View File

@ -54,17 +54,17 @@ export function createSimulator(
const id = asset.id ? ` data-id="${asset.id}"` : '';
const lv = asset.level || level || AssetLevel.Environment;
if (asset.type === AssetType.JSUrl) {
(scripts[lv] || scripts[AssetLevel.App]).push(
scripts[lv].push(
`<script src="${asset.content}"${id}></script>`,
);
} else if (asset.type === AssetType.JSText) {
(scripts[lv] || scripts[AssetLevel.App]).push(`<script${id}>${asset.content}</script>`);
scripts[lv].push(`<script${id}>${asset.content}</script>`);
} else if (asset.type === AssetType.CSSUrl) {
(styles[lv] || styles[AssetLevel.App]).push(
styles[lv].push(
`<link rel="stylesheet" href="${asset.content}"${id} />`,
);
} else if (asset.type === AssetType.CSSText) {
(styles[lv] || styles[AssetLevel.App]).push(
styles[lv].push(
`<style type="text/css"${id}>${asset.content}</style>`,
);
}
@ -98,8 +98,9 @@ export function createSimulator(
doc.close();
return new Promise((resolve) => {
if (win.SimulatorRenderer || host.renderer) {
return resolve(win.SimulatorRenderer || host.renderer);
const renderer = win.SimulatorRenderer || host.renderer;
if (renderer) {
return resolve(renderer);
}
const loaded = () => {
resolve(win.SimulatorRenderer || host.renderer);

View File

@ -420,7 +420,7 @@ export class Designer {
}
get(key: string): any {
return this.props ? this.props[key] : null;
return this.props?.[key];
}
@obx.ref private _simulatorComponent?: ComponentType<any>;

View File

@ -6,6 +6,7 @@ import { DropLocation } from './location';
import { Node, DocumentModel } from '../document';
import { ISimulatorHost, isSimulatorHost, NodeInstance, ComponentInstance } from '../simulator';
import { Designer } from './designer';
import { makeEventsHandler } from '../utils/misc';
export interface LocateEvent {
readonly type: 'LocateEvent';
@ -135,7 +136,7 @@ export function isShaken(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent)
);
}
function isInvalidPoint(e: any, last: any): boolean {
export function isInvalidPoint(e: any, last: any): boolean {
return (
e.clientX === 0 &&
e.clientY === 0 &&
@ -144,7 +145,7 @@ function isInvalidPoint(e: any, last: any): boolean {
);
}
function isSameAs(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean {
export function isSameAs(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean {
return e1.clientY === e2.clientY && e1.clientX === e2.clientX;
}
@ -159,31 +160,6 @@ function getSourceSensor(dragObject: DragObject): ISimulatorHost | null {
return dragObject.nodes[0]?.document.simulator || null;
}
/**
* make a handler that listen all sensors:document, avoid frame lost
*/
function makeEventsHandler(
boostEvent: MouseEvent | DragEvent,
sensors: ISimulatorHost[],
): (fn: (sdoc: Document) => void) => void {
const topDoc = window.document;
const sourceDoc = boostEvent.view?.document || topDoc;
// TODO: optimize this logic, reduce listener
const docs = new Set<Document>();
docs.add(topDoc);
docs.add(sourceDoc);
sensors.forEach((sim) => {
const sdoc = sim.contentDocument;
if (sdoc) {
docs.add(sdoc);
}
});
return (handle: (sdoc: Document) => void) => {
docs.forEach((doc) => handle(doc));
};
}
function isDragEvent(e: any): e is DragEvent {
return e?.type?.startsWith('drag');
}
@ -325,6 +301,7 @@ export class Dragon {
const locateEvent = createLocateEvent(e);
const sensor = chooseSensor(locateEvent);
/* istanbul ignore next */
if (isRGL) {
// 禁止被拖拽元素的阻断
const nodeInst = dragObject.nodes[0].getDOMNode();
@ -429,6 +406,7 @@ export class Dragon {
// 发送drop事件
if (e) {
const { isRGL, rglNode } = getRGL(e);
/* istanbul ignore next */
if (isRGL && this._canDrop) {
const tarNode = dragObject.nodes[0];
if (rglNode.id !== tarNode.id) {
@ -468,7 +446,7 @@ export class Dragon {
this._dragging = false;
try {
this.emitter.emit('dragend', { dragObject, copy });
} catch (ex) {
} catch (ex) /* istanbul ignore next */ {
exception = ex;
}
}
@ -489,6 +467,7 @@ export class Dragon {
doc.removeEventListener('keydown', checkcopy, false);
doc.removeEventListener('keyup', checkcopy, false);
});
/* istanbul ignore next */
if (exception) {
throw exception;
}
@ -509,7 +488,7 @@ export class Dragon {
if (!sourceDocument || sourceDocument === document) {
evt.globalX = e.clientX;
evt.globalY = e.clientY;
} /* istanbul ignore next */ else {
} else /* istanbul ignore next */ {
// event from simulator sandbox
let srcSim: ISimulatorHost | undefined;
const lastSim = lastSensor && isSimulatorHost(lastSensor) ? lastSensor : null;
@ -616,6 +595,7 @@ export class Dragon {
}
}
/* istanbul ignore next */
private getMasterSensors(): ISimulatorHost[] {
return Array.from(
new Set(

View File

@ -220,8 +220,10 @@ export class DocumentModel {
if (this.hasNode(schema?.id)) {
schema.id = null;
}
/* istanbul ignore next */
if (schema.id) {
node = this.getNode(schema.id);
// TODO: 底下这几段代码似乎永远都进不去
if (node && node.componentName === schema.componentName) {
if (node.parent) {
node.internalSetParent(null, false);
@ -239,12 +241,6 @@ export class DocumentModel {
// todo: this.activeNodes?.push(node);
}
const origin = this._nodesMap.get(node.id);
if (origin && origin !== node) {
// almost will not go here, ensure the id is unique
origin.internalSetWillPurge();
}
this._nodesMap.set(node.id, node);
this.nodes.add(node);
@ -578,6 +574,7 @@ export class DocumentModel {
/**
* @deprecated
*/
/* istanbul ignore next */
getAddonData(name: string) {
const addon = this._addons.find((item) => item.name === name);
if (addon) {
@ -588,6 +585,7 @@ export class DocumentModel {
/**
* @deprecated
*/
/* istanbul ignore next */
exportAddonData() {
const addons = {};
this._addons.forEach((addon) => {
@ -604,6 +602,7 @@ export class DocumentModel {
/**
* @deprecated
*/
/* istanbul ignore next */
registerAddon(name: string, exportData: any) {
if (['id', 'params', 'layout'].indexOf(name) > -1) {
throw new Error('addon name cannot be id, params, layout');
@ -618,6 +617,7 @@ export class DocumentModel {
});
}
/* istanbul ignore next */
acceptRootNodeVisitor(
visitorName = 'default',
visitorFn: (node: RootNode) => any,
@ -637,6 +637,7 @@ export class DocumentModel {
return visitorResult;
}
/* istanbul ignore next */
getRootNodeVisitor(name: string) {
return this.rootNodeVisitorMap[name];
}

View File

@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
import { Node } from './node';
import { DocumentModel } from '../document-model';
function getModalNodes(node: Node) {
export function getModalNodes(node: Node) {
if (!node) return [];
let nodes: any = [];
if (node.componentMeta.isModal) {
@ -40,44 +40,37 @@ export class ModalNodesManager {
];
}
public getModalNodes() {
getModalNodes() {
return this.modalNodes;
}
public getVisibleModalNode() {
const visibleNode = this.modalNodes
? this.modalNodes.find((node: Node) => {
return node.getVisible();
})
: null;
return visibleNode;
getVisibleModalNode() {
return this.getModalNodes().find((node: Node) => node.getVisible());
}
public hideModalNodes() {
if (this.modalNodes) {
this.modalNodes.forEach((node: Node) => {
node.setVisible(false);
});
}
hideModalNodes() {
this.modalNodes.forEach((node: Node) => {
node.setVisible(false);
});
}
public setVisible(node: Node) {
setVisible(node: Node) {
this.hideModalNodes();
node.setVisible(true);
}
public setInvisible(node: Node) {
setInvisible(node: Node) {
node.setVisible(false);
}
public onVisibleChange(func: () => any) {
onVisibleChange(func: () => any) {
this.emitter.on('visibleChange', func);
return () => {
this.emitter.removeListener('visibleChange', func);
};
}
public onModalNodesChange(func: () => any) {
onModalNodesChange(func: () => any) {
this.emitter.on('modalNodesChange', func);
return () => {
this.emitter.removeListener('modalNodesChange', func);
@ -122,7 +115,7 @@ export class ModalNodesManager {
}
}
public setNodes() {
setNodes() {
const nodes = getModalNodes(this.page.getRoot()!);
this.modalNodes = nodes;
this.modalNodes.forEach((node: Node) => {

View File

@ -161,6 +161,7 @@ export class NodeChildren {
}
}
const { document } = node;
/* istanbul ignore next */
if (globalContext.has('editor')) {
globalContext.get('editor').emit('node.remove', { node, index: i });
}
@ -197,6 +198,7 @@ export class NodeChildren {
const i = children.indexOf(node);
if (node.parent) {
/* istanbul ignore next */
globalContext.has('editor') &&
globalContext.get('editor').emit('node.remove.topLevel', {
node,
@ -229,6 +231,7 @@ export class NodeChildren {
node,
});
this.emitter.emit('insert', node);
/* istanbul ignore next */
if (globalContext.has('editor')) {
globalContext.get('editor').emit('node.add', { node });
}

View File

@ -259,7 +259,7 @@ export class Prop implements IPropParent {
} else {
this._type = 'map';
}
} /* istanbul ignore next */ else {
} else /* istanbul ignore next */ {
this._type = 'expression';
this._value = {
type: 'JSExpression',
@ -502,6 +502,7 @@ export class Prop implements IPropParent {
*/
@action
delete(prop: Prop): void {
/* istanbul ignore else */
if (this._items) {
const i = this._items.indexOf(prop);
if (i > -1) {
@ -519,6 +520,7 @@ export class Prop implements IPropParent {
*/
@action
deleteKey(key: string): void {
/* istanbul ignore else */
if (this.maps) {
const prop = this.maps.get(key);
if (prop) {

View File

@ -147,9 +147,8 @@ export class Selection {
if (n === PositionNO.Contains || n === PositionNO.TheSame) {
isTop = false;
break;
}
// node contains nodes[i], delete nodes[i]
if (n === PositionNO.ContainedBy) {
} else if (n === PositionNO.ContainedBy) {
// node contains nodes[i], delete nodes[i]
nodes.splice(i, 1);
}
}

View File

@ -1,4 +1,5 @@
import Viewport from '../builtin-simulator/viewport';
import { ISimulatorHost } from '../simulator';
export function isElementNode(domNode: Element) {
return domNode.nodeType === Node.ELEMENT_NODE;
@ -28,4 +29,28 @@ export function isDOMNodeVisible(domNode: Element, viewport: Viewport) {
*/
export function normalizeTriggers(triggers: string[]) {
return triggers.map((trigger: string) => trigger?.toUpperCase());
}
/**
* make a handler that listen all sensors:document, avoid frame lost
*/
export function makeEventsHandler(
boostEvent: MouseEvent | DragEvent,
sensors: ISimulatorHost[],
): (fn: (sdoc: Document) => void) => void {
const topDoc = window.document;
const sourceDoc = boostEvent.view?.document || topDoc;
const docs = new Set<Document>();
docs.add(topDoc);
docs.add(sourceDoc);
sensors.forEach((sim) => {
const sdoc = sim.contentDocument;
if (sdoc) {
docs.add(sdoc);
}
});
return (handle: (sdoc: Document) => void) => {
docs.forEach((doc) => handle(doc));
};
}

View File

@ -57,7 +57,8 @@ describe('DragResizeEngine 测试', () => {
});
// do nothing
resizeEngine.from();
const noop = resizeEngine.from();
noop();
const offFrom = resizeEngine.from(document, 'e', mockedBoostFn);

View File

@ -273,6 +273,18 @@ describe('Designer 测试', () => {
expect(designer._componentMetasMap.has('Div')).toBeTruthy();
const { editor: editorFromDesigner2, ...others2 } = designer.props;
expect(others2).toEqual(updatedProps);
// 第三次设置 props跟第二次值一样for 覆盖率测试
const updatedProps2 = updatedProps;
designer.setProps(updatedProps2);
expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true });
expect(designer.simulatorProps).toEqual({ designMode: 'live' });
expect(designer.suspensed).toBeFalsy();
expect(designer._componentMetasMap.has('Button')).toBeTruthy();
expect(designer._componentMetasMap.has('Div')).toBeTruthy();
const { editor: editorFromDesigner3, ...others3 } = designer.props;
expect(others3).toEqual(updatedProps);
});
describe('getSuitableInsertion', () => {
@ -313,6 +325,70 @@ describe('Designer 测试', () => {
});
});
it('getComponentMetasMap', () => {
designer.createComponentMeta({
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'procode',
tags: ['布局'],
});
expect(designer.getComponentMetasMap().get('Div')).not.toBeUndefined();
});
it('refreshComponentMetasMap', () => {
designer.createComponentMeta({
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'procode',
tags: ['布局'],
});
const originalMetasMap = designer.getComponentMetasMap();
designer.refreshComponentMetasMap();
expect(originalMetasMap).not.toBe(designer.getComponentMetasMap());
});
describe('loadIncrementalAssets', () => {
it('components && packages', async () => {
editor.set('assets', { components: [], packages: [] });
const fn = jest.fn();
project.mountSimulator({
setupComponents: fn,
});
await designer.loadIncrementalAssets({
components: [{
componentName: 'Div2',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'proCode',
tags: ['布局'],
}],
packages: [],
});
const comps = editor.get('assets').components;
expect(comps).toHaveLength(1);
expect(fn).toHaveBeenCalled();
});
it('no components && packages', async () => {
editor.set('assets', { components: [], packages: [] });
const fn = jest.fn();
project.mountSimulator({
setupComponents: fn,
});
await designer.loadIncrementalAssets({});
expect(fn).not.toHaveBeenCalled();
});
});
it('createLocation / clearLocation', () => {
const mockTarget = {
document: doc,

View File

@ -1,14 +1,18 @@
import { Detecting } from '../../src/designer/detecting';
it('Detecting 测试', () => {
const fn = jest.fn();
const detecting = new Detecting();
detecting.onDetectingChange(fn);
expect(detecting.enable).toBeTruthy();
const mockNode = { document };
detecting.capture(mockNode);
expect(fn).toHaveBeenCalledWith(detecting.current);
expect(detecting.current).toBe(mockNode);
detecting.release({});
detecting.release(mockNode);
expect(detecting.current).toBeNull();

View File

@ -3,16 +3,6 @@ import { set } from '../utils';
import { Editor, globalContext } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import {
isRootNode,
Node,
isNode,
comparePosition,
contains,
insertChild,
insertChildren,
PositionNO,
} from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import {
Dragon,
@ -23,12 +13,10 @@ import {
DragObjectType,
isShaken,
setShaken,
isInvalidPoint,
isSameAs,
} from '../../src/designer/dragon';
import formSchema from '../fixtures/schema/form';
import divMetadata from '../fixtures/component-metadata/div';
import formMetadata from '../fixtures/component-metadata/form';
import otherMeta from '../fixtures/component-metadata/other';
import pageMetadata from '../fixtures/component-metadata/page';
import { fireEvent } from '@testing-library/react';
describe('Dragon 测试', () => {
@ -273,9 +261,32 @@ describe('Dragon 测试', () => {
});
it('addSensor / removeSensor', () => {
const sensor = {};
const sensor = {
locate: () => {},
sensorAvailable: true,
isEnter: () => true,
fixEvent: () => {},
deactiveSensor: () => {},
};
const sensor2 = {};
dragon.addSensor(sensor);
expect(dragon.sensors.length).toBe(1);
expect(dragon.activeSensor).toBeUndefined();
dragon.boost(
{
type: DragObjectType.NodeData,
data: [{ componentName: 'Button' }],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragon.activeSensor).toBe(sensor);
// remove a non-existing sensor
dragon.removeSensor(sensor2);
expect(dragon.sensors.length).toBe(1);
dragon.removeSensor(sensor);
expect(dragon.sensors.length).toBe(0);
});
@ -343,4 +354,16 @@ describe('导出的其他函数', () => {
setShaken(e);
expect(isShaken(e)).toBeTruthy();
});
it('isInvalidPoint', () => {
expect(isInvalidPoint({ clientX: 0, clientY: 0 }, { clientX: 6, clientY: 1 })).toBeTruthy();
expect(isInvalidPoint({ clientX: 0, clientY: 0 }, { clientX: 1, clientY: 6 })).toBeTruthy();
expect(isInvalidPoint({ clientX: 0, clientY: 0 }, { clientX: 6, clientY: 6 })).toBeTruthy();
expect(isInvalidPoint({ clientX: 1, clientY: 1 }, { clientX: 2, clientY: 1 })).toBeFalsy();
});
it('isSameAs', () => {
expect(isSameAs({ clientX: 1, clientY: 1 }, { clientX: 1, clientY: 1 })).toBeTruthy();
expect(isSameAs({ clientX: 1, clientY: 1 }, { clientX: 2, clientY: 1 })).toBeFalsy();
});
});

View File

@ -23,7 +23,7 @@ describe('document-model 测试', () => {
project = designer.project;
});
test('empty schema', () => {
it('empty schema', () => {
const doc = new DocumentModel(project);
expect(doc.rootNode.id).toBe('root');
expect(doc.currentRoot).toBe(doc.rootNode);
@ -44,7 +44,7 @@ describe('document-model 测试', () => {
});
});
test('各种方法测试', () => {
it('各种方法测试', () => {
const doc = new DocumentModel(project, formSchema);
const mockNode = { id: 1 };
doc.addWillPurge(mockNode);
@ -115,8 +115,89 @@ describe('document-model 测试', () => {
expect(doc.history).toBe(doc.getHistory());
});
it('focusNode - using drillDown', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.focusNode.id).toBe('page');
doc.drillDown(doc.getNode('node_k1ow3cbb'));
expect(doc.focusNode.id).toBe('node_k1ow3cbb');
});
it('focusNode - using drillDown & import', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.focusNode.id).toBe('page');
doc.drillDown(doc.getNode('node_k1ow3cbb'));
doc.import(formSchema);
expect(doc.focusNode.id).toBe('node_k1ow3cbb');
});
it('focusNode - using focusNodeSelector', () => {
const doc = new DocumentModel(project, formSchema);
editor.set('focusNodeSelector', (rootNode) => {
return rootNode.children.get(1);
});
expect(doc.focusNode.id).toBe('node_k1ow3cbb');
});
it('getNodeCount', () => {
const doc = new DocumentModel(project);
// using default schema, only one node
expect(doc.getNodeCount()).toBe(1);
});
it('getNodeSchema', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.getNodeSchema('page').id).toBe('page');
});
it('export - with __isTopFixed__', () => {
formSchema.children[1].props.__isTopFixed__ = true;
const doc = new DocumentModel(project, formSchema);
const schema = doc.export();
expect(schema.children).toHaveLength(3);
expect(schema.children[0].componentName).toBe('RootContent');
expect(schema.children[1].componentName).toBe('RootHeader');
expect(schema.children[2].componentName).toBe('RootFooter');
});
describe('createNode', () => {
it('same id && componentName', () => {
const doc = new DocumentModel(project, formSchema);
const node = doc.createNode({
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
});
expect(node.parent).toBeNull();
});
it('same id && different componentName', () => {
const doc = new DocumentModel(project, formSchema);
const originalNode = doc.getNode('node_k1ow3cbc');
const node = doc.createNode({
componentName: 'RootFooter2',
id: 'node_k1ow3cbc',
props: {},
condition: true,
});
// expect(originalNode.parent).toBeNull();
expect(node.id).not.toBe('node_k1ow3cbc');
});
});
it('setSuspense', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.opened).toBeFalsy();
doc.setSuspense(false);
});
it('registerAddon / getAddonData / exportAddonData', () => {
const doc = new DocumentModel(project);
expect(doc.getAddonData('a')).toBeUndefined();
doc.registerAddon('a', () => 'addon a');
doc.registerAddon('a', () => 'modified addon a');
doc.registerAddon('b', () => 'addon b');
@ -177,6 +258,8 @@ describe('document-model 测试', () => {
expect(comps.find(comp => comp.componentName === 'Page')).toEqual(
{ componentName: 'Page', devMode: 'lowCode' }
);
const comps2 = doc.getComponentsMap(['Div']);
});
it('acceptRootNodeVisitor / getRootNodeVisitor', () => {

View File

@ -1,53 +1,35 @@
import '../../fixtures/window';
import { set, delayObxTick, delay } from '../../utils';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { DocumentModel } from '../../../src/document/document-model';
import {
isRootNode,
Node,
isNode,
comparePosition,
contains,
insertChild,
insertChildren,
PositionNO,
} from '../../../src/document/node/node';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form-with-modal';
import divMetadata from '../../fixtures/component-metadata/div';
import dlgMetadata from '../../fixtures/component-metadata/dialog';
import buttonMetadata from '../../fixtures/component-metadata/button';
import formMetadata from '../../fixtures/component-metadata/form';
import otherMeta from '../../fixtures/component-metadata/other';
import pageMetadata from '../../fixtures/component-metadata/page';
import rootHeaderMetadata from '../../fixtures/component-metadata/root-header';
import rootContentMetadata from '../../fixtures/component-metadata/root-content';
import rootFooterMetadata from '../../fixtures/component-metadata/root-footer';
import { getModalNodes } from '../../../src/document/node/modal-nodes-manager';
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor });
designer.createComponentMeta(dlgMetadata);
project = designer.project;
doc = new DocumentModel(project, formSchema);
});
afterEach(() => {
project.unload();
designer.purge();
editor = null;
designer = null;
project = null;
});
describe('ModalNodesManager 方法测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor });
designer.createComponentMeta(dlgMetadata);
project = designer.project;
doc = new DocumentModel(project, formSchema);
});
afterEach(() => {
project.unload();
designer.purge();
editor = null;
designer = null;
project = null;
});
it('getModalNodes / getVisibleModalNode', () => {
const mgr = doc.modalNodesManager;
const nodes = mgr.getModalNodes();
@ -100,5 +82,30 @@ describe('ModalNodesManager 方法测试', () => {
mgr.addNode(newNode);
expect(visibleMockFn).not.toHaveBeenCalled();
expect(nodesMockFn).not.toHaveBeenCalled();
const newNode2 = new Node(doc, { componentName: 'Dialog' });
mgr.addNode(newNode2);
mgr.setInvisible(newNode2);
mgr.removeNode(newNode2);
const newNode3 = new Node(doc, { componentName: 'Dialog' });
mgr.removeNode(newNode3);
const newNode4 = new Node(doc, { componentName: 'Non-Modal' });
mgr.removeNode(newNode4);
const newNode5 = doc.createNode({ componentName: 'Non-Modal' });
newNode5.remove(); // trigger node destroy
});
});
describe('其他方法', () => {
it('getModalNodes - null', () => {
expect(getModalNodes()).toEqual([]);
});
it('getModalNodes - no children', () => {
const node = doc.createNode({ componentName: 'Leaf', children: 'haha' });
expect(getModalNodes(node)).toEqual([]);
});
});

View File

@ -1,29 +1,13 @@
import '../../fixtures/window';
import { set, delayObxTick, delay } from '../../utils';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { DocumentModel } from '../../../src/document/document-model';
import {
isRootNode,
Node,
isNode,
comparePosition,
contains,
insertChild,
insertChildren,
PositionNO,
} from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import divMetadata from '../../fixtures/component-metadata/div';
import buttonMetadata from '../../fixtures/component-metadata/button';
import formMetadata from '../../fixtures/component-metadata/form';
import otherMeta from '../../fixtures/component-metadata/other';
import pageMetadata from '../../fixtures/component-metadata/page';
import rootHeaderMetadata from '../../fixtures/component-metadata/root-header';
import rootContentMetadata from '../../fixtures/component-metadata/root-content';
import rootFooterMetadata from '../../fixtures/component-metadata/root-footer';
describe('NodeChildren 方法测试', () => {
let editor: Editor;
@ -57,6 +41,49 @@ describe('NodeChildren 方法测试', () => {
expect(firstBtn.children.isEmpty()).toBeTruthy();
});
it('export', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
expect(children.export().length).toBe(2);
});
it('export - Leaf', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.parent!.insertAfter({ componentName: 'Leaf', children: 'haha' });
const { children } = firstBtn.parent!;
expect(children.export().length).toBe(3);
expect(children.export()[2]).toBe('haha');
});
it('import', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.import(children.export());
expect(children.export().length).toBe(2);
});
it('delete', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const leafNode = doc.createNode({ componentName: 'Leaf', children: 'haha' });
firstBtn.parent!.insertAfter(leafNode);
const { children } = firstBtn.parent!;
children.delete(leafNode);
expect(children.export().length).toBe(2);
});
it('delete - 插入已有的节点', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.parent!.insertBefore(firstBtn, firstBtn);
const { children } = firstBtn.parent!;
expect(children.export().length).toBe(2);
});
it('purge / for of', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
@ -65,6 +92,9 @@ describe('NodeChildren 方法测试', () => {
for (const child of children) {
expect(child.isPurged).toBeTruthy();
}
// purge when children is purged
children.purge();
});
it('splice', () => {
@ -138,6 +168,28 @@ describe('NodeChildren 方法测试', () => {
expect(found?.componentName).toBe('Button');
});
it('concat', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const ret = children.concat([doc.createNode({ componentName: 'Button' })]);
expect(ret.length).toBe(3);
});
it('reduce', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
let ret = 0;
ret = children.reduce((count, node) => {
count = count + 1;
return count;
}, 0);
expect(ret).toBe(2);
});
it('mergeChildren', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
@ -159,6 +211,9 @@ describe('NodeChildren 方法测试', () => {
expect(children.size).toBe(3);
expect(changeMockFn).toHaveBeenCalled();
offChange();
// no remover && adder && sorter
children.mergeChildren();
});
it('insert / onInsert', () => {

View File

@ -95,6 +95,7 @@ describe('Prop 类测试', () => {
it('getValue / getAsString / setValue', () => {
expect(strProp.getValue()).toBe('haha');
strProp.setValue('heihei');
strProp.setValue('heihei');
expect(strProp.getValue()).toBe('heihei');
expect(strProp.getAsString()).toBe('heihei');
@ -177,6 +178,7 @@ describe('Prop 类测试', () => {
it('compare', () => {
const newProp = new Prop(mockedPropsInst, 'haha');
const newProp2 = new Prop(mockedPropsInst, { a: 1 });
expect(strProp.compare(newProp)).toBe(0);
expect(strProp.compare(expProp)).toBe(2);
@ -184,6 +186,7 @@ describe('Prop 类测试', () => {
expect(strProp.compare(newProp)).toBe(2);
strProp.unset();
expect(strProp.compare(newProp)).toBe(0);
expect(strProp.compare(newProp2)).toBe(2);
});
it('isVirtual', () => {
@ -435,6 +438,28 @@ describe('Prop 类测试', () => {
prop = new Prop(mockedPropsInst, [undefined, undefined], '___loopArgs___');
expect(prop.getValue()).toBeUndefined();
});
it('迭代器 / map / forEach', () => {
const listProp = new Prop(mockedPropsInst, [1, 2]);
const mockedFn = jest.fn();
for (const item of listProp) {
mockedFn();
}
expect(mockedFn).toHaveBeenCalledTimes(2);
mockedFn.mockClear();
listProp.forEach((item) => {
mockedFn();
});
expect(mockedFn).toHaveBeenCalledTimes(2);
mockedFn.mockClear();
listProp.map((item) => {
return mockedFn();
});
expect(mockedFn).toHaveBeenCalledTimes(2);
mockedFn.mockClear();
});
});
});

View File

@ -67,6 +67,7 @@ describe('选择区测试', () => {
expect(selection.selected).toEqual(['node_k1ow3cbj', 'form']);
selectionChangeHandler.mockClear();
selection.remove('node_k1ow3cbj_fake');
selection.remove('node_k1ow3cbj');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
@ -141,7 +142,7 @@ describe('选择区测试', () => {
selectionChangeHandler.mockClear();
});
it('dispose 方法', () => {
it('dispose 方法 - 选中的节点没有被删除的', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
@ -152,16 +153,13 @@ describe('选择区测试', () => {
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj', 'form2']);
selection.selectAll(['form', 'node_k1ow3cbj']);
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.dispose();
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'node_k1ow3cbj']);
expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']);
selectionChangeHandler.mockClear();
expect(selectionChangeHandler).not.toHaveBeenCalled();
});
it('containsNode 方法', () => {
@ -242,4 +240,50 @@ describe('选择区测试', () => {
expect(selection.selected).toEqual(['page']);
selectionChangeHandler.mockClear();
});
it('getNodes', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
const { currentDocument } = project;
const { selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj', 'form2']);
// form2 is not a valid node
expect(selection.getNodes()).toHaveLength(2);
});
it('getTopNodes - BeforeOrAfter', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
const { currentDocument } = project;
const { selection } = currentDocument!;
selection.selectAll(['node_k1ow3cbj', 'node_k1ow3cbo']);
expect(selection.getTopNodes()).toHaveLength(2);
});
it('getTopNodes', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
const { currentDocument } = project;
const { selection } = currentDocument!;
selection.selectAll(['node_k1ow3cbj', 'node_k1ow3cbo', 'form', 'node_k1ow3cbl', 'form2']);
// form2 is not a valid node, and node_k1ow3cbj is a child node of form
expect(selection.getTopNodes()).toHaveLength(1);
});
});

View File

@ -1,5 +1,5 @@
// @ts-nocheck
import { isElementNode, isDOMNodeVisible, normalizeTriggers } from '../../src/utils/misc';
import { isElementNode, isDOMNodeVisible, normalizeTriggers, makeEventsHandler } from '../../src/utils/misc';
it('isElementNode', () => {
expect(isElementNode(document.createElement('div'))).toBeTruthy();
@ -152,3 +152,13 @@ describe('isDOMNodeVisible', () => {
it('normalizeTriggers', () => {
expect(normalizeTriggers(['n', 'w'])).toEqual(['N', 'W']);
});
it('makeEventsHandler', () => {
const sensor = { contentDocument: document };
// no contentDocument
const sensor2 = {};
const bind = makeEventsHandler({ view: { document } } as any, [sensor, sensor2]);
const fn = jest.fn();
bind((doc) => fn(doc));
expect(fn).toHaveBeenCalledTimes(1);
});

View File

@ -11,7 +11,10 @@ export class PopupPipe {
private currentId?: string;
create(props?: object): { send: (content: ReactNode, title: ReactNode) => void; show: (target: Element) => void } {
create(props?: object): {
send: (content: ReactNode, title: ReactNode) => void;
show: (target: Element) => void;
} {
let sendContent: ReactNode = null;
let sendTitle: ReactNode = null;
const id = uniqueId('popup');
@ -60,26 +63,30 @@ export class PopupPipe {
}
}
export default class PopupService extends Component<{ popupPipe?: PopupPipe; actionKey?: string; safeId?: string }> {
export default class PopupService extends Component<{
popupPipe?: PopupPipe;
actionKey?: string;
safeId?: string;
popupContainer?: string;
}> {
private popupPipe = this.props.popupPipe || new PopupPipe();
componentWillUnmount() {
this.popupPipe.purge();
}
render() {
const { children, actionKey, safeId } = this.props;
const { children, actionKey, safeId, popupContainer } = this.props;
return (
<PopupContext.Provider value={this.popupPipe}>
{children}
<PopupContent key={`pop${ actionKey}`} safeId={safeId} />
<PopupContent key={`pop${actionKey}`} safeId={safeId} popupContainer={popupContainer} />
</PopupContext.Provider>
);
}
}
export class PopupContent extends PureComponent<{ safeId?: string }> {
export class PopupContent extends PureComponent<{ safeId?: string; popupContainer?: string }> {
static contextType = PopupContext;
popupContainerId = uniqueId('popupContainer');
@ -143,11 +150,11 @@ export class PopupContent extends PureComponent<{ safeId?: string }> {
visible={visible}
offset={[offsetX, 0]}
hasMask={false}
onVisibleChange={(visible, type) => {
onVisibleChange={(_visible, type) => {
if (avoidLaterHidden) {
return;
}
if (!visible && type === 'closeClick') {
if (!_visible && type === 'closeClick') {
this.setState({ visible: false });
}
}}
@ -159,10 +166,11 @@ export class PopupContent extends PureComponent<{ safeId?: string }> {
id={this.props.safeId}
safeNode={id}
closeable
container={this.props.popupContainer}
>
<div className="lc-ballon-title">{title}</div>
<div className="lc-ballon-content">
<PopupService actionKey={actionKey} safeId={id}>
<PopupService actionKey={actionKey} safeId={id} popupContainer={this.popupContainerId}>
<ConfigProvider popupContainer={this.popupContainerId}>
{content}
</ConfigProvider>
@ -170,6 +178,7 @@ export class PopupContent extends PureComponent<{ safeId?: string }> {
</div>
<div id={this.popupContainerId} />
<div id="engine-variable-setter-dialog" />
<div id="engine-popup-container" />
</Drawer>
);
}

View File

@ -349,7 +349,6 @@ export default function baseRendererFactory(): IBaseRenderComponent {
this.__dataHelper.getInitData()
.then((res: any) => {
if (isEmpty(res)) {
this.forceUpdate();
return resolve({});
}
this.setState(res, resolve as () => void);

687
specs/assets-spec.md Normal file
View File

@ -0,0 +1,687 @@
# 《低代码引擎资产包协议规范》
# 1 介绍
## 1.1 本协议规范涉及的问题域
- 定义本协议版本号规范
- 定义本协议中每个子规范需要被支持的 Level
- 定义本协议相关的领域名词
- 定义低代码资产包协议版本号规范A
- 定义低代码资产包协议组件及依赖资源描述规范A
- 定义低代码资产包协议组件描述资源加载规范A
- 定义低代码资产包协议组件在面板展示规范AA
## 1.2 协议草案起草人
- 撰写:金禅、璿玑、彼洋
- 审阅:力皓、絮黎、光弘、戊子、潕量、游鹿
## 1.3 版本号
1.1.0
## 1.4 协议版本号规范A
本协议采用语义版本号,版本号格式为 `major.minor.patch` 的形式。
- major 是大版本号用于发布不向下兼容的协议格式修改
- minor 是小版本号用于发布向下兼容的协议功能新增
- patch 是补丁号:用于发布向下兼容的协议问题修正
## 1.5 协议中子规范 Level 定义
| 规范等级 | 实现要求 |
| -------- | ------------------------------------------------------------ |
| A | 基础规范,低代码引擎核心层支持; |
| AA | 推荐规范由低代码引擎官方插件、setter 支持。 |
| AAA | 参考规范,需由基于引擎的上层搭建平台支持,实现可参考该规范。 |
## 1.6 名词术语
- **资产包**: 低代码引擎加载资源的动态数据集合,主要包含组件及其依赖的资源、组件低代码描述、动态插件/设置器资源等。
## 1.7 背景
根据低代码引擎的实现,一个组件要在引擎上渲染和配置,需要提供组件的 umd 资源以及组件的`低代码描述`,并且组件通常都是以集合的形式被引擎消费的;除了组件之外,还有组件的依赖资源、引擎的动态插件/设置器等资源也需要注册到引擎中;因此我们定义了“低代码资产包”这个数据结构,来描述引擎所需加载的动态资源的集合。
## 1.8 受众
本协议适用于使用“低代码引擎”构建搭建平台的开发者,通过本协议的定义来进行资源的分类和加载。阅读及使用本协议,需要对低代码搭建平台的交互和实现有一定的了解,对前端开发相关技术栈的熟悉也会有帮助,协议中对通用的前端相关术语不会做进一步的解释说明。
# 2 协议结构
协议最顶层结构如下,包含 7 方面的描述内容:
- version { String } 当前协议版本号
- packages { Array } 低代码编辑器中加载的资源列表
- components { Array } 所有组件的描述协议列表
- sort { Object } 用于描述组件面板中的 tab 和 category
- plugins { Array } 设计器插件描述协议列表
- setters { Array } 设计器中设置器描述协议列表
- extConfig { Object } 平台自定义扩展字段
## 2.1 versionA
定义当前协议 schema 的版本号;
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| ---------- | ------ | ---------- | -------- | ------ |
| version | String | 协议版本号 | - | 1.1.0 |
## 2.2 packagesA
定义低代码编辑器中加载的资源列表,包含公共库和组件(库) cdn 资源等;
| 字段 | 字段描述 | 字段类型 | 规范等级 | 备注 |
| -------------------- | --------------------------------------------------------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| packages[].id? | 资源唯一标识 | String | A | 资源唯一标识,如果为空,则以 package 为唯一标识 |
| packages[].title? | 资源标题 | String | A | 资源标题 |
| packages[].package | npm 包名 | String | A | 组件资源唯一标识 |
| packages[].version | npm 包版本号 | String | A | 组件资源版本号 |
| packages[].type | 资源包类型 | String | AA | 取值为: proCode源码、lowCode低代码默认为 proCode |
| packages[].schema | 低代码组件 schema 内容 | object | AA | 取值为: proCode源码、lowCode低代码 |
| packages[].deps | 当前资源包的依赖资源的唯一标识列表 | Array<String> | A | 唯一标识为 id 或者 package 对应的值 |
| packages[].library | 作为全局变量引用时的名称,用来定义全局变量名 | String | A | 低代码引擎通过该字段获取组件实例 |
| packages[].editUrls | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array<String> | A | 低代码引擎编辑器会加载这些 url |
| packages[].urls | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array<String> | AA | 低代码引擎渲染模块会加载这些 url |
| packages[].advancedEditUrls | 组件多个编辑态视图打包后的 CDN url 列表集合,包含 js 和 css | Object | AAA | 上层平台根据特定标识提取某个编辑态的资源,低代码引擎编辑器会加载这些资源,优先级高于 packages[].editUrls |
| packages[].advancedUrls | 组件多个端的渲染态视图打包后的 CDN url 列表集合,包含 js 和 css | Object | AAA | 上层平台根据特定标识提取某个渲染态的资源, 低代码引擎渲染模块会加载这些资源,优先级高于 packages[].urls |
| packages[].external | 当前资源在作为其他资源的依赖,在其他依赖打包时时是否被排除了(同 webpack 中 external 概念) | Boolean | AAA | 某些资源会被单独提取出来,是其他依赖的前置依赖,根据这个字段决定是否提前加载该资源 |
| packages[].loadEnv | 指定当前资源加载的环境 | Array<String> | AAA | 主要用于指定 external 资源加载的环境,取值为 design(设计态)、runtime(预览态)中的一个或多个 |
| packages[].exportSourceId | 标识当前 package 内容是从哪个 package 导出来的 | String | AAA | 此时 urls 无效 |
| packages[].exportSourceLibrary | 标识当前 package 是从 window 上的哪个属性导出来的 | String | AAA | exportSourceId 的优先级高于exportSourceLibrary ,此时 urls 无效 |
| packages[].async | 标识当前 package 资源加载在 window.library 上的是否是一个异步对象 | Boolean | A | async 为 true 时,需要通过 await 才能拿到真正内容 |
| packages[].exportMode | 标识当前 package 从其他 package 的导出方式 | String | A | 目前只支持 `"functionCall"`, exportMode等于 `"functionCall"`当前package 的内容以函数的方式从其他 package 中导出,具体导出接口如: (library: string, packageName: string, isRuntime?: boolean) => any | Promise<any>, library 为当前 package 的 library, packageName 为当前的包名,返回值为当前 package 的导出内容 |
描述举例:
```json
{
"packages": [
{
"title": "fusion 组件库",
"package": "@alifd/next",
"version": "1.23.0",
"urls": [
"https://g.alicdn.com/code/lib/alifd__next/1.23.18/next.min.css",
"https://g.alicdn.com/code/lib/alifd__next/1.23.18/next-with-locales.min.js"
],
"library": "Next"
},
{
"title": "Fusion 精品组件库",
"package": "@alife/fusion-ui",
"version": "0.1.5",
"editUrls": [
"https://g.alicdn.com/code/npm/@alife/fusion-ui/0.1.7/build/lowcode/view.js",
"https://g.alicdn.com/code/npm/@alife/fusion-ui/0.1.7/build/lowcode/view.css"
],
"urls": [
"https://g.alicdn.com/code/npm/@alife/fusion-ui/0.1.7/dist/FusionUI.js",
"https://g.alicdn.com/code/npm/@alife/fusion-ui/0.1.7/dist/FusionUI.css"
],
"library": "FusionUI"
},
{
"title": "低代码组件 A",
"id": "lcc-a",
"version": "0.1.5",
"type": "lowCode",
"schema": {
"componentsMap": [
{
"package": "@ali/vc-text",
"componentName": "Text",
"version": "4.1.1"
}
],
"utils": [
{
"name": "dataSource",
"type": "npm",
"content": {
"package": "@ali/vu-dataSource",
"exportName": "dataSource",
"version": "1.0.4"
}
}
],
"componentsTree": [
{
"defaultProps": {
"content": "这是默认值"
},
"methods": {
"__initMethods__": {
"compiled": "function (exports, module) { /*set actions code here*/ }",
"source": "function (exports, module) { /*set actions code here*/ }",
"type": "js"
}
},
"loopArgs": ["item", "index"],
"props": {
"mobileSlot": {
"type": "JSBlock",
"value": {
"children": [
{
"condition": true,
"hidden": false,
"isLocked": false,
"conditionGroup": "",
"componentName": "Text",
"id": "node_ockxiczf4m2",
"title": "",
"props": {
"maxLine": 0,
"showTitle": false,
"behavior": "NORMAL",
"content": {
"en-US": "Title",
"zh-CN": "页面标题",
"type": "i18n"
},
"__style__": {},
"fieldId": "text_kxiczgj4"
}
}
],
"componentName": "Slot",
"props": {
"slotName": "mobileSlot",
"slotTitle": "mobile 容器"
}
}
},
"className": "component_k8e4naln",
"useDevice": false,
"fieldId": "symbol_k8bnubw4"
},
"condition": true,
"children": [
{
"condition": true,
"loopArgs": [null, null],
"componentName": "Text",
"id": "node_ockxiczf4m4",
"props": {
"maxLine": 0,
"showTitle": false,
"behavior": "NORMAL",
"content": {
"variable": "props.content",
"type": "variable",
"value": {
"use": "zh-CN",
"en-US": "Tips content",
"zh-CN": "这是一个低代码组件",
"type": "i18n"
}
},
"fieldId": "text_kxid1d9n"
}
}
],
"propTypes": [
{
"defaultValue": "这是默认值",
"name": "content",
"title": "文本内容",
"type": "string"
}
],
"componentName": "Component",
"id": "node_k8bnubvz",
"state": {}
}
]
},
"library": "LCCA"
},
{
"title": "多端组件库",
"package": "@ali/atest1",
"version": "1.23.0",
"advancedUrls": {
"default": [
"https://g.alicdn.com/legao-comp/web_bundle_0724/@alife/theme-254/1.24.0/@ali/atest1/1.0.0/theme.7c897c2.css",
"https://g.alicdn.com/legao-comp/web_bundle_0724/@ali/atest1/1.0.0/main.3354663.js"
],
"mobile": [
"https://g.alicdn.com/legao-comp/web_bundle_0724/@alife/theme-254/1.24.0/@ali/atest1/1.0.0/theme.7c897c2.css",
"https://g.alicdn.com/legao-comp/web_bundle_0724/@ali/atest1/1.0.0/main.mobile.3354663.js"
],
"rax": [
"https://g.alicdn.com/legao-comp/web_bundle_0724/@alife/theme-254/1.24.0/@ali/atest1/1.0.0/theme.7c897c2.css",
"https://g.alicdn.com/legao-comp/web_bundle_0724/@ali/atest1/1.0.0/main.rax.3354663.js"
]
},
"advancedEditUrls": {
"design": [
"https://g.alicdn.com/legao-comp/web_bundle_0724/@alife/theme-254/1.24.0/@ali/atest1/1.0.0/theme.7c897c2.css",
"https://g.alicdn.com/legao-comp/web_bundle_0724/@ali/atest1/1.0.0/editView.design.js"
],
"default": [
"https://g.alicdn.com/legao-comp/web_bundle_0724/@alife/theme-254/1.24.0/@ali/atest1/1.0.0/theme.7c897c2.css",
"https://g.alicdn.com/legao-comp/web_bundle_0724/@ali/atest1/1.0.0/editView.js"
]
},
"library": "Atest1"
},
{
"library":"UiPaaSServerless3",
"advancedUrls":{
"default":[
"https://g.alicdn.com/legao-comp/serverless3/1.1.0/env-staging-d224466e-0614-497d-8cd5-e4036dc50b70/main.js"
]
},
"id":"UiPaaSServerless3-view",
"type":"procode",
"version":"1.0.0"
},
{
"package":"react-color",
"library":"ReactColor",
"id":"react-color",
"type":"procode",
"version":"2.19.3",
"async":true,
"exportMode":"functionCall",
"exportSourceId":"UiPaaSServerless3-view"
}
]
}
```
## 2.3 components A
定义资产包中包含的所有组件的低代码描述的集合分为“ComponentDescription”和“RemoteComponentDescription”(详见 2.6 TypeScript 定义)
- ComponentDescription: 符合“组件描述协议”的数据,详见物料规范中`2.2.2 组件描述协议`部分;
- RemoteComponentDescription 是将一个或多个 ComponentDescription 构建打包的 js 资源的描述,在浏览器中加载该资源后可获取到其中包含的每个组件的 ComponentDescription 的具体内容;
## 2.4 sort AA
定义组件列表分组
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| ----------------- | -------- | -------------------------------------------------------------------------------------------- | -------- | ---------------------------------------- |
| sort.groupList | String[] | 组件分组,用于组件面板 tab 展示 | - | ['精选组件', '原子组件'] |
| sort.categoryList | String[] | 组件面板中同一个 tab 下的不同区间用 category 区分category 的排序依照 categoryList 顺序排列 | - | ['通用', '数据展示', '表格类', '表单类'] |
## 2.5 plugins (AAA)
自定义设计器插件列表
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| --------------------- | --------- | -------------------- | -------- | ------ |
| plugins[].name | String | 插件名称 | - | - |
| plugins[].title | String | 插件标题 | - | - |
| plugins[].description | String | 插件描述 | - | - |
| plugins[].docUrl | String | 插件文档地址 | - | - |
| plugins[].screenshot | String | 插件截图地址 | - | - |
| plugins[].tags | String[] | 插件标签分类 | - | - |
| plugins[].keywords | String[] | 插件检索关键字 | - | - |
| plugins[].reference | Reference | 插件引用的资源包信息 | - | - |
## 2.6 setters (AAA)
自定义设置器列表
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| --------------------- | --------- | ---------------------- | -------- | ------ |
| setters[].name | String | 设置器组件名称 | - | - |
| setters[].title | String | 设置器标题 | - | - |
| setters[].description | String | 设置器描述 | - | - |
| setters[].docUrl | String | 设置器文档地址 | - | - |
| setters[].screenshot | String | 设置器截图地址 | - | - |
| setters[].tags | String[] | 设置器标签分类 | - | - |
| setters[].keywords | String[] | 设置器检索关键字 | - | - |
| setters[].reference | Reference | 设置器引用的资源包信息 | - | - |
## 2.7 extConfig (AAA)
定义平台相关的扩展内容,用于存放平台自身实现的一些私有协议, 以允许存量平台能够平滑地迁移至标准协议。 extConfig 是一个 key-value 结构的对象,协议不会规定 extConfig 中的字段名称以及类型, 完全自定义
## 2.8 TypeScript 定义
_组件低代码描述相关部分字段含义详见物料规范中`2.2.2 组件描述协议`部分_
```TypeScript
/**
* 资产包协议
*/
export interface Assets {
/**
* 资产包协议版本号
*/
version: string;
/**
* 资源列表
*/
packages?: Array<Package>;
/**
* 所有组件的描述协议集合
*/
components: Array<ComponentDescription|RemoteComponentDescription>;
/**
* 低代码编辑器插件集合
*/
plugins?: Array<PluginDescription>;
/**
* 低代码设置器集合
*/
setters?: Array<SetterDescription>;
/**
* 平台扩展配置
*/
extConfig?: AssetsExtConfig;
/**
* 用于描述组件面板中的 tab 和 category
*/
sort: ComponentSort;
}
export interface AssetsExtConfig{
[index: string]: any;
}
/**
* 描述组件面板中的 tab 和 category 排布
*/
export interface ComponentSort {
/**
* 用于描述组件面板的 tab 项及其排序,例如:["精选组件", "原子组件"]
*/
groupList?: String[];
/**
* 组件面板中同一个 tab 下的不同区间用 category 区分category 的排序依照 categoryList 顺序排列;
*/
categoryList?: String[];
}
/**
* 定义资产包依赖信息
*/
export interface Package {
/**
* 唯一标识
*/
id: string;
/**
* 包名
*/
package: string;
/**
* 包版本号
*/
version: string;
/**
* 资源类型
*/
type: string;
/**
* 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css
*/
urls?: string[] | any;
/**
* 组件多个渲染态视图打包后的 CDN url 列表,包含 js 和 css优先级高于 urls
*/
advancedUrls?: ComplexUrls;
/**
* 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css
*/
editUrls?: string[] | any;
/**
* 组件多个编辑态视图打包后的 CDN url 列表,包含 js 和 css优先级高于 editUrls
*/
advancedEditUrls?: ComplexUrls;
/**
* 低代码组件的 schema 内容
*/
schema?: ComponentSchema;
/**
* 当前资源所依赖的其他资源包的 id 列表
*/
deps?: string[];
/**
* 指定当前资源加载的环境
*/
loadEnv?: LoadEnv[];
/**
* 当前资源是否是 external 资源
*/
external?: boolean;
/**
* 作为全局变量引用时的名称,和 webpack output.library 字段含义一样,用来定义全局变量名
*/
library: string;
/**
* 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容;
*/
exportName?: string;
/**
* 标识当前 package 资源加载在 window.library 上的是否是一个异步对象
*/
async?: boolean;
/**
* 标识当前 package 从其他 package 的导出方式
*/
exportMode?: string;
/**
* 标识当前 package 内容是从哪个 package 导出来的
*/
exportSourceId?: string;
/**
* 标识当前 package 是从 window 上的哪个属性导出来的
*/
exportSourceLibrary?: string;
}
/**
* 复杂 urls 结构,同时兼容简单结构和多模态结构
*/
export type ComplexUrls = string[] | MultiModeUrls;
/**
* 多模态资源
*/
export interface MultiModeUrls {
/**
* 默认的资源 url
*/
default: string[];
/**
* 其他模态资源的 url
*/
[index: string]: string[];
}
/**
* 资源加载环境种类
*/
export enum LoadEnv {
/**
* 设计态
*/
design = "design",
/**
* 运行态
*/
runtime = "runtime"
}
/**
* 低代码设置器描述
*/
export type SetterDescription = PluginDescription;
/**
* 低代码插件器描述
*/
export interface PluginDescription {
/**
* 插件名称
*/
name: string;
/**
* 插件标题
*/
title: string;
/**
* 插件类型
*/
type?: string;
/**
* 插件描述
*/
description?: string;
/**
* 插件文档地址
*/
docUrl: string;
/**
* 插件截图
*/
screenshot: string;
/**
* 插件相关的标签
*/
tags?: string[];
/**
* 插件关键字
*/
keywords?: string[];
/**
* 插件引用的资源信息
*/
reference: Reference;
}
/**
* 资源引用信息Npm 的升级版本,
*/
export interface Reference {
/**
* 引用资源的 id 标识
*/
id?: string;
/**
* 引用资源的包名
*/
package?: string;
/**
* 引用资源的导出对象中的属性值名称
*/
exportName: string;
/**
* 引用 exportName 上的子对象
*/
subName: string;
/**
* 引用的资源主入口
*/
main?: string;
/**
* 是否从引用资源的导出对象中获取属性值
*/
destructuring: boolean;
/**
* 资源版本号
*/
version: string;
}
/**
* 低代码片段
*
* 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema
*/
export interface Snippet {
title: string;
screenshot?: string;
schema: ElementJSON;
}
/**
* 组件低代码描述
*/
export interface ComponentDescription {
componentName: string;
title: string;
description?: string;
docUrl: string;
screenshot: string;
icon?: string;
tags?: string[];
keywords?: string[];
devMode?: 'proCode' | 'lowCode';
npm: Npm;
props: Prop[];
configure: Configure;
/**
* 多模态下的组件描述, 优先级高于 configure
*/
advancedConfigures: MultiModeConfigures;
snippets: Snippet[];
group: string;
category: string;
priority: number;
/**
* 组件引用的资源信息
*/
reference: Reference;
}
export interface MultiModeConfigures {
default: Configure;
[index: string]: Configure;
}
/**
* 远程物料描述
*/
export interface RemoteComponentDescription {
/**
* 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容;
*/
exportName?: string;
/**
* 组件描述的资源链接;
*/
url?: string;
/**
* 组件多模态描述的资源信息,优先级高于 url
*/
advancedUrls?: ComplexUrl;
/**
* 组件(库)的 npm 信息;
*/
package?: {
npm?: string;
};
}
export type ComplexUrl = string | MultiModeUrl
export interface MultiModeUrl {
default: string;
[index: string]: string;
}
export interface ComponentSchema {
version: string;
componentsMap: ComponentsMap;
componentsTree: [ComponentTree];
i18n: I18nMap;
utils: UtilItem[];
}
```
`ComponentSchema` 的定义见[低代码业务组件描述](./1.material-spec.md#221-组件规范)

1462
specs/lowcode-spec.md Normal file

File diff suppressed because it is too large Load Diff

1821
specs/material-spec.md Normal file

File diff suppressed because it is too large Load Diff