optimize settings pane

This commit is contained in:
kangwei 2020-04-21 00:26:21 +08:00
parent 4da47f2300
commit 92ea6505c1
16 changed files with 983 additions and 945 deletions

View File

@ -24,7 +24,6 @@ export class Field extends Component<FieldProps> {
export interface FieldGroupProps extends FieldProps {
defaultCollapsed?: boolean;
// gap?: number;
onExpandChange?: (collapsed: boolean) => void;
}

View File

@ -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 };

View File

@ -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();
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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';

View File

@ -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>
);
}
}

View 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();
}

View 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();
}
}

View 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 () => {};
}
}

View 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;
}

View 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;
}

View 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>
);
}
}

View File

@ -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;

View File

@ -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": "",

View File

@ -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) => {