Merge branch 'feat/entry-setting-field' into 'release/0.9.0'

Feat/entry setting field

支持 entry 模式

See merge request !907961
This commit is contained in:
康为 2020-07-26 14:06:04 +08:00
commit d95da04eff
9 changed files with 256 additions and 126 deletions

View File

@ -224,6 +224,13 @@ export class SettingTopEntry implements SettingEntry {
getPage() { getPage() {
return this.first.document; return this.first.document;
} }
/**
* @deprecated
*/
getNode() {
return this.nodes[0];
}
} }
interface Purgeable { interface Purgeable {

View File

@ -66,6 +66,7 @@ export class Prop implements IPropParent {
const type = this._type; const type = this._type;
if (type === 'unset') { if (type === 'unset') {
// return UNSET; @康为 之后 review 下这块改造
return undefined; return undefined;
} }
@ -98,6 +99,10 @@ export class Prop implements IPropParent {
const maps: any = {}; const maps: any = {};
this.items!.forEach((prop, key) => { this.items!.forEach((prop, key) => {
const v = prop.export(stage); const v = prop.export(stage);
// if (v !== UNSET) {
// maps[prop.key == null ? key : prop.key] = v;
// }
// @康为 之后 review 下这块改造
maps[prop.key == null ? key : prop.key] = v; maps[prop.key == null ? key : prop.key] = v;
}); });
return maps; return maps;

View File

@ -1,5 +1,5 @@
import { ComponentType, ReactElement, isValidElement, ComponentClass } from 'react'; import { ComponentType, ReactElement, isValidElement, ComponentClass } from 'react';
import { isPlainObject } from '@ali/lowcode-utils'; import { isPlainObject, uniqueId } from '@ali/lowcode-utils';
import { isI18nData, SettingTarget, InitialItem, FilterItem, isJSSlot, ProjectSchema, AutorunItem } from '@ali/lowcode-types'; import { isI18nData, SettingTarget, InitialItem, FilterItem, isJSSlot, ProjectSchema, AutorunItem } from '@ali/lowcode-types';
import { untracked } from '@ali/lowcode-editor-core'; import { untracked } from '@ali/lowcode-editor-core';
import { editor, designer } from '../editor'; import { editor, designer } from '../editor';
@ -218,7 +218,7 @@ export function upgradePropConfig(config: OldPropConfig, collector: ConfigCollec
}; };
const newConfig: any = { const newConfig: any = {
type: type === 'group' ? 'group' : 'field', type: type === 'group' ? 'group' : 'field',
name, name: type === 'group' && !name ? uniqueId('group') : name,
title, title,
extraProps, extraProps,
}; };

View File

@ -31,6 +31,10 @@ export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget =
} }
add(config: T | C): T { add(config: T | C): T {
const item = this.container.get(config.name);
if (item) {
return item;
}
return this.container.add(config); return this.container.add(config);
} }

View File

@ -2,19 +2,17 @@ import { Component } from 'react';
import { isObject } from 'lodash'; import { isObject } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon } from '@alifd/next'; import { Icon } from '@alifd/next';
import { Title, Tip } from '@ali/lowcode-editor-core'; import { Title } from '@ali/lowcode-editor-core';
import { TitleContent } from '@ali/lowcode-types'; import { TitleContent } from '@ali/lowcode-types';
import { PopupPipe, PopupContext } from '../popup'; import { PopupPipe, PopupContext } from '../popup';
import { intlNode } from '../../locale';
import './index.less'; import './index.less';
import { IconClear } from '../../icons/clear';
import InlineTip from './inlinetip'; import InlineTip from './inlinetip';
export interface FieldProps { export interface FieldProps {
className?: string; className?: string;
meta?: { package: string; componentName: string } | string; meta?: { package: string; componentName: string } | string;
title?: TitleContent | null; title?: TitleContent | null;
defaultDisplay?: 'accordion' | 'inline' | 'block'; defaultDisplay?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry';
collapsed?: boolean; collapsed?: boolean;
valueState?: number; valueState?: number;
name?: string; name?: string;
@ -134,14 +132,18 @@ export class Field extends Component<FieldProps> {
})} })}
id={id} id={id}
> >
<div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}> {
<div className="lc-field-title"> display !== 'plain' && (
{createValueState(valueState, this.handleClear)} <div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}>
<Title title={title || ''} /> <div className="lc-field-title">
<InlineTip position="top">{tipContent}</InlineTip> {createValueState(valueState, this.handleClear)}
</div> <Title title={title || ''} />
{isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />} <InlineTip position="top">{tipContent}</InlineTip>
</div> </div>
{isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />}
</div>
)
}
<div key="body" ref={(shell) => (this.body = shell)} className="lc-field-body"> <div key="body" ref={(shell) => (this.body = shell)} className="lc-field-body">
{children} {children}
</div> </div>
@ -250,21 +252,17 @@ export interface EntryFieldProps extends FieldProps {
export class EntryField extends Component<EntryFieldProps> { export class EntryField extends Component<EntryFieldProps> {
render() { render() {
const { stageName, title, className } = this.props; const { title, className, stageName } = this.props;
const classNameList = classNames('engine-setting-field', 'engine-entry-field', className); const classNameList = classNames('lc-field', 'lc-entry-field', className);
const fieldProps: any = {};
if (stageName) {
// 为 stage 切换奠定基础
fieldProps['data-stage-target'] = stageName;
}
return ( return (
<div className={classNameList} {...fieldProps}> <div className={classNameList}>
<div className="lc-field-title"> <div className="lc-field-head" data-stage-target={stageName}>
<Title title={title || ''} /> <div className="lc-field-title">
<Title title={title || ''} />
</div>
<Icon className="lc-field-icon" type="arrow-right" size="xs" />
</div> </div>
<Icon className="lc-field-icon" type="arrow-left" size="xs" />
</div> </div>
); );
} }

View File

@ -1,6 +1,10 @@
@x-gap: 12px; @x-gap: 12px;
@y-gap: 8px; @y-gap: 8px;
.lc-settings-content > .lc-field:first-child > .lc-field-head{
border-top: none !important;
}
.lc-field { .lc-field {
.lc-field-head { .lc-field-head {
display: flex; display: flex;
@ -68,17 +72,7 @@
&.lc-inline-field { &.lc-inline-field {
display: flex; display: flex;
align-items: center; align-items: center;
// for top-level style margin: 12px;
padding: 16px;
&:first-child{
padding-top: 16px;
}
&:last-child{
padding-bottom: 16px;
}
&+.lc-inline-field{
padding-top: 0;
}
> .lc-field-head { > .lc-field-head {
width: 70px; width: 70px;
@ -96,11 +90,11 @@
} }
} }
&.lc-block-field, &.lc-accordion-field { &.lc-block-field, &.lc-accordion-field, &.lc-entry-field {
display: block; display: block;
&:first-child { &:first-child {
> .lc-field-head { > .lc-field-head {
border-top: none; // border-top: none;
} }
} }
> .lc-field-head { > .lc-field-head {
@ -112,7 +106,7 @@
border-top: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); border-top: 1px solid var(--color-line-normal,rgba(31,56,88,.1));
border-bottom: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); border-bottom: 1px solid var(--color-line-normal,rgba(31,56,88,.1));
color: var(--color-title); color: var(--color-title);
padding: 0 16px; padding: 0 12px;
user-select: none; user-select: none;
> .lc-field-icon { > .lc-field-icon {
@ -121,18 +115,30 @@
} }
> .lc-field-body { > .lc-field-body {
// padding: @y-gap @x-gap/2;
padding: 12px; padding: 12px;
.lc-inline-field{
margin-bottom: 12px; .lc-inline-field {
&:last-child{ margin: 12px 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
} }
+ .lc-inline-field { // + .lc-inline-field {
border-top: 1px solid var(--color-line-normal); // border-top: 1px solid var(--color-line-normal);
// }
}
&.lc-entry-field {
margin-bottom: 6px;
> .lc-field-head {
cursor: pointer;
} }
} }
@ -154,6 +160,10 @@
&.lc-accordion-field { &.lc-accordion-field {
position: relative; position: relative;
> .lc-field-head {
cursor: pointer;
}
&.lc-field-is-collapsed { &.lc-field-is-collapsed {
margin-bottom: 6px; margin-bottom: 6px;
} }
@ -179,37 +189,35 @@
// 2rd level reset // 2rd level reset
.lc-field-body { .lc-field-body {
.lc-inline-field { // .lc-inline-field {
// padding: @y-gap @x-gap/2 0 @x-gap/2; // &:first-child {
padding: 0; // padding-top: 0;
&:first-child { // }
padding-top: 0; // + .lc-accordion-field, +.lc-block-field {
} // margin-top: @y-gap;
+ .lc-accordion-field, +.lc-block-field { // }
margin-top: @y-gap; // }
}
}
.lc-field { // .lc-field {
border-top: none !important; // border-top: none !important;
} // }
.lc-accordion-field, .lc-block-field { // .lc-accordion-field, .lc-block-field {
> .lc-field-head { // > .lc-field-head {
padding-left: @x-gap; // padding-left: @x-gap;
background: var(--color-block-background-light); // background: var(--color-block-background-light);
border-bottom: none; // border-bottom: none;
border-top: none; // border-top: none;
> .lc-field-icon { // > .lc-field-icon {
// margin-right: @x-gap/2; // // margin-right: @x-gap/2;
margin-right: 0; // margin-right: 0;
} // }
} // }
> .lc-field-body { // > .lc-field-body {
padding: 8px; // padding: 8px;
} // }
} // }
// 3rd level field title width should short // 3rd level field title width should short
// .lc-field-body .lc-inline-field { // .lc-field-body .lc-inline-field {
@ -220,8 +228,8 @@
// } // }
// } // }
// } // }
>.lc-block-setter { // >.lc-block-setter {
flex: 1; // flex: 1;
} // }
} }
} }

View File

@ -1,20 +1,23 @@
import { Component, MouseEvent } from 'react'; import { Component, MouseEvent, Fragment } from 'react';
import { shallowIntl, createSetterContent, observer } from '@ali/lowcode-editor-core'; import { shallowIntl, createSetterContent, observer, obx, Title } from '@ali/lowcode-editor-core';
import { createContent } from '@ali/lowcode-utils'; import { createContent } from '@ali/lowcode-utils';
import { Field, createField } from '../field'; import { createField } from '../field';
import PopupService, { PopupPipe } from '../popup';
import { SkeletonContext } from '../../context'; import { SkeletonContext } from '../../context';
import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer'; import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer';
import { Icon } from '@alifd/next';
import { isSetterConfig, CustomView } from '@ali/lowcode-types'; import { isSetterConfig, CustomView } from '@ali/lowcode-types';
import { intl } from '../../locale'; import { intl } from '../../locale';
import { Skeleton } from 'editor-skeleton/src/skeleton'; import { Skeleton } from '../../skeleton';
import { Stage } from '../../widget/stage';
@observer @observer
class SettingFieldView extends Component<{ field: SettingField }> { class SettingFieldView extends Component<{ field: SettingField }> {
static contextType = SkeletonContext;
render() { render() {
const { field } = this.props; const { field } = this.props;
const { extraProps } = field; const { extraProps } = field;
const { condition, defaultValue } = extraProps; const { condition, defaultValue, display } = extraProps;
const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true;
if (!visible) { if (!visible) {
return null; return null;
@ -59,7 +62,26 @@ class SettingFieldView extends Component<{ field: SettingField }> {
value = field.getValue(); value = field.getValue();
} }
const skeleton = this.context as Skeleton;
const { stages } = skeleton;
// todo: error handling // todo: error handling
let stageName;
if (display === 'entry') {
const stage = stages.add({
type: 'Widget',
name: field.getNode().id + '_' + field.name.toString(),
content: (
<Fragment>
{field.items.map((item, index) => createSettingFieldView(item, field, index))}
</Fragment>
),
props: {
title: field.title,
},
});
stageName = stage.name;
}
return createField( return createField(
{ {
@ -69,9 +91,12 @@ class SettingFieldView extends Component<{ field: SettingField }> {
valueState: field.isRequired ? 10 : field.valueState, valueState: field.isRequired ? 10 : field.valueState,
onExpandChange: (expandState) => field.setExpanded(expandState), onExpandChange: (expandState) => field.setExpanded(expandState),
onClear: () => field.clearValue(), onClear: () => field.clearValue(),
// field: field,
// stages,
stageName,
...extraProps, ...extraProps,
}, },
createSetterContent(setterType, { !stageName && createSetterContent(setterType, {
...shallowIntl(setterProps), ...shallowIntl(setterProps),
forceInline: extraProps.forceInline, forceInline: extraProps.forceInline,
key: field.id, key: field.id,
@ -104,6 +129,8 @@ class SettingFieldView extends Component<{ field: SettingField }> {
@observer @observer
class SettingGroupView extends Component<{ field: SettingField }> { class SettingGroupView extends Component<{ field: SettingField }> {
static contextType = SkeletonContext;
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
@ -111,36 +138,54 @@ class SettingGroupView extends Component<{ field: SettingField }> {
render() { render() {
const { field } = this.props; const { field } = this.props;
const { extraProps } = field; const { extraProps } = field;
const { condition } = extraProps; const { condition, display } = extraProps;
const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true;
if (!visible) { if (!visible) {
return null; return null;
} }
const skeleton = this.context as Skeleton;
const { stages } = skeleton;
let stageName;
if (display === 'entry') {
const stage = stages.add({
type: 'Widget',
name: field.getNode().id + '_' + field.name.toString(),
content: (
<Fragment>
{field.items.map((item, index) => createSettingFieldView(item, field, index))}
</Fragment>
),
props: {
title: field.title,
},
});
stageName = stage.name;
}
// todo: split collapsed state | field.items for optimize // todo: split collapsed state | field.items for optimize
return ( return createField({
<Field meta: field.componentMeta?.npm || field.componentMeta?.componentName || '',
defaultDisplay="accordion" title: field.title,
meta={field?.componentMeta?.npm || field?.componentMeta?.componentName || ''} collapsed: !field.expanded,
title={field.title} onExpandChange: (expandState) => field.setExpanded(expandState),
collapsed={!field.expanded} // field: field,
onExpandChange={(expandState) => { // stages,
field.setExpanded(expandState); stageName,
}} },
> field.items.map((item, index) => createSettingFieldView(item, field, index)),
{field.items.map((item, index) => createSettingFieldView(item, field, index))} display);
</Field>
);
} }
} }
export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) { export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) {
if (isSettingField(item)) { if (isSettingField(item)) {
if (item.isGroup) { if (item.isGroup) {
return <SettingGroupView field={item} key={item.id} />; return <SettingGroupView field={item} key={item.id}/>;
} else { } else {
return <SettingFieldView field={item} key={item.id} />; return <SettingFieldView field={item} key={item.id}/>;
} }
} else { } else {
return createContent(item, { key: index, field }); return createContent(item, { key: index, field });
@ -150,18 +195,15 @@ export function createSettingFieldView(item: SettingField | CustomView, field: S
@observer @observer
export class SettingsPane extends Component<{ target: SettingTopEntry | SettingField }> { export class SettingsPane extends Component<{ target: SettingTopEntry | SettingField }> {
static contextType = SkeletonContext; static contextType = SkeletonContext;
@obx
private currentStage?: Stage;
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
private popupPipe = new PopupPipe();
private pipe = this.popupPipe.create();
private handleClick = (e: MouseEvent) => { private handleClick = (e: MouseEvent) => {
// compatiable vision stageBox
// TODO: optimize these codes
const pane = e.currentTarget as HTMLDivElement; const pane = e.currentTarget as HTMLDivElement;
let entry: any;
function getTarget(node: any): any { function getTarget(node: any): any {
if (!pane.contains(node) || (node.nodeName === 'A' && node.getAttribute('href'))) { if (!pane.contains(node) || (node.nodeName === 'A' && node.getAttribute('href'))) {
return null; return null;
@ -169,7 +211,6 @@ export class SettingsPane extends Component<{ target: SettingTopEntry | SettingF
const target = node.dataset ? node.dataset.stageTarget : null; const target = node.dataset ? node.dataset.stageTarget : null;
if (target) { if (target) {
entry = node;
return target; return target;
} }
return getTarget(node.parentNode); return getTarget(node.parentNode);
@ -185,22 +226,40 @@ export class SettingsPane extends Component<{ target: SettingTopEntry | SettingF
} }
const stage = skeleton.stages.container.get(target); const stage = skeleton.stages.container.get(target);
if (stage) { if (stage) {
this.pipe.send(stage.content, stage.title); if (this.currentStage) {
this.pipe.show(entry); stage.setPrevious(this.currentStage);
}
this.currentStage = stage;
} }
}; };
private popStage() {
this.currentStage = this.currentStage?.getPrevious();
}
render() { render() {
const { target } = this.props; let { target } = this.props;
const items = target.items;
return ( return (
<div className="lc-settings-pane" onClick={this.handleClick}> <div className="lc-settings-pane" onClick={this.handleClick}>
{/* todo: add head for single use */} {
<PopupService popupPipe={this.popupPipe}> this.currentStage && (
<div className="lc-settings-content"> <div className="lc-setting-stage-back">
{items.map((item, index) => createSettingFieldView(item, target, index))} <Icon
</div> className="lc-setting-stage-back-icon"
</PopupService> type="arrow-left"
size="xs"
onClick={this.popStage.bind(this)}
/>
<Title title={this.currentStage.title}/>
</div>
)
}
<div className="lc-settings-content">
{
this.currentStage ? this.currentStage.content : target.items.map((item, index) => createSettingFieldView(item, target, index))
}
</div>
</div> </div>
); );
} }

View File

@ -3,6 +3,42 @@
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
.lc-settings-content {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
overflow-y: auto;
}
.lc-setting-stage-back + .lc-settings-content {
top: 38px;
}
.lc-setting-stage-back {
height: 32px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
background: var(--color-block-background-shallow, rgba(31,56,88,.06));
color: var(--color-title);
padding: 0 16px;
user-select: none;
position: relative;
margin-bottom: 4px;
position: absolute;
.lc-setting-stage-back-icon {
position: absolute;
left: 8px;
top: 8px;
color: #8F9BB3;
cursor: pointer;
}
}
.lc-settings-notice { .lc-settings-notice {
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;

View File

@ -212,6 +212,9 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
}, { }, {
componentName: 'VariableSetter' componentName: 'VariableSetter'
}], }],
extraProps: {
display: 'block',
},
}); });
} }
if (supports.loop !== false) { if (supports.loop !== false) {
@ -252,18 +255,28 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
} }
}, },
}, },
{
name: 'key',
title: '循环 Key',
setter: [{
componentName: 'StringSetter',
}, {
componentName: 'VariableSetter'
}],
},
], ],
extraProps: {
display: 'accordion',
},
}) })
} }
advanceGroup.push({
name: 'key',
title: {
label: '渲染唯一标识key',
tip: '搭配「条件渲染」或「循环渲染」时使用,和 react 组件中的 key 原理相同,点击查看帮助',
docUrl: 'https://yuque.antfin-inc.com/legao/help3.0/ca5in7',
},
setter: [{
componentName: 'StringSetter',
}, {
componentName: 'VariableSetter'
}],
extraProps: {
display: 'block',
},
},)
} }
if (advanceGroup.length > 0) { if (advanceGroup.length > 0) {
combined.push({ combined.push({