mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 09:41:57 +00:00
optimize settings pane
This commit is contained in:
parent
4da47f2300
commit
92ea6505c1
@ -24,7 +24,6 @@ export class Field extends Component<FieldProps> {
|
||||
|
||||
export interface FieldGroupProps extends FieldProps {
|
||||
defaultCollapsed?: boolean;
|
||||
// gap?: number;
|
||||
onExpandChange?: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
|
||||
@ -1,154 +1,9 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import { Tab, Breadcrumb } from '@alifd/next';
|
||||
import { Title, createIcon } from '@ali/lowcode-globals';
|
||||
import { Node } from '@ali/lowcode-designer';
|
||||
import OutlinePane from '@ali/lowcode-plugin-outline-pane';
|
||||
import { SettingsMain, SettingField, isSettingField } from './main';
|
||||
import SettingsPane, { createSettingFieldView } from './settings-pane';
|
||||
import { createSettingFieldView } from './settings/settings-pane';
|
||||
import './transducers/register';
|
||||
import './setters/register';
|
||||
import './style.less';
|
||||
import SettingsMainView from './settings/main-view';
|
||||
|
||||
export default class SettingsMainView extends Component {
|
||||
private main: SettingsMain;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.main = new SettingsMain(props.editor);
|
||||
this.main.onNodesChange(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.main.purge();
|
||||
}
|
||||
|
||||
renderBreadcrumb() {
|
||||
if (this.main.isMulti) {
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{createIcon(this.main.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})}
|
||||
<Title title={this.main.componentMeta!.title} />
|
||||
<span>x {this.main.nodes.length}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let node: Node | null = this.main.nodes[0]!;
|
||||
const items = [];
|
||||
let l = 3;
|
||||
while (l-- > 0 && node) {
|
||||
const props =
|
||||
l === 2
|
||||
? {}
|
||||
: {
|
||||
onMouseOver: hoverNode.bind(null, node, true),
|
||||
onMouseOut: hoverNode.bind(null, node, false),
|
||||
onClick: selectNode.bind(null, node),
|
||||
};
|
||||
items.unshift(<Breadcrumb.Item {...props} key={node.id}><Title title={node.title} /></Breadcrumb.Item>);
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{createIcon(this.main.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})}
|
||||
<Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.main.isNone) {
|
||||
// 未选中节点,提示选中 或者 显示根节点设置
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
<div className="lc-settings-notice">
|
||||
<p>请在左侧画布选中节点</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.main.isSame) {
|
||||
// todo: future support 获取设置项交集编辑
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
<div className="lc-settings-notice">
|
||||
<p>请选中同一类型节点编辑</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { items } = this.main;
|
||||
if (items.length > 5 || items.some(item => !isSettingField(item) || !item.isGroup)) {
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
{this.renderBreadcrumb()}
|
||||
<div className="lc-settings-body">
|
||||
<SettingsPane target={this.main} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
<Tab
|
||||
navClassName="lc-settings-tabs"
|
||||
animation={false}
|
||||
excessMode="dropdown"
|
||||
contentClassName="lc-settings-tabs-content"
|
||||
extra={this.renderBreadcrumb()}
|
||||
>
|
||||
{(items as SettingField[]).map(field => (
|
||||
<Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}>
|
||||
<SettingsPane target={field} key={field.id} />
|
||||
</Tab.Item>
|
||||
))}
|
||||
</Tab>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OutlinePaneEntry extends PureComponent<{ main: SettingsMain }> {
|
||||
state = {
|
||||
outlineInited: false,
|
||||
};
|
||||
private dispose = this.props.main.onceOutlineVisible(() => {
|
||||
this.setState({
|
||||
outlineInited: true,
|
||||
});
|
||||
});
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
render() {
|
||||
if (!this.state.outlineInited) {
|
||||
return null;
|
||||
}
|
||||
return <OutlinePane editor={this.props.main.editor} config={{
|
||||
name: '__IN_SETTINGS__'
|
||||
}} />;
|
||||
}
|
||||
}
|
||||
|
||||
function hoverNode(node: Node, flag: boolean) {
|
||||
node.hover(flag);
|
||||
}
|
||||
function selectNode(node: Node) {
|
||||
node.select();
|
||||
}
|
||||
export default SettingsMainView;
|
||||
|
||||
export { createSettingFieldView };
|
||||
|
||||
@ -1,525 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { uniqueId, DynamicSetter, isDynamicSetter } from '@ali/lowcode-globals';
|
||||
import { ComponentMeta, Node, Designer, Selection } from '@ali/lowcode-designer';
|
||||
import { TitleContent, FieldExtraProps, SetterType, CustomView, FieldConfig, isCustomView } from '@ali/lowcode-globals';
|
||||
import { getTreeMaster } from '@ali/lowcode-plugin-outline-pane';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { Transducer } from './utils';
|
||||
|
||||
export interface SettingTarget {
|
||||
// 所设置的节点集,至少一个
|
||||
readonly nodes: Node[];
|
||||
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
|
||||
readonly items: Array<SettingField | CustomView>;
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
readonly isSame: boolean;
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
readonly isOne: boolean;
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
readonly isMulti: boolean;
|
||||
|
||||
/**
|
||||
* 无
|
||||
*/
|
||||
readonly isNone: boolean;
|
||||
|
||||
/**
|
||||
* 编辑器引用
|
||||
*/
|
||||
readonly editor: object;
|
||||
|
||||
readonly designer?: Designer;
|
||||
|
||||
readonly path: string[];
|
||||
|
||||
readonly top: SettingTarget;
|
||||
|
||||
// 响应式自动运行
|
||||
onEffect(action: () => void): () => void;
|
||||
|
||||
// 获取属性值
|
||||
getPropValue(propName: string | number): any;
|
||||
|
||||
// 设置属性值
|
||||
setPropValue(propName: string | number, value: any): void;
|
||||
|
||||
// 获取附属属性值
|
||||
getExtraPropValue(propName: string): any;
|
||||
|
||||
// 设置附属属性值
|
||||
setExtraPropValue(propName: string, value: any): void;
|
||||
}
|
||||
|
||||
export class SettingField implements SettingTarget {
|
||||
readonly isSettingField = true;
|
||||
readonly id = uniqueId('field');
|
||||
readonly type: 'field' | 'group';
|
||||
readonly isRequired: boolean = false;
|
||||
readonly isGroup: boolean;
|
||||
private _name: string | number;
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
readonly title: TitleContent;
|
||||
readonly editor: any;
|
||||
readonly extraProps: FieldExtraProps;
|
||||
private _setter?: SetterType | DynamicSetter;
|
||||
get setter(): SetterType | null {
|
||||
if (!this._setter) {
|
||||
return null;
|
||||
}
|
||||
if (isDynamicSetter(this._setter)) {
|
||||
return this._setter(this);
|
||||
}
|
||||
return this._setter;
|
||||
}
|
||||
readonly isSame: boolean;
|
||||
readonly isMulti: boolean;
|
||||
readonly isOne: boolean;
|
||||
readonly isNone: boolean;
|
||||
readonly nodes: Node[];
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
readonly designer: Designer;
|
||||
readonly top: SettingTarget;
|
||||
readonly transducer: Transducer;
|
||||
get path() {
|
||||
const path = this.parent.path.slice();
|
||||
if (this.type === 'field') {
|
||||
path.push(String(this.name));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
constructor(readonly parent: SettingTarget, config: FieldConfig) {
|
||||
const { type, title, name, items, setter, extraProps, ...rest } = config;
|
||||
|
||||
if (type == null) {
|
||||
const c = typeof name === 'string' ? name.substr(0, 1) : '';
|
||||
if (c === '#') {
|
||||
this.type = 'group';
|
||||
} else {
|
||||
this.type = 'field';
|
||||
}
|
||||
} else {
|
||||
this.type = type;
|
||||
}
|
||||
// initial self properties
|
||||
this._name = name;
|
||||
// make this reactive
|
||||
this.title = title || (typeof name === 'number' ? `项目 ${name}` : name);
|
||||
this._setter = setter;
|
||||
this.extraProps = {
|
||||
...rest,
|
||||
...extraProps,
|
||||
};
|
||||
this.isRequired = config.isRequired || (setter as any)?.isRequired;
|
||||
this.isGroup = this.type === 'group';
|
||||
|
||||
// copy parent properties
|
||||
this.editor = parent.editor;
|
||||
this.nodes = parent.nodes;
|
||||
this.componentMeta = parent.componentMeta;
|
||||
this.isSame = parent.isSame;
|
||||
this.isMulti = parent.isMulti;
|
||||
this.isOne = parent.isOne;
|
||||
this.isNone = parent.isNone;
|
||||
this.designer = parent.designer!;
|
||||
this.top = parent.top;
|
||||
|
||||
// initial items
|
||||
if (this.type === 'group' && items) {
|
||||
this.initItems(items);
|
||||
}
|
||||
|
||||
this.transducer = new Transducer(this, { setter });
|
||||
}
|
||||
|
||||
onEffect(action: () => void): () => void {
|
||||
return this.designer.autorun(action, true);
|
||||
}
|
||||
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private initItems(items: Array<FieldConfig | CustomView>) {
|
||||
this._items = items.map((item) => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item);
|
||||
});
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach(item => isSettingField(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
createField(config: FieldConfig): SettingField {
|
||||
return new SettingField(this, config);
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
// ====== 当前属性读写 =====
|
||||
|
||||
/**
|
||||
* 判断当前属性值是否一致
|
||||
* 0 无值/多种值
|
||||
* 1 类似值,比如数组长度一样
|
||||
* 2 单一植
|
||||
*/
|
||||
get valueState(): number {
|
||||
if (this.type !== 'field') {
|
||||
return 0;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
const first = this.nodes[0].getProp(propName)!;
|
||||
let l = this.nodes.length;
|
||||
let state = 2;
|
||||
while (l-- > 1) {
|
||||
const next = this.nodes[l].getProp(propName, false);
|
||||
const s = first.compare(next);
|
||||
if (s > 1) {
|
||||
return 0;
|
||||
}
|
||||
if (s === 1) {
|
||||
state = 1;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
getValue(): any {
|
||||
let val: any = null;
|
||||
if (this.type === 'field') {
|
||||
val = this.parent.getPropValue(this.name);
|
||||
}
|
||||
const { getValue } = this.extraProps;
|
||||
return getValue ? getValue(this, val) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
if (this.type === 'field') {
|
||||
this.parent.setPropValue(this.name, val);
|
||||
}
|
||||
const { setValue } = this.extraProps;
|
||||
if (setValue) {
|
||||
setValue(this, val);
|
||||
}
|
||||
}
|
||||
|
||||
setKey(key: string | number) {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName, true)!.key = key;
|
||||
}
|
||||
this._name = key;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName)?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string | number, value: any) {
|
||||
const path = this.type === 'field' ? `${this.name}.${propName}` : propName;
|
||||
this.parent.setPropValue(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string | number): any {
|
||||
const path = this.type === 'field' ? `${this.name}.${propName}` : propName;
|
||||
return this.parent.getPropValue(path);
|
||||
}
|
||||
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.top.getExtraPropValue(propName);
|
||||
}
|
||||
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.top.setExtraPropValue(propName, value);
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeItems();
|
||||
}
|
||||
|
||||
// ======= compatibles ====
|
||||
getHotValue(): any {
|
||||
return this.transducer.toHot(this.getValue());
|
||||
}
|
||||
|
||||
setHotValue(data: any) {
|
||||
this.setValue(this.transducer.toNative(data));
|
||||
}
|
||||
|
||||
getNode() {
|
||||
return this.nodes[0];
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
onValueChange() {
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
|
||||
export function isSettingField(obj: any): obj is SettingField {
|
||||
return obj && obj.isSettingField;
|
||||
}
|
||||
|
||||
export class SettingsMain implements SettingTarget {
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
private _nodes: Node[] = [];
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private _sessionId = '';
|
||||
private _componentMeta: ComponentMeta | null = null;
|
||||
private _isSame: boolean = true;
|
||||
readonly path = [];
|
||||
readonly top: SettingTarget = this;
|
||||
|
||||
get nodes(): Node[] {
|
||||
return this._nodes;
|
||||
}
|
||||
|
||||
get componentMeta() {
|
||||
return this._componentMeta;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
get isSame(): boolean {
|
||||
return this._isSame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
get isOne(): boolean {
|
||||
return this.nodes.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
get isMulti(): boolean {
|
||||
return this.nodes.length > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 无
|
||||
*/
|
||||
get isNone() {
|
||||
return this.nodes.length < 1;
|
||||
}
|
||||
|
||||
private disposeListener: () => void;
|
||||
|
||||
private _designer?: Designer;
|
||||
get designer() {
|
||||
return this._designer || this.editor.get(Designer);
|
||||
}
|
||||
|
||||
constructor(readonly editor: Editor) {
|
||||
const setupSelection = (selection?: Selection) => {
|
||||
if (selection) {
|
||||
if (!this._designer) {
|
||||
this._designer = selection.doc.designer;
|
||||
}
|
||||
this.setup(selection.getNodes());
|
||||
} else {
|
||||
this.setup([]);
|
||||
}
|
||||
};
|
||||
editor.on('designer.selection.change', setupSelection);
|
||||
this.disposeListener = () => {
|
||||
editor.removeListener('designer.selection.change', setupSelection);
|
||||
};
|
||||
(async () => {
|
||||
const designer = await editor.onceGot(Designer);
|
||||
getTreeMaster(designer).onceEnableBuiltin(() => {
|
||||
this.emitter.emit('outline-visible');
|
||||
});
|
||||
setupSelection(designer.currentSelection);
|
||||
})();
|
||||
}
|
||||
|
||||
onEffect(action: () => void): () => void {
|
||||
action();
|
||||
return this.onNodesChange(action);
|
||||
}
|
||||
|
||||
onceOutlineVisible(fn: () => void): () => void {
|
||||
this.emitter.on('outline-visible', fn);
|
||||
return () => {
|
||||
this.emitter.removeListener('outline-visible', fn);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性值
|
||||
*/
|
||||
getPropValue(propName: string): any {
|
||||
if (this.nodes.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return this.nodes[0].getProp(propName, true)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性值
|
||||
*/
|
||||
setPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach(node => {
|
||||
node.setPropValue(propName, value);
|
||||
});
|
||||
}
|
||||
|
||||
getExtraPropValue(propName: string) {
|
||||
if (this.nodes.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return this.nodes[0].getExtraProp(propName, false)?.getValue();
|
||||
}
|
||||
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach(node => {
|
||||
node.getExtraProp(propName, true)?.setValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,替换原有值
|
||||
setProps(data: object) {
|
||||
this.nodes.forEach(node => {
|
||||
node.setProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,和原有值合并
|
||||
mergeProps(data: object) {
|
||||
this.nodes.forEach(node => {
|
||||
node.mergeProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
private setup(nodes: Node[]) {
|
||||
this._nodes = nodes;
|
||||
|
||||
// check nodes change
|
||||
const sessionId = this.nodes
|
||||
.map(node => node.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
if (sessionId === this._sessionId) {
|
||||
return;
|
||||
}
|
||||
this._sessionId = sessionId;
|
||||
|
||||
// setups
|
||||
this.setupComponentMeta();
|
||||
|
||||
// todo: enhance when componentType not changed do merge
|
||||
// clear fields
|
||||
this.setupItems();
|
||||
|
||||
// emit change
|
||||
this.emitter.emit('nodeschange');
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach(item => isSettingField(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
private setupComponentMeta() {
|
||||
if (this.nodes.length < 1) {
|
||||
this._isSame = false;
|
||||
this._componentMeta = null;
|
||||
return;
|
||||
}
|
||||
const first = this.nodes[0];
|
||||
const meta = first.componentMeta;
|
||||
const l = this.nodes.length;
|
||||
let theSame = true;
|
||||
for (let i = 1; i < l; i++) {
|
||||
const other = this.nodes[i];
|
||||
if (other.componentMeta !== meta) {
|
||||
theSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (theSame) {
|
||||
this._isSame = true;
|
||||
this._componentMeta = meta;
|
||||
} else {
|
||||
this._isSame = false;
|
||||
this._componentMeta = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupItems() {
|
||||
this.disposeItems();
|
||||
if (this.componentMeta) {
|
||||
this._items = this.componentMeta.configure.map(item => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onNodesChange(fn: () => void): () => void {
|
||||
this.emitter.on('nodeschange', fn);
|
||||
return () => {
|
||||
this.emitter.removeListener('nodeschange', fn);
|
||||
};
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeListener();
|
||||
this.disposeItems();
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Component, Fragment } from 'react';
|
||||
import { Icon, Button, Message } from '@alifd/next';
|
||||
import { Title, SetterType, FieldConfig, SetterConfig } from '@ali/lowcode-globals';
|
||||
import { SettingField } from '../../main';
|
||||
import { createSettingFieldView } from '../../settings-pane';
|
||||
import { SettingField } from '../../settings/setting-field';
|
||||
import { createSettingFieldView } from '../../settings/settings-pane';
|
||||
import { PopupContext, PopupPipe } from '../../popup';
|
||||
import Sortable from './sortable';
|
||||
import './style.less';
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { PureComponent, Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Dropdown, Button, Menu, Icon } from '@alifd/next';
|
||||
import { getSetter, getSettersMap, SetterConfig, computed, obx, CustomView, DynamicProps, DynamicSetter, TitleContent, isSetterConfig, Title, createSetterContent } from '@ali/lowcode-globals';
|
||||
import { SettingField } from 'plugin-settings-pane/src/main';
|
||||
import { SettingField } from 'plugin-settings-pane/src/settings/main';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@ -79,7 +79,7 @@ function nomalizeSetters(setters?: Array<string | SetterConfig | CustomView | Dy
|
||||
export default class MixedSetter extends Component<{
|
||||
field: SettingField;
|
||||
setters?: Array<string | SetterConfig | CustomView | DynamicSetter>;
|
||||
onSetterChange: (field: SettingField, name: string) => void;
|
||||
onSetterChange?: (field: SettingField, name: string) => void;
|
||||
}> {
|
||||
private setters = nomalizeSetters(this.props.setters);
|
||||
@obx.ref private used?: string;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Component, Fragment } from 'react';
|
||||
import { Icon, Button } from '@alifd/next';
|
||||
import { Title, SetterType, FieldConfig } from '@ali/lowcode-globals';
|
||||
import { SettingField } from '../../main';
|
||||
import { createSettingFieldView } from '../../settings-pane';
|
||||
import { createSettingFieldView } from '../../settings/settings-pane';
|
||||
import { SettingField } from '../../settings/setting-field';
|
||||
import { PopupContext, PopupPipe } from '../../popup';
|
||||
import './style.less';
|
||||
|
||||
|
||||
@ -1,246 +0,0 @@
|
||||
import { Component } from 'react';
|
||||
import {
|
||||
createContent,
|
||||
CustomView,
|
||||
DynamicProps,
|
||||
intl,
|
||||
shallowIntl,
|
||||
isSetterConfig,
|
||||
createSetterContent,
|
||||
shallowEqual,
|
||||
} from '@ali/lowcode-globals';
|
||||
import { SettingField, isSettingField, SettingTarget } from './main';
|
||||
import { Field, FieldGroup } from './field';
|
||||
import PopupService from './popup';
|
||||
|
||||
class SettingFieldView extends Component<{ field: SettingField }> {
|
||||
state = {
|
||||
visible: false,
|
||||
value: null,
|
||||
setterProps: {},
|
||||
};
|
||||
private dispose: () => void;
|
||||
private setterType?: string | CustomView;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const { field } = this.props;
|
||||
const { setter } = field;
|
||||
let setterProps: object | DynamicProps = {};
|
||||
if (Array.isArray(setter)) {
|
||||
this.setterType = 'MixedSetter';
|
||||
// setterProps =
|
||||
}
|
||||
if (isSetterConfig(setter)) {
|
||||
this.setterType = setter.componentName;
|
||||
if (setter.props) {
|
||||
setterProps = setter.props;
|
||||
}
|
||||
} else if (setter) {
|
||||
this.setterType = setter;
|
||||
}
|
||||
let firstRun = true;
|
||||
this.dispose = field.onEffect(() => {
|
||||
const state: any = {};
|
||||
const { extraProps } = field;
|
||||
const { condition, defaultValue } = extraProps;
|
||||
state.visible = field.isOne && typeof condition === 'function' ? !condition(field) : true;
|
||||
if (state.visible) {
|
||||
state.setterProps = {
|
||||
...(typeof setterProps === 'function' ? setterProps(field) : setterProps),
|
||||
};
|
||||
if (field.type === 'field') {
|
||||
if (defaultValue != null && !('defaultValue' in state.setterProps)) {
|
||||
state.setterProps.defaultValue = defaultValue;
|
||||
}
|
||||
if (field.valueState > 0) {
|
||||
state.value = field.getValue();
|
||||
} else {
|
||||
state.value = null;
|
||||
state.setterProps.multiValue = true;
|
||||
if (!('placeholder' in props)) {
|
||||
state.setterProps.placeholder = intl({
|
||||
type: 'i18n',
|
||||
'zh-CN': '多种值',
|
||||
'en-US': 'Multiple Value',
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: error handling
|
||||
}
|
||||
}
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
this.state = state;
|
||||
} else {
|
||||
this.setState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: any) {
|
||||
const { state } = this;
|
||||
if (
|
||||
nextState.value !== state.value ||
|
||||
nextState.visible !== state.visible ||
|
||||
!shallowEqual(state.setterProps, nextState.setterProps)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visible, value, setterProps } = this.state;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
const { field } = this.props;
|
||||
const { title, extraProps } = field;
|
||||
|
||||
// todo: error handling
|
||||
|
||||
return (
|
||||
<Field title={extraProps.forceInline ? null : title}>
|
||||
{createSetterContent(this.setterType, {
|
||||
// TODO: refresh intl
|
||||
...shallowIntl(setterProps),
|
||||
forceInline: extraProps.forceInline,
|
||||
key: field.id,
|
||||
// === injection
|
||||
prop: field, // for compatible vision
|
||||
field,
|
||||
// === IO
|
||||
value, // reaction point
|
||||
onChange: (value: any) => {
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
field.setValue(value);
|
||||
},
|
||||
})}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingGroupView extends Component<{ field: SettingField }> {
|
||||
state = {
|
||||
visible: false,
|
||||
items: [],
|
||||
};
|
||||
private dispose: () => void;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const { field } = this.props;
|
||||
const { condition } = field.extraProps;
|
||||
let firstRun = true;
|
||||
this.dispose = field.onEffect(() => {
|
||||
const state: any = {};
|
||||
state.visible = field.isOne && typeof condition === 'function' ? !condition(field) : true;
|
||||
if (state.visible) {
|
||||
state.items = field.items.slice();
|
||||
}
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
this.state = state;
|
||||
} else {
|
||||
this.setState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: any) {
|
||||
// todo: shallowEqual ?
|
||||
if (nextState.items !== this.state.items || nextState.visible !== this.state.visible) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field } = this.props;
|
||||
const { title, extraProps } = field;
|
||||
const { defaultCollapsed } = extraProps;
|
||||
const { visible, items } = this.state;
|
||||
// reaction point
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldGroup title={title} defaultCollapsed={defaultCollapsed}>
|
||||
{items.map((item, index) => createSettingFieldView(item, field, index))}
|
||||
</FieldGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSettingFieldView(item: SettingField | CustomView, field: SettingTarget, index?: number) {
|
||||
if (isSettingField(item)) {
|
||||
if (item.isGroup) {
|
||||
return <SettingGroupView field={item} key={item.id} />;
|
||||
} else {
|
||||
return <SettingFieldView field={item} key={item.id} />;
|
||||
}
|
||||
} else {
|
||||
return createContent(item, { key: index, field });
|
||||
}
|
||||
}
|
||||
|
||||
export default class SettingsPane extends Component<{ target: SettingTarget }> {
|
||||
state: { items: Array<SettingField | CustomView> } = {
|
||||
items: [],
|
||||
};
|
||||
private dispose: () => void;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const { target } = this.props;
|
||||
let firstRun = true;
|
||||
this.dispose = target.onEffect(() => {
|
||||
const state = {
|
||||
items: target.items.slice(),
|
||||
};
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
this.state = state;
|
||||
} else {
|
||||
this.setState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: any) {
|
||||
if (nextState.items !== this.state.items) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items } = this.state;
|
||||
const { target } = this.props;
|
||||
return (
|
||||
<div className="lc-settings-pane">
|
||||
{/* todo: add head for single use */}
|
||||
<PopupService>
|
||||
<div className="lc-settings-content">
|
||||
{items.map((item, index) => createSettingFieldView(item, target, index))}
|
||||
</div>
|
||||
</PopupService>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
149
packages/plugin-settings-pane/src/settings/main-view.tsx
Normal file
149
packages/plugin-settings-pane/src/settings/main-view.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import { Tab, Breadcrumb } from '@alifd/next';
|
||||
import { Title, createIcon, observer } from '@ali/lowcode-globals';
|
||||
import { Node } from '@ali/lowcode-designer';
|
||||
import OutlinePane from '@ali/lowcode-plugin-outline-pane';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { SettingsMain } from './main';
|
||||
import SettingsPane from './settings-pane';
|
||||
import { isSettingField, SettingField } from './setting-field';
|
||||
|
||||
@observer
|
||||
export default class SettingsMainView extends Component<{ editor: Editor }> {
|
||||
private main = new SettingsMain(this.props.editor);
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.main.purge();
|
||||
}
|
||||
|
||||
renderBreadcrumb() {
|
||||
const { settings } = this.main;
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
if (settings.isMultiNodes) {
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{createIcon(settings.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})}
|
||||
<Title title={settings.componentMeta!.title} />
|
||||
<span>x {settings.nodes.length}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let node: Node | null = settings.first;
|
||||
const items = [];
|
||||
let l = 3;
|
||||
while (l-- > 0 && node) {
|
||||
const props =
|
||||
l === 2
|
||||
? {}
|
||||
: {
|
||||
onMouseOver: hoverNode.bind(null, node, true),
|
||||
onMouseOut: hoverNode.bind(null, node, false),
|
||||
onClick: selectNode.bind(null, node),
|
||||
};
|
||||
items.unshift(<Breadcrumb.Item {...props} key={node.id}><Title title={node.title} /></Breadcrumb.Item>);
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{createIcon(this.main.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})}
|
||||
<Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { settings } = this.main;
|
||||
if (!settings) {
|
||||
// 未选中节点,提示选中 或者 显示根节点设置
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
<div className="lc-settings-notice">
|
||||
<p>请在左侧画布选中节点</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!settings.isSameComponent) {
|
||||
// todo: future support 获取设置项交集编辑
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
<div className="lc-settings-notice">
|
||||
<p>请选中同一类型节点编辑</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { items } = settings;
|
||||
if (items.length > 5 || items.some(item => !isSettingField(item) || !item.isGroup)) {
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
{this.renderBreadcrumb()}
|
||||
<div className="lc-settings-body">
|
||||
<SettingsPane target={settings} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<OutlinePaneEntry main={this.main} />
|
||||
<Tab
|
||||
navClassName="lc-settings-tabs"
|
||||
animation={false}
|
||||
excessMode="dropdown"
|
||||
contentClassName="lc-settings-tabs-content"
|
||||
extra={this.renderBreadcrumb()}
|
||||
>
|
||||
{(items as SettingField[]).map(field => (
|
||||
<Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}>
|
||||
<SettingsPane target={field} key={field.id} />
|
||||
</Tab.Item>
|
||||
))}
|
||||
</Tab>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OutlinePaneEntry extends PureComponent<{ main: SettingsMain }> {
|
||||
state = {
|
||||
outlineInited: false,
|
||||
};
|
||||
private dispose = this.props.main.onceOutlineVisible(() => {
|
||||
this.setState({
|
||||
outlineInited: true,
|
||||
});
|
||||
});
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
render() {
|
||||
if (!this.state.outlineInited) {
|
||||
return null;
|
||||
}
|
||||
return <OutlinePane editor={this.props.main.editor} config={{
|
||||
name: '__IN_SETTINGS__'
|
||||
}} />;
|
||||
}
|
||||
}
|
||||
|
||||
function hoverNode(node: Node, flag: boolean) {
|
||||
node.hover(flag);
|
||||
}
|
||||
function selectNode(node: Node) {
|
||||
node.select();
|
||||
}
|
||||
74
packages/plugin-settings-pane/src/settings/main.ts
Normal file
74
packages/plugin-settings-pane/src/settings/main.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { obx, computed } from '@ali/lowcode-globals';
|
||||
import { Node, Designer, Selection } from '@ali/lowcode-designer';
|
||||
import { getTreeMaster } from '@ali/lowcode-plugin-outline-pane';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { SettingTopEntry, generateSessionId } from './setting-entry';
|
||||
|
||||
export class SettingsMain {
|
||||
private emitter = new EventEmitter();
|
||||
private _sessionId = '';
|
||||
@obx.ref private _settings?: SettingTopEntry;
|
||||
|
||||
@computed get length(): number | undefined {
|
||||
return this._settings?.nodes.length;
|
||||
}
|
||||
|
||||
@computed get componentMeta() {
|
||||
return this._settings?.componentMeta;
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
private disposeListener: () => void;
|
||||
|
||||
constructor(readonly editor: Editor) {
|
||||
const setupSelection = (selection?: Selection) => {
|
||||
if (selection) {
|
||||
this.setup(selection.getNodes());
|
||||
} else {
|
||||
this.setup([]);
|
||||
}
|
||||
};
|
||||
editor.on('designer.selection.change', setupSelection);
|
||||
this.disposeListener = () => {
|
||||
editor.removeListener('designer.selection.change', setupSelection);
|
||||
};
|
||||
(async () => {
|
||||
const designer = await editor.onceGot(Designer);
|
||||
getTreeMaster(designer).onceEnableBuiltin(() => {
|
||||
this.emitter.emit('outline-visible');
|
||||
});
|
||||
setupSelection(designer.currentSelection);
|
||||
})();
|
||||
}
|
||||
|
||||
private setup(nodes: Node[]) {
|
||||
// check nodes change
|
||||
const sessionId = generateSessionId(nodes);
|
||||
if (sessionId === this._sessionId) {
|
||||
return;
|
||||
}
|
||||
this._sessionId = sessionId;
|
||||
if (nodes.length < 1) {
|
||||
this._settings = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._settings = new SettingTopEntry(this.editor, nodes);
|
||||
}
|
||||
|
||||
onceOutlineVisible(fn: () => void): () => void {
|
||||
this.emitter.on('outline-visible', fn);
|
||||
return () => {
|
||||
this.emitter.removeListener('outline-visible', fn);
|
||||
};
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeListener();
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
}
|
||||
394
packages/plugin-settings-pane/src/settings/setting-entry.ts
Normal file
394
packages/plugin-settings-pane/src/settings/setting-entry.ts
Normal file
@ -0,0 +1,394 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Node, ComponentMeta, Designer } from '@ali/lowcode-designer';
|
||||
import { CustomView, obx, uniqueId, computed, isCustomView } from '@ali/lowcode-globals';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { SettingTarget } from './setting-target';
|
||||
import { SettingField } from './setting-field';
|
||||
|
||||
export function generateSessionId(nodes: Node[]) {
|
||||
return nodes
|
||||
.map((node) => node.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
}
|
||||
|
||||
export class SettingTopEntry implements SettingTarget {
|
||||
private emitter = new EventEmitter();
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private _componentMeta: ComponentMeta | null = null;
|
||||
private _isSame: boolean = true;
|
||||
readonly path = [];
|
||||
readonly top = this;
|
||||
readonly parent = this;
|
||||
|
||||
get componentMeta() {
|
||||
return this._componentMeta;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
get isSameComponent(): boolean {
|
||||
return this._isSame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
get isOneNode(): boolean {
|
||||
return this.nodes.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
get isMultiNodes(): boolean {
|
||||
return this.nodes.length > 1;
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
readonly first: Node;
|
||||
readonly designer: Designer;
|
||||
|
||||
constructor(readonly editor: Editor, readonly nodes: Node[], ) {
|
||||
if (nodes.length < 1) {
|
||||
throw new ReferenceError('nodes should not be empty');
|
||||
}
|
||||
this.id = generateSessionId(nodes);
|
||||
this.first = nodes[0];
|
||||
this.designer = this.first.document.designer;
|
||||
|
||||
// setups
|
||||
this.setupComponentMeta();
|
||||
|
||||
// clear fields
|
||||
this.setupItems();
|
||||
}
|
||||
|
||||
private setupComponentMeta() {
|
||||
// todo: enhance compile a temp configure.compiled
|
||||
const first = this.first;
|
||||
const meta = first.componentMeta;
|
||||
const l = this.nodes.length;
|
||||
let theSame = true;
|
||||
for (let i = 1; i < l; i++) {
|
||||
const other = this.nodes[i];
|
||||
if (other.componentMeta !== meta) {
|
||||
theSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (theSame) {
|
||||
this._isSame = true;
|
||||
this._componentMeta = meta;
|
||||
} else {
|
||||
this._isSame = false;
|
||||
this._componentMeta = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupItems() {
|
||||
if (this.componentMeta) {
|
||||
this._items = this.componentMeta.configure.map((item) => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
@computed getValue(): any {
|
||||
this.first.propsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
this.setProps(val);
|
||||
// TODO: emit value change
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子项
|
||||
*/
|
||||
get(propName: string | number): SettingPropEntry {
|
||||
return new SettingPropEntry(this, propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.setPropValue(propName, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string): any {
|
||||
return this.first.getProp(propName, true)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兄弟项
|
||||
*/
|
||||
getSibling(propName: string | number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得兄弟属性值
|
||||
*/
|
||||
getSiblingValue(propName: string | number): any {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置兄弟属性值
|
||||
*/
|
||||
setSiblingValue(propName: string | number, value: any): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶层附属属性值
|
||||
*/
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.first.getExtraProp(propName, false)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶层附属属性值
|
||||
*/
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.getExtraProp(propName, true)?.setValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,替换原有值
|
||||
setProps(data: object) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.setProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,和原有值合并
|
||||
mergeProps(data: object) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.mergeProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach((item) => isPurgeable(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeItems();
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
// ==== compatibles for vision =====
|
||||
getProp(propName: string | number) {
|
||||
return this.get(propName);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Purgeable {
|
||||
purge(): void;
|
||||
}
|
||||
export function isPurgeable(obj: any): obj is Purgeable {
|
||||
return obj && obj.purge;
|
||||
}
|
||||
|
||||
|
||||
export class SettingPropEntry implements SettingTarget {
|
||||
// === static properties ===
|
||||
readonly editor: Editor;
|
||||
readonly isSameComponent: boolean;
|
||||
readonly isMultiNodes: boolean;
|
||||
readonly isOneNode: boolean;
|
||||
readonly nodes: Node[];
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
readonly designer: Designer;
|
||||
readonly top: SettingTarget;
|
||||
readonly isGroup: boolean;
|
||||
readonly type: 'field' | 'group';
|
||||
readonly id = uniqueId('entry');
|
||||
|
||||
// ==== dynamic properties ====
|
||||
@obx.ref private _name: string | number;
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
@computed get path() {
|
||||
const path = this.parent.path.slice();
|
||||
if (this.type === 'field') {
|
||||
path.push(this.name);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
extraProps: any = {};
|
||||
|
||||
constructor(readonly parent: SettingTarget, name: string | number, type?: 'field' | 'group') {
|
||||
if (type == null) {
|
||||
const c = typeof name === 'string' ? name.substr(0, 1) : '';
|
||||
if (c === '#') {
|
||||
this.type = 'group';
|
||||
} else {
|
||||
this.type = 'field';
|
||||
}
|
||||
} else {
|
||||
this.type = type;
|
||||
}
|
||||
// initial self properties
|
||||
this._name = name;
|
||||
this.isGroup = this.type === 'group';
|
||||
|
||||
// copy parent static properties
|
||||
this.editor = parent.editor;
|
||||
this.nodes = parent.nodes;
|
||||
this.componentMeta = parent.componentMeta;
|
||||
this.isSameComponent = parent.isSameComponent;
|
||||
this.isMultiNodes = parent.isMultiNodes;
|
||||
this.isOneNode = parent.isOneNode;
|
||||
this.designer = parent.designer;
|
||||
this.top = parent.top;
|
||||
}
|
||||
|
||||
setKey(key: string | number) {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName, true)!.key = key;
|
||||
}
|
||||
this._name = key;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName)?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 当前属性读写 =====
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
@computed getValue(): any {
|
||||
let val: any = null;
|
||||
if (this.type === 'field') {
|
||||
val = this.parent.getPropValue(this.name);
|
||||
}
|
||||
const { getValue } = this.extraProps;
|
||||
return getValue ? getValue(this, val) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
if (this.type === 'field') {
|
||||
this.parent.setPropValue(this.name, val);
|
||||
}
|
||||
const { setValue } = this.extraProps;
|
||||
if (setValue) {
|
||||
setValue(this, val);
|
||||
}
|
||||
// TODO: emit value change
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子项
|
||||
*/
|
||||
get(propName: string | number) {
|
||||
const path = this.path.concat(propName).join('.');
|
||||
return this.top.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string | number, value: any) {
|
||||
const path = this.path.concat(propName).join('.');
|
||||
this.top.setPropValue(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string | number): any {
|
||||
return this.top.getPropValue(this.path.concat(propName).join('.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兄弟项
|
||||
*/
|
||||
getSibling(propName: string | number) {
|
||||
return this.parent.get(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得兄弟属性值
|
||||
*/
|
||||
getSiblingValue(propName: string | number): any {
|
||||
return this.parent.getPropValue(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置兄弟属性值
|
||||
*/
|
||||
setSiblingValue(propName: string | number, value: any): void {
|
||||
this.parent.setPropValue(propName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶层附属属性值
|
||||
*/
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.top.getExtraPropValue(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶层附属属性值
|
||||
*/
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.top.setExtraPropValue(propName, value);
|
||||
}
|
||||
|
||||
// ======= compatibles for vision ======
|
||||
getNode() {
|
||||
return this.nodes[0];
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.top;
|
||||
}
|
||||
|
||||
onValueChange() {
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
123
packages/plugin-settings-pane/src/settings/setting-field.ts
Normal file
123
packages/plugin-settings-pane/src/settings/setting-field.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { TitleContent, computed, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView } from '@ali/lowcode-globals';
|
||||
import { Transducer } from '../utils';
|
||||
import { SettingPropEntry } from './setting-entry';
|
||||
import { SettingTarget } from './setting-target';
|
||||
|
||||
export class SettingField extends SettingPropEntry implements SettingTarget {
|
||||
readonly isSettingField = true;
|
||||
readonly isRequired: boolean;
|
||||
readonly transducer: Transducer;
|
||||
extraProps: FieldExtraProps;
|
||||
|
||||
// ==== dynamic properties ====
|
||||
private _title?: TitleContent;
|
||||
get title() {
|
||||
// FIXME! intl
|
||||
return this._title || (typeof this.name === 'number' ? `项目 ${this.name}` : this.name);
|
||||
}
|
||||
private _setter?: SetterType | DynamicSetter;
|
||||
@computed get setter(): SetterType | null {
|
||||
if (!this._setter) {
|
||||
return null;
|
||||
}
|
||||
if (isDynamicSetter(this._setter)) {
|
||||
return this._setter(this);
|
||||
}
|
||||
return this._setter;
|
||||
}
|
||||
|
||||
constructor(readonly parent: SettingTarget, config: FieldConfig) {
|
||||
super(parent, config.name, config.type);
|
||||
|
||||
const { title, items, setter, extraProps, ...rest } = config;
|
||||
this._title = title;
|
||||
this._setter = setter;
|
||||
this.extraProps = {
|
||||
...rest,
|
||||
...extraProps,
|
||||
};
|
||||
this.isRequired = config.isRequired || (setter as any)?.isRequired;
|
||||
|
||||
// initial items
|
||||
if (this.type === 'group' && items) {
|
||||
this.initItems(items);
|
||||
}
|
||||
|
||||
this.transducer = new Transducer(this, { setter });
|
||||
}
|
||||
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
|
||||
get items(): Array<SettingField | CustomView> {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
private initItems(items: Array<FieldConfig | CustomView>) {
|
||||
this._items = items.map((item) => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item);
|
||||
});
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach(item => isSettingField(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
// 创建子配置项,通常用于 object/array 类型数据
|
||||
createField(config: FieldConfig): SettingField {
|
||||
return new SettingField(this, config);
|
||||
}
|
||||
|
||||
// ====== 当前属性读写 =====
|
||||
|
||||
/**
|
||||
* 判断当前属性值是否一致
|
||||
* 0 无值/多种值
|
||||
* 1 类似值,比如数组长度一样
|
||||
* 2 单一植
|
||||
*/
|
||||
get valueState(): number {
|
||||
if (this.type !== 'field') {
|
||||
return 0;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
const first = this.nodes[0].getProp(propName)!;
|
||||
let l = this.nodes.length;
|
||||
let state = 2;
|
||||
while (l-- > 1) {
|
||||
const next = this.nodes[l].getProp(propName, false);
|
||||
const s = first.compare(next);
|
||||
if (s > 1) {
|
||||
return 0;
|
||||
}
|
||||
if (s === 1) {
|
||||
state = 1;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeItems();
|
||||
}
|
||||
|
||||
// ======= compatibles for vision ======
|
||||
getHotValue(): any {
|
||||
return this.transducer.toHot(this.getValue());
|
||||
}
|
||||
|
||||
setHotValue(data: any) {
|
||||
this.setValue(this.transducer.toNative(data));
|
||||
}
|
||||
|
||||
onEffect(action: () => void): () => void {
|
||||
return this.designer.autorun(action, true);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSettingField(obj: any): obj is SettingField {
|
||||
return obj && obj.isSettingField;
|
||||
}
|
||||
70
packages/plugin-settings-pane/src/settings/setting-target.ts
Normal file
70
packages/plugin-settings-pane/src/settings/setting-target.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { ComponentMeta, Designer, Node } from '@ali/lowcode-designer';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
|
||||
export interface SettingTarget {
|
||||
|
||||
readonly nodes: Node[];
|
||||
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
|
||||
/**
|
||||
* 同样类型的节点
|
||||
*/
|
||||
readonly isSameComponent: boolean;
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
readonly isOneNode: boolean;
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
readonly isMultiNodes: boolean;
|
||||
|
||||
/**
|
||||
* 编辑器引用
|
||||
*/
|
||||
readonly editor: Editor;
|
||||
|
||||
readonly designer: Designer;
|
||||
|
||||
readonly path: Array<string| number>;
|
||||
|
||||
// 顶端对应 Props
|
||||
readonly top: SettingTarget;
|
||||
|
||||
// 父级
|
||||
readonly parent: SettingTarget;
|
||||
|
||||
|
||||
// 获取当前值
|
||||
getValue(): any;
|
||||
|
||||
// 设置当前值
|
||||
setValue(value: any): void;
|
||||
|
||||
// 取得子项
|
||||
get(propName: string | number): SettingTarget;
|
||||
|
||||
// 获取子项属性值
|
||||
getPropValue(propName: string | number): any;
|
||||
|
||||
// 设置子项属性值
|
||||
setPropValue(propName: string | number, value: any): void;
|
||||
|
||||
// 取得兄弟项
|
||||
getSibling(propName: string | number): SettingTarget | null;
|
||||
|
||||
// 取得兄弟属性值
|
||||
getSiblingValue(propName: string | number): any;
|
||||
|
||||
// 设置兄弟属性值
|
||||
setSiblingValue(propName: string | number, value: any): void;
|
||||
|
||||
// 获取顶层附属属性值
|
||||
getExtraPropValue(propName: string): any;
|
||||
|
||||
// 设置顶层附属属性值
|
||||
setExtraPropValue(propName: string, value: any): void;
|
||||
}
|
||||
146
packages/plugin-settings-pane/src/settings/settings-pane.tsx
Normal file
146
packages/plugin-settings-pane/src/settings/settings-pane.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import { Component } from 'react';
|
||||
import {
|
||||
createContent,
|
||||
CustomView,
|
||||
intl,
|
||||
shallowIntl,
|
||||
isSetterConfig,
|
||||
createSetterContent,
|
||||
observer,
|
||||
} from '@ali/lowcode-globals';
|
||||
import { Field, FieldGroup } from '../field';
|
||||
import PopupService from '../popup';
|
||||
import { SettingField, isSettingField } from './setting-field';
|
||||
import { SettingTarget } from './setting-target';
|
||||
import { SettingTopEntry } from './setting-entry';
|
||||
|
||||
@observer
|
||||
class SettingFieldView extends Component<{ field: SettingField }> {
|
||||
render() {
|
||||
const { field } = this.props;
|
||||
const { extraProps } = field;
|
||||
const { condition, defaultValue } = extraProps;
|
||||
const visible = field.isOneNode && typeof condition === 'function' ? !condition(field) : true;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
const { setter } = field;
|
||||
let setterProps: any = {};
|
||||
let setterType: any;
|
||||
if (Array.isArray(setter)) {
|
||||
setterType = 'MixedSetter';
|
||||
setterProps = {
|
||||
setters: setter,
|
||||
};
|
||||
} else if (isSetterConfig(setter)) {
|
||||
setterType = setter.componentName;
|
||||
if (setter.props) {
|
||||
setterProps = setter.props;
|
||||
if (typeof setterProps === 'function') {
|
||||
setterProps = setterProps(field);
|
||||
}
|
||||
}
|
||||
} else if (setter) {
|
||||
setterType = setter;
|
||||
}
|
||||
let value = null;
|
||||
if (field.type === 'field') {
|
||||
if (defaultValue != null && !('defaultValue' in setterProps)) {
|
||||
setterProps.defaultValue = defaultValue;
|
||||
}
|
||||
if (field.valueState > 0) {
|
||||
value = field.getValue();
|
||||
} else {
|
||||
setterProps.multiValue = true;
|
||||
if (!('placeholder' in setterProps)) {
|
||||
// FIXME! move to locale file
|
||||
setterProps.placeholder = intl({
|
||||
type: 'i18n',
|
||||
'zh-CN': '多种值',
|
||||
'en-US': 'Multiple Value',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// todo: error handling
|
||||
|
||||
return (
|
||||
<Field title={extraProps.forceInline ? null : field.title}>
|
||||
{createSetterContent(setterType, {
|
||||
...shallowIntl(setterProps),
|
||||
forceInline: extraProps.forceInline,
|
||||
key: field.id,
|
||||
// === injection
|
||||
prop: field, // for compatible vision
|
||||
field,
|
||||
// === IO
|
||||
value, // reaction point
|
||||
onChange: (value: any) => {
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
field.setValue(value);
|
||||
},
|
||||
})}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class SettingGroupView extends Component<{ field: SettingField }> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field } = this.props;
|
||||
const { extraProps } = field;
|
||||
const { condition, defaultCollapsed } = extraProps;
|
||||
const visible = field.isOneNode && typeof condition === 'function' ? !condition(field) : true;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldGroup title={field.title} defaultCollapsed={defaultCollapsed}>
|
||||
{field.items.map((item, index) => createSettingFieldView(item, field, index))}
|
||||
</FieldGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSettingFieldView(item: SettingField | CustomView, field: SettingTarget, index?: number) {
|
||||
if (isSettingField(item)) {
|
||||
if (item.isGroup) {
|
||||
return <SettingGroupView field={item} key={item.id} />;
|
||||
} else {
|
||||
return <SettingFieldView field={item} key={item.id} />;
|
||||
}
|
||||
} else {
|
||||
return createContent(item, { key: index, field });
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class SettingsPane extends Component<{ target: SettingTopEntry | SettingField }> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { target } = this.props;
|
||||
const items = target.items
|
||||
return (
|
||||
<div className="lc-settings-pane">
|
||||
{/* todo: add head for single use */}
|
||||
<PopupService>
|
||||
<div className="lc-settings-content">
|
||||
{items.map((item, index) => createSettingFieldView(item, target, index))}
|
||||
</div>
|
||||
</PopupService>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { TransformedComponentMetadata, FieldConfig } from '@ali/lowcode-globals';
|
||||
import { SettingField } from '../main';
|
||||
import { SettingField } from '../settings/setting-field';
|
||||
|
||||
export default function(metadata: TransformedComponentMetadata): TransformedComponentMetadata {
|
||||
const { componentName, configure = {} } = metadata;
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"packages": {
|
||||
"moment": {
|
||||
"packages": [
|
||||
{
|
||||
"package": "moment",
|
||||
"urls": ["https://g.alicdn.com/mylib/moment/2.24.0/min/moment.min.js"],
|
||||
"library": "moment"
|
||||
},
|
||||
"@alifd/next": {
|
||||
{
|
||||
"title": "fusion组件库",
|
||||
"package": "@alifd/next",
|
||||
"version": "1.19.18",
|
||||
"urls": ["https://unpkg.antfin-inc.com/@alife/next@1.19.18/dist/next.js", "https://unpkg.antfin-inc.com/@alife/next@1.19.18/dist/next.css"],
|
||||
"library": "Next"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"Page": {
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"componentName": "Page",
|
||||
"title": "页面",
|
||||
"configure": {
|
||||
@ -39,7 +39,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Div": {
|
||||
{
|
||||
"componentName": "Div",
|
||||
"title": "容器",
|
||||
"configure": {
|
||||
@ -48,7 +48,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Button": {
|
||||
{
|
||||
"componentName": "Button",
|
||||
"title": "按钮",
|
||||
"devMode": "proCode",
|
||||
@ -147,7 +147,7 @@
|
||||
"propType": "node"
|
||||
}]
|
||||
},
|
||||
"Button.Group": {
|
||||
{
|
||||
"componentName": "Button.Group",
|
||||
"title": "按钮组",
|
||||
"devMode": "proCode",
|
||||
@ -186,7 +186,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Input": {
|
||||
{
|
||||
"componentName": "Input",
|
||||
"title": "输入框",
|
||||
"devMode": "proCode",
|
||||
@ -300,7 +300,7 @@
|
||||
"description": "预览态模式下渲染的内容\n@param {number} value 评分值"
|
||||
}]
|
||||
},
|
||||
"Form": {
|
||||
{
|
||||
"componentName": "Form",
|
||||
"title": "表单容器",
|
||||
"devMode": "proCode",
|
||||
@ -423,7 +423,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Form.Item": {
|
||||
{
|
||||
"componentName": "Form.Item",
|
||||
"title": "表单项",
|
||||
"devMode": "proCode",
|
||||
@ -677,7 +677,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"NumberPicker": {
|
||||
{
|
||||
"componentName": "NumberPicker",
|
||||
"title": "数字输入",
|
||||
"devMode": "proCode",
|
||||
@ -829,7 +829,7 @@
|
||||
"description": "预设屏幕宽度"
|
||||
}]
|
||||
},
|
||||
"Select": {
|
||||
{
|
||||
"componentName": "Select",
|
||||
"title": "下拉",
|
||||
"devMode": "proCode",
|
||||
@ -1450,7 +1450,7 @@
|
||||
}]
|
||||
}
|
||||
},
|
||||
"Select.Option": {
|
||||
{
|
||||
"componentName": "Select.Option",
|
||||
"title": "选择项",
|
||||
"devMode": "proCode",
|
||||
@ -1485,7 +1485,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
"componentList": [{
|
||||
"title": "基础",
|
||||
"icon": "",
|
||||
|
||||
@ -16,8 +16,7 @@ async function load() {
|
||||
|
||||
const externals = ['react', 'react-dom', 'prop-types', 'react-router', 'react-router-dom', '@ali/recore'];
|
||||
async function loadAssets() {
|
||||
const assets = await editor.utils.get('./legao-assets.json');
|
||||
// Trunk.setPackages(assets.packages);
|
||||
const assets = await editor.utils.get('./assets.json');
|
||||
|
||||
if (assets.packages) {
|
||||
assets.packages.forEach((item: any) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user