diff --git a/packages/designer/src/designer/setting/setting-top-entry.ts b/packages/designer/src/designer/setting/setting-top-entry.ts index 5c276afb2..c7931a62c 100644 --- a/packages/designer/src/designer/setting/setting-top-entry.ts +++ b/packages/designer/src/designer/setting/setting-top-entry.ts @@ -224,6 +224,13 @@ export class SettingTopEntry implements SettingEntry { getPage() { return this.first.document; } + + /** + * @deprecated + */ + getNode() { + return this.nodes[0]; + } } interface Purgeable { diff --git a/packages/designer/src/document/node/node-children.ts b/packages/designer/src/document/node/node-children.ts index aeb306a9b..ed6c16f50 100644 --- a/packages/designer/src/document/node/node-children.ts +++ b/packages/designer/src/document/node/node-children.ts @@ -132,6 +132,7 @@ export class NodeChildren { } this.emitter.emit('change'); + this.reportModified(node, this.owner, { type: 'insert' }); // check condition group if (node.conditionGroup) { @@ -288,24 +289,49 @@ export class NodeChildren { return; } this.purged = true; - this.children.forEach(child => child.purge()); + this.children.forEach((child) => child.purge()); } get [Symbol.toStringTag]() { // 保证向前兼容性 - return "Array"; + return 'Array'; + } + + /** + * @deprecated + * 为了兼容vision体系存量api + */ + getChildrenArray() { + return this.children; } private reportModified(node: Node, owner: Node, options = {}) { - if (!node) { return; } - if (node.isRoot()) { return; } + if (!node) { + return; + } + if (node.isRoot()) { + return; + } const callbacks = owner.componentMeta.getMetadata().experimental?.callbacks; - if (callbacks?.onSubtreeModified) { - callbacks?.onSubtreeModified.call(node, owner, options); + if (callbacks?.onSubtreeModified && options?.type !== 'insert') { + try { + // 此处传入的 owner节点需要对getChildren进行处理,兼容老的数据结构 + callbacks?.onSubtreeModified.call(node, owner.getVisionCapabledNode(), options); + } catch (e) { + console.error('error when excute experimental.callbacks.onNodeAdd', e); + } + } + + if (callbacks?.onNodeAdd && options?.type === 'insert') { + try { + callbacks?.onNodeAdd.call(owner, node.getVisionCapabledNode(), owner); + } catch (e) { + console.error('error when excute experimental.callbacks.onNodeAdd', e); + } } if (owner.parent && !owner.parent.isRoot()) { - this.reportModified(node, owner.parent, options); + this.reportModified(node, owner.parent, options); } } } diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index b6f0ed4c3..c135fcb9b 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -818,6 +818,33 @@ export class Node { toString() { return this.id; } + + /** + * 慎用,可能有极端未知后果 + * @deprecated + */ + getVisionCapabledNode() { + // 判断是否已经是 nodeForVision + if (!this.isVisionNode) { + const children = this.getChildren(); + this.getChildren = () => { + return children?.getChildrenArray() || []; + }; + this.getProps = () => { + const props = this.props.export(); + props.getPropValue = (key) => { + return this.props.getPropValue(key); + }; + props.getNode = () => { + return this; + }; + return props; + }; + this.isVisionNode = true; + } + return this; + } + } export interface ParentalNode extends Node { diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index 8526e81fc..40e8a502b 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -66,6 +66,7 @@ export class Prop implements IPropParent { const type = this._type; if (type === 'unset') { + // return UNSET; @康为 之后 review 下这块改造 return undefined; } @@ -98,6 +99,10 @@ export class Prop implements IPropParent { const maps: any = {}; this.items!.forEach((prop, key) => { 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; }); return maps; diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts index 1da17eda7..8393dad75 100644 --- a/packages/designer/src/document/node/props/props.ts +++ b/packages/designer/src/document/node/props/props.ts @@ -329,4 +329,13 @@ export class Props implements IPropParent { setPropValue(path: string, value: any) { this.getProp(path, true)!.setValue(value); } + + /** + * @deprecated + * 兼容vision体系 + */ + getNode() { + const nodeForVision = this.owner?.getVisionCapabledNode(); + return nodeForVision; + } } diff --git a/packages/editor-preset-vision/src/bundle/upgrade-metadata.ts b/packages/editor-preset-vision/src/bundle/upgrade-metadata.ts index cf7ef1ff9..d46a06929 100644 --- a/packages/editor-preset-vision/src/bundle/upgrade-metadata.ts +++ b/packages/editor-preset-vision/src/bundle/upgrade-metadata.ts @@ -1,5 +1,5 @@ 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 { untracked } from '@ali/lowcode-editor-core'; import { editor, designer } from '../editor'; @@ -218,7 +218,7 @@ export function upgradePropConfig(config: OldPropConfig, collector: ConfigCollec }; const newConfig: any = { type: type === 'group' ? 'group' : 'field', - name, + name: type === 'group' && !name ? uniqueId('group') : name, title, extraProps, }; diff --git a/packages/editor-skeleton/src/area.ts b/packages/editor-skeleton/src/area.ts index 403a93dcf..d0f4b0f4f 100644 --- a/packages/editor-skeleton/src/area.ts +++ b/packages/editor-skeleton/src/area.ts @@ -31,6 +31,10 @@ export default class Area { })} id={id} > -
-
- {createValueState(valueState, this.handleClear)} - - <InlineTip position="top">{tipContent}</InlineTip> - </div> - {isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />} - </div> + { + display !== 'plain' && ( + <div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}> + <div className="lc-field-title"> + {createValueState(valueState, this.handleClear)} + <Title title={title || ''} /> + <InlineTip position="top">{tipContent}</InlineTip> + </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"> {children} </div> @@ -250,21 +252,17 @@ export interface EntryFieldProps extends FieldProps { export class EntryField extends Component<EntryFieldProps> { render() { - const { stageName, title, className } = this.props; - const classNameList = classNames('engine-setting-field', 'engine-entry-field', className); - const fieldProps: any = {}; - - if (stageName) { - // 为 stage 切换奠定基础 - fieldProps['data-stage-target'] = stageName; - } + const { title, className, stageName } = this.props; + const classNameList = classNames('lc-field', 'lc-entry-field', className); return ( - <div className={classNameList} {...fieldProps}> - <div className="lc-field-title"> - <Title title={title || ''} /> + <div className={classNameList}> + <div className="lc-field-head" data-stage-target={stageName}> + <div className="lc-field-title"> + <Title title={title || ''} /> + </div> + <Icon className="lc-field-icon" type="arrow-right" size="xs" /> </div> - <Icon className="lc-field-icon" type="arrow-left" size="xs" /> </div> ); } diff --git a/packages/editor-skeleton/src/components/field/index.less b/packages/editor-skeleton/src/components/field/index.less index 6e4cc3560..cc5f0139f 100644 --- a/packages/editor-skeleton/src/components/field/index.less +++ b/packages/editor-skeleton/src/components/field/index.less @@ -1,6 +1,10 @@ @x-gap: 12px; @y-gap: 8px; +.lc-settings-content > .lc-field:first-child > .lc-field-head{ + border-top: none !important; +} + .lc-field { .lc-field-head { display: flex; @@ -68,17 +72,7 @@ &.lc-inline-field { display: flex; align-items: center; - // for top-level style - padding: 16px; - &:first-child{ - padding-top: 16px; - } - &:last-child{ - padding-bottom: 16px; - } - &+.lc-inline-field{ - padding-top: 0; - } + margin: 12px; > .lc-field-head { width: 70px; @@ -96,11 +90,11 @@ } } - &.lc-block-field, &.lc-accordion-field { + &.lc-block-field, &.lc-accordion-field, &.lc-entry-field { display: block; &:first-child { > .lc-field-head { - border-top: none; + // border-top: none; } } > .lc-field-head { @@ -112,7 +106,7 @@ 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)); color: var(--color-title); - padding: 0 16px; + padding: 0 12px; user-select: none; > .lc-field-icon { @@ -121,18 +115,30 @@ } > .lc-field-body { - // padding: @y-gap @x-gap/2; padding: 12px; - .lc-inline-field{ - margin-bottom: 12px; - &:last-child{ + + .lc-inline-field { + margin: 12px 0; + + &:first-child { + margin-top: 0; + } + &:last-child { margin-bottom: 0; } } } - + .lc-inline-field { - border-top: 1px solid var(--color-line-normal); + // + .lc-inline-field { + // border-top: 1px solid var(--color-line-normal); + // } + } + + &.lc-entry-field { + margin-bottom: 6px; + + > .lc-field-head { + cursor: pointer; } } @@ -153,6 +159,10 @@ &.lc-accordion-field { position: relative; + + > .lc-field-head { + cursor: pointer; + } &.lc-field-is-collapsed { margin-bottom: 6px; @@ -179,37 +189,35 @@ // 2rd level reset .lc-field-body { - .lc-inline-field { - // padding: @y-gap @x-gap/2 0 @x-gap/2; - padding: 0; - &:first-child { - padding-top: 0; - } - + .lc-accordion-field, +.lc-block-field { - margin-top: @y-gap; - } - } + // .lc-inline-field { + // &:first-child { + // padding-top: 0; + // } + // + .lc-accordion-field, +.lc-block-field { + // margin-top: @y-gap; + // } + // } - .lc-field { - border-top: none !important; - } + // .lc-field { + // border-top: none !important; + // } - .lc-accordion-field, .lc-block-field { - > .lc-field-head { - padding-left: @x-gap; - background: var(--color-block-background-light); - border-bottom: none; - border-top: none; - > .lc-field-icon { - // margin-right: @x-gap/2; - margin-right: 0; - } - } + // .lc-accordion-field, .lc-block-field { + // > .lc-field-head { + // padding-left: @x-gap; + // background: var(--color-block-background-light); + // border-bottom: none; + // border-top: none; + // > .lc-field-icon { + // // margin-right: @x-gap/2; + // margin-right: 0; + // } + // } - > .lc-field-body { - padding: 8px; - } - } + // > .lc-field-body { + // padding: 8px; + // } + // } // 3rd level field title width should short // .lc-field-body .lc-inline-field { @@ -220,8 +228,8 @@ // } // } // } - >.lc-block-setter { - flex: 1; - } + // >.lc-block-setter { + // flex: 1; + // } } } diff --git a/packages/editor-skeleton/src/components/settings/settings-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-pane.tsx index 5b75bddfa..76ea9c282 100644 --- a/packages/editor-skeleton/src/components/settings/settings-pane.tsx +++ b/packages/editor-skeleton/src/components/settings/settings-pane.tsx @@ -1,20 +1,23 @@ -import { Component, MouseEvent } from 'react'; -import { shallowIntl, createSetterContent, observer } from '@ali/lowcode-editor-core'; +import { Component, MouseEvent, Fragment } from 'react'; +import { shallowIntl, createSetterContent, observer, obx, Title } from '@ali/lowcode-editor-core'; import { createContent } from '@ali/lowcode-utils'; -import { Field, createField } from '../field'; -import PopupService, { PopupPipe } from '../popup'; +import { createField } from '../field'; import { SkeletonContext } from '../../context'; import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer'; +import { Icon } from '@alifd/next'; import { isSetterConfig, CustomView } from '@ali/lowcode-types'; import { intl } from '../../locale'; -import { Skeleton } from 'editor-skeleton/src/skeleton'; +import { Skeleton } from '../../skeleton'; +import { Stage } from '../../widget/stage'; @observer class SettingFieldView extends Component<{ field: SettingField }> { + static contextType = SkeletonContext; + render() { const { field } = this.props; const { extraProps } = field; - const { condition, defaultValue } = extraProps; + const { condition, defaultValue, display } = extraProps; const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; if (!visible) { return null; @@ -59,7 +62,26 @@ class SettingFieldView extends Component<{ field: SettingField }> { value = field.getValue(); } + const skeleton = this.context as Skeleton; + const { stages } = skeleton; + // 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( { @@ -69,9 +91,12 @@ class SettingFieldView extends Component<{ field: SettingField }> { valueState: field.isRequired ? 10 : field.valueState, onExpandChange: (expandState) => field.setExpanded(expandState), onClear: () => field.clearValue(), + // field: field, + // stages, + stageName, ...extraProps, }, - createSetterContent(setterType, { + !stageName && createSetterContent(setterType, { ...shallowIntl(setterProps), forceInline: extraProps.forceInline, key: field.id, @@ -104,6 +129,8 @@ class SettingFieldView extends Component<{ field: SettingField }> { @observer class SettingGroupView extends Component<{ field: SettingField }> { + static contextType = SkeletonContext; + shouldComponentUpdate() { return false; } @@ -111,36 +138,54 @@ class SettingGroupView extends Component<{ field: SettingField }> { render() { const { field } = this.props; const { extraProps } = field; - const { condition } = extraProps; + const { condition, display } = extraProps; const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; if (!visible) { 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 - return ( - <Field - defaultDisplay="accordion" - meta={field?.componentMeta?.npm || field?.componentMeta?.componentName || ''} - title={field.title} - collapsed={!field.expanded} - onExpandChange={(expandState) => { - field.setExpanded(expandState); - }} - > - {field.items.map((item, index) => createSettingFieldView(item, field, index))} - </Field> - ); + return createField({ + meta: field.componentMeta?.npm || field.componentMeta?.componentName || '', + title: field.title, + collapsed: !field.expanded, + onExpandChange: (expandState) => field.setExpanded(expandState), + // field: field, + // stages, + stageName, + }, + field.items.map((item, index) => createSettingFieldView(item, field, index)), + display); } } export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) { if (isSettingField(item)) { if (item.isGroup) { - return <SettingGroupView field={item} key={item.id} />; + return <SettingGroupView field={item} key={item.id}/>; } else { - return <SettingFieldView field={item} key={item.id} />; + return <SettingFieldView field={item} key={item.id}/>; } } else { return createContent(item, { key: index, field }); @@ -150,18 +195,15 @@ export function createSettingFieldView(item: SettingField | CustomView, field: S @observer export class SettingsPane extends Component<{ target: SettingTopEntry | SettingField }> { static contextType = SkeletonContext; + @obx + private currentStage?: Stage; + shouldComponentUpdate() { return false; } - private popupPipe = new PopupPipe(); - private pipe = this.popupPipe.create(); - private handleClick = (e: MouseEvent) => { - // compatiable vision stageBox - // TODO: optimize these codes const pane = e.currentTarget as HTMLDivElement; - let entry: any; function getTarget(node: any): any { if (!pane.contains(node) || (node.nodeName === 'A' && node.getAttribute('href'))) { return null; @@ -169,7 +211,6 @@ export class SettingsPane extends Component<{ target: SettingTopEntry | SettingF const target = node.dataset ? node.dataset.stageTarget : null; if (target) { - entry = node; return target; } return getTarget(node.parentNode); @@ -185,22 +226,40 @@ export class SettingsPane extends Component<{ target: SettingTopEntry | SettingF } const stage = skeleton.stages.container.get(target); if (stage) { - this.pipe.send(stage.content, stage.title); - this.pipe.show(entry); + if (this.currentStage) { + stage.setPrevious(this.currentStage); + } + this.currentStage = stage; } }; + private popStage() { + this.currentStage = this.currentStage?.getPrevious(); + } + render() { - const { target } = this.props; - const items = target.items; + let { target } = this.props; + return ( <div className="lc-settings-pane" onClick={this.handleClick}> - {/* todo: add head for single use */} - <PopupService popupPipe={this.popupPipe}> - <div className="lc-settings-content"> - {items.map((item, index) => createSettingFieldView(item, target, index))} - </div> - </PopupService> + { + this.currentStage && ( + <div className="lc-setting-stage-back"> + <Icon + className="lc-setting-stage-back-icon" + 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> ); } diff --git a/packages/editor-skeleton/src/components/settings/style.less b/packages/editor-skeleton/src/components/settings/style.less index a76bfbc78..826e6393d 100644 --- a/packages/editor-skeleton/src/components/settings/style.less +++ b/packages/editor-skeleton/src/components/settings/style.less @@ -3,6 +3,42 @@ height: 100%; 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 { text-align: center; font-size: 12px; diff --git a/packages/editor-skeleton/src/transducers/addon-combine.ts b/packages/editor-skeleton/src/transducers/addon-combine.ts index 35844ab29..1cc54dd50 100644 --- a/packages/editor-skeleton/src/transducers/addon-combine.ts +++ b/packages/editor-skeleton/src/transducers/addon-combine.ts @@ -212,6 +212,9 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp }, { componentName: 'VariableSetter' }], + extraProps: { + display: 'block', + }, }); } 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) { combined.push({