From 26e96a11e76f2a2c90a27d1291792bd877d902bf Mon Sep 17 00:00:00 2001 From: "Tyler B. Thrailkill" Date: Thu, 11 Apr 2024 00:47:23 -0600 Subject: [PATCH 01/25] Begin adding new Tour component from Antd currently only shows a tour modal, not connected to any components can be triggered with the Control a Component event handler --- .../src/comps/comps/tourComp/cascaderComp.tsx | 61 +++ .../comps/comps/tourComp/cascaderContants.tsx | 79 +++ .../src/comps/comps/tourComp/checkboxComp.tsx | 187 +++++++ .../tourComp/componentSelectorControl.tsx | 174 ++++++ .../src/comps/comps/tourComp/radioComp.tsx | 140 +++++ .../comps/tourComp/radioCompConstants.tsx | 98 ++++ .../comps/comps/tourComp/segmentedControl.tsx | 159 ++++++ .../src/comps/comps/tourComp/tourComp.tsx | 65 +++ .../comps/tourComp/tourCompConstants.tsx | 227 ++++++++ .../comps/tourComp/tourInputConstants.tsx | 149 +++++ .../actionSelector/executeCompAction.tsx | 4 + .../src/comps/controls/tourStepControl.tsx | 516 ++++++++++++++++++ client/packages/lowcoder/src/comps/index.tsx | 14 + .../lowcoder/src/comps/uiCompRegistry.ts | 3 +- .../src/pages/editor/editorConstants.tsx | 2 +- 15 files changed, 1876 insertions(+), 2 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/cascaderComp.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/cascaderContants.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/checkboxComp.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/componentSelectorControl.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/radioComp.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/radioCompConstants.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/segmentedControl.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/tourComp.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/tourCompConstants.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tourComp/tourInputConstants.tsx create mode 100644 client/packages/lowcoder/src/comps/controls/tourStepControl.tsx diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/cascaderComp.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/cascaderComp.tsx new file mode 100644 index 000000000..f5afba12b --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/cascaderComp.tsx @@ -0,0 +1,61 @@ +import { default as Cascader } from "antd/es/cascader"; +import { CascaderStyleType } from "comps/controls/styleControlConstants"; +import { blurMethod, focusMethod } from "comps/utils/methodUtils"; +import { trans } from "i18n"; +import styled from "styled-components"; +import { UICompBuilder, withDefault } from "../../generators"; +import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generators/withExposing"; +import { CascaderChildren, CascaderPropertyView, defaultDataSource } from "./cascaderContants"; +import { getStyle } from "./tourCompConstants"; +import { refMethods } from "comps/generators/withMethodExposing"; + +const CascaderStyle = styled(Cascader)<{ $style: CascaderStyleType }>` + width: 100%; + font-family:"Montserrat"; + ${(props) => props.$style && getStyle(props.$style)} +`; + +let CascaderBasicComp = (function () { + const childrenMap = CascaderChildren; + + return new UICompBuilder(childrenMap, (props) => { + return props.label({ + style: props.style, + children: ( + props.onEvent("focus")} + onBlur={() => props.onEvent("blur")} + onChange={(value: (string | number)[]) => { + props.value.onChange(value as string[]); + props.onEvent("change"); + }} + /> + ), + }); + }) + .setPropertyViewFn((children) => ( + <> + + + )) + .setExposeMethodConfigs(refMethods([focusMethod, blurMethod])) + .build(); +})(); + +const CascaderComp = withExposingConfigs(CascaderBasicComp, [ + new NameConfig("value", trans("selectInput.valueDesc")), + ...CommonNameConfig, +]); + +export const CascaderWithDefault = withDefault(CascaderComp, { + options: defaultDataSource, +}); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/cascaderContants.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/cascaderContants.tsx new file mode 100644 index 000000000..1a631e2eb --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/cascaderContants.tsx @@ -0,0 +1,79 @@ +import { SelectEventHandlerControl } from "../../controls/eventHandlerControl"; +import { Section, sectionNames } from "lowcoder-design"; +import { RecordConstructorToComp } from "lowcoder-core"; +import { BoolCodeControl, JSONObjectArrayControl, StringControl } from "comps/controls/codeControl"; +import { arrayStringExposingStateControl } from "comps/controls/codeStateControl"; +import { BoolControl } from "comps/controls/boolControl"; +import { LabelControl } from "comps/controls/labelControl"; +import { styleControl } from "comps/controls/styleControl"; +import { CascaderStyle } from "comps/controls/styleControlConstants"; +import { + allowClearPropertyView, + disabledPropertyView, + hiddenPropertyView, + placeholderPropertyView, + showSearchPropertyView, +} from "comps/utils/propertyUtils"; +import { i18nObjs, trans } from "i18n"; +import { RefControl } from "comps/controls/refControl"; +import { CascaderRef } from "antd/lib/cascader"; + +import { MarginControl } from "../../controls/marginControl"; +import { PaddingControl } from "../../controls/paddingControl"; + +import { useContext } from "react"; +import { EditorContext } from "comps/editorState"; + +export const defaultDataSource = JSON.stringify(i18nObjs.cascader, null, " "); + +export const CascaderChildren = { + value: arrayStringExposingStateControl("value", i18nObjs.cascaderDefult), + label: LabelControl, + placeholder: StringControl, + disabled: BoolCodeControl, + onEvent: SelectEventHandlerControl, + allowClear: BoolControl, + options: JSONObjectArrayControl, + style: styleControl(CascaderStyle), + showSearch: BoolControl.DEFAULT_TRUE, + viewRef: RefControl, + margin: MarginControl, + padding: PaddingControl, +}; + +export const CascaderPropertyView = ( + children: RecordConstructorToComp +) => ( + <> +
+ {children.options.propertyView({ label: trans("cascader.options") })} + {children.value.propertyView({ label: trans("prop.defaultValue") })} + {placeholderPropertyView(children)} +
+ + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {children.onEvent.getPropertyView()} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
+ )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + children.label.getPropertyView() + )} + + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {allowClearPropertyView(children)} + {showSearchPropertyView(children)} +
+ )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {children.style.getPropertyView()} +
+ )} + +); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/checkboxComp.tsx new file mode 100644 index 000000000..a64ba0286 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/checkboxComp.tsx @@ -0,0 +1,187 @@ +import { default as AntdCheckboxGroup } from "antd/es/checkbox/Group"; +import { SelectInputOptionControl } from "comps/controls/optionsControl"; +import { BoolCodeControl } from "../../controls/codeControl"; +import { arrayStringExposingStateControl } from "../../controls/codeStateControl"; +import { LabelControl } from "../../controls/labelControl"; +import { ChangeEventHandlerControl } from "../../controls/eventHandlerControl"; +import { UICompBuilder } from "../../generators"; +import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generators/withExposing"; +import styled, { css } from "styled-components"; +import { + selectDivRefMethods, + TourInputInvalidConfig, + SelectInputValidationChildren, + useSelectInputValidate, +} from "./tourInputConstants"; +import { formDataChildren } from "../formComp/formDataConstants"; +import { styleControl } from "comps/controls/styleControl"; +import { CheckboxStyle, CheckboxStyleType } from "comps/controls/styleControlConstants"; +import { RadioLayoutOptions, RadioPropertyView } from "./radioCompConstants"; +import { dropdownControl } from "../../controls/dropdownControl"; +import { ValueFromOption } from "lowcoder-design"; +import { EllipsisTextCss } from "lowcoder-design"; +import { trans } from "i18n"; +import { RefControl } from "comps/controls/refControl"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; + +export const getStyle = (style: CheckboxStyleType) => { + return css` + &, + .ant-checkbox-wrapper:not(.ant-checkbox-wrapper-disabled) { + color: ${style.staticText}; + max-width: calc(100% - 8px); + + span:not(.ant-checkbox) { + ${EllipsisTextCss}; + } + + .ant-checkbox-checked { + .ant-checkbox-inner { + background-color: ${style.checkedBackground}; + border-color: ${style.checkedBackground}; + border-width:${!!style.borderWidth ? style.borderWidth : '2px'}; + + &::after { + border-color: ${style.checked}; + } + } + + &::after { + border-color: ${style.checkedBackground}; + border-width:${!!style.borderWidth ? style.borderWidth : '2px'}; + border-radius: ${style.radius}; + } + } + + .ant-checkbox-inner { + border-radius: ${style.radius}; + background-color: ${style.uncheckedBackground}; + border-color: ${style.uncheckedBorder}; + border-width:${!!style.borderWidth ? style.borderWidth : '2px'}; + } + + &:hover .ant-checkbox-inner, + .ant-checkbox:hover .ant-checkbox-inner, + .ant-checkbox-input + ant-checkbox-inner { + background-color:${style.hoverBackground ? style.hoverBackground :'#fff'}; + } + + &:hover .ant-checkbox-checked .ant-checkbox-inner, + .ant-checkbox:hover .ant-checkbox-inner, + .ant-checkbox-input + ant-checkbox-inner { + background-color:${style.hoverBackground ? style.hoverBackground:'#ffff'}; + } + + &:hover .ant-checkbox-inner, + .ant-checkbox:hover .ant-checkbox-inner, + .ant-checkbox-input:focus + .ant-checkbox-inner { + border-color: ${style.checkedBackground}; + border-width:${!!style.borderWidth ? style.borderWidth : '2px'}; + } + } + + + + .ant-checkbox-group-item { + font-family:${style.fontFamily}; + font-size:${style.textSize}; + font-weight:${style.textWeight}; + font-style:${style.fontStyle}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; + } + .ant-checkbox-wrapper { + padding: ${style.padding}; + .ant-checkbox-inner, + .ant-checkbox-checked::after { + border-radius: ${style.radius}; + } + } + `; +}; + +const CheckboxGroup = styled(AntdCheckboxGroup) <{ + $style: CheckboxStyleType; + $layout: ValueFromOption; +}>` + min-height: 32px; + ${(props) => props.$style && getStyle(props.$style)} + ${(props) => { + if (props.$layout === "horizontal") { + return css` + display: flex; + align-items: center; + flex-wrap: wrap; + `; + } else if (props.$layout === "vertical") { + return css` + display: flex; + flex-direction: column; + `; + } else if (props.$layout === "auto_columns") { + return css` + break-inside: avoid; + columns: 160px; + `; + } + }} +`; + +let CheckboxBasicComp = (function () { + const childrenMap = { + defaultValue: arrayStringExposingStateControl("defaultValue"), + value: arrayStringExposingStateControl("value"), + label: LabelControl, + disabled: BoolCodeControl, + onEvent: ChangeEventHandlerControl, + options: SelectInputOptionControl, + style: styleControl(CheckboxStyle), + layout: dropdownControl(RadioLayoutOptions, "horizontal"), + viewRef: RefControl, + + ...SelectInputValidationChildren, + ...formDataChildren, + }; + return new UICompBuilder(childrenMap, (props) => { + const [ + validateState, + handleChange, + ] = useSelectInputValidate(props); + return props.label({ + required: props.required, + style: props.style, + children: ( + option.value !== undefined && !option.hidden) + .map((option) => ({ + label: option.label, + value: option.value, + disabled: option.disabled, + }))} + onChange={(values) => { + handleChange(values as string[]); + }} + /> + ), + ...validateState, + }); + }) + .setPropertyViewFn((children) => ) + .setExposeMethodConfigs(selectDivRefMethods) + .build(); +})(); + +CheckboxBasicComp = migrateOldData(CheckboxBasicComp, fixOldInputCompData); + +export const CheckboxComp = withExposingConfigs(CheckboxBasicComp, [ + new NameConfig("value", trans("selectInput.valueDesc")), + TourInputInvalidConfig, + ...CommonNameConfig, +]); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/componentSelectorControl.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/componentSelectorControl.tsx new file mode 100644 index 000000000..dda3c4662 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/componentSelectorControl.tsx @@ -0,0 +1,174 @@ +import { trans } from "@lowcoder-ee/i18n"; +import { getPromiseAfterDispatch } from "@lowcoder-ee/util/promiseUtils"; +import { CompParams, ConstructorToDataType, customAction, routeByNameAction } from "lowcoder-core"; +import { ExecuteAction, ParamsConfig } from "@lowcoder-ee/comps/controls/actionSelector/executeCompTypes"; +import { CompNameContext, EditorContext, EditorState } from "@lowcoder-ee/comps/editorState"; +import { mapValues } from "lodash"; +import { Dropdown } from "components/Dropdown"; +import { GridItemComp } from "@lowcoder-ee/comps/comps/gridItemComp"; +import { HookComp } from "@lowcoder-ee/comps/hooks/hookComp"; +import { TemporaryStateItemComp } from "@lowcoder-ee/comps/comps/temporaryStateComp"; +import { SimpleNameComp } from "@lowcoder-ee/comps/comps/simpleNameComp"; +import { list, MultiCompBuilder, valueComp } from "lowcoder-sdk"; +import { Fragment, ReactNode } from "react"; +import { ParamsValueComp } from "@lowcoder-ee/comps/controls/actionSelector/executeCompAction"; + +const ParamsValueTmpControl = list(ParamsValueComp); + +class ParamsValueControl extends ParamsValueTmpControl { + constructor(params: CompParams>) { + // Compatible with old dsl, params is an array of strings, no parameter names + if (params.value && params.value.length > 0 && !params.value[0].hasOwnProperty("compType")) { + params.value = params.value.map((param) => ({ + comp: param, + compType: "string" + })); + } + super(params); + } + + propertyView(params: ParamsConfig): ReactNode { + return this.getView().map((view, i) => ( + + {view.children.comp.propertyView({ + tooltip: params[i]?.description, + label: params[i]?.name, + layout: "vertical" + })} + + )); + } +} + +const ExecuteCompTmpAction = (function() { + const childrenMap = { + name: SimpleNameComp, + methodName: valueComp(""), + params: ParamsValueControl + }; + return new MultiCompBuilder(childrenMap, () => { + return () => Promise.resolve(undefined as unknown); + }) + .setPropertyViewFn(() => <>) + .build(); +})(); + +interface ExecuteCompActionOptions { + compListGetter: ( + es: EditorState + ) => (GridItemComp | HookComp | InstanceType)[]; + selectLabel?: string; +} + +export function targetCompAction(params: ExecuteCompActionOptions) { + const { compListGetter, selectLabel = trans("eventHandler.component") } = params; + + class InternalExecuteCompAction extends ExecuteCompTmpAction { + displayName() { + const name = this.children.name.getView(); + const method = this.children.methodName.getView(); + if (name && method) { + return `${name}.${method}()`; + } + } + + override getView() { + const name = this.children.name.getView(); + if (!name) { + return () => Promise.resolve(); + } + return () => + getPromiseAfterDispatch( + this.dispatch, + routeByNameAction( + name, + customAction( + { + type: "execute", + methodName: this.children.methodName.getView(), + params: this.children.params.getView().map((x) => x.getView()) + }, + false + ) + ), + { + notHandledError: trans("eventHandler.notHandledError") + } + ); + } + + exposingNode() { + return this.node(); + } + + propertyView() { + return ( + + {(editorState) => { + const compMethods: Record> = {}; + const compList: (GridItemComp | HookComp | InstanceType)[] = compListGetter(editorState); + + compList.forEach((item) => { + compMethods[item.children.name.getView()] = mapValues( + item.exposingInfo().methods, + (v) => v.params + ); + }); + + function changeMethodAction(compName: string, methodName: string) { + const currentMethods = compMethods[compName] ?? {}; + const params = currentMethods[methodName]; + return { + name: compName, + methodName: methodName, + params: params?.map((p) => ({ + compType: p.type, + name: p.name + })) + }; + } + + const name = this.children.name.getView(); + const methods = compMethods[name] ?? {}; + const params = methods[this.children.methodName.getView()]; + return ( + <> + + {(compName) => ( + + Object.keys(compMethods[item.children.name.getView()]).length > 0 + ) + .filter((item) => item.children.name.getView() !== compName) + .map((item) => ({ + label: item.children.name.getView(), + value: item.children.name.getView() + }))} + label={selectLabel} + onChange={(value) => + this.dispatchChangeValueAction( + changeMethodAction(value, Object.keys(compMethods[value])[0]) + ) + } + /> + )} + + + ); + }} + + ); + } + } + + return InternalExecuteCompAction; +} + + +export const TargetCompAction = targetCompAction({ + compListGetter: (editorState: EditorState) => Object.values(editorState.getAllUICompMap()) +}); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/radioComp.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/radioComp.tsx new file mode 100644 index 000000000..29cf36b86 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/radioComp.tsx @@ -0,0 +1,140 @@ +import { default as AntdRadioGroup } from "antd/es/radio/group"; +import { RadioStyleType } from "comps/controls/styleControlConstants"; +import styled, { css } from "styled-components"; +import { UICompBuilder } from "../../generators"; +import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generators/withExposing"; +import { RadioChildrenMap, RadioLayoutOptions, RadioPropertyView } from "./radioCompConstants"; +import { + selectDivRefMethods, + TourInputInvalidConfig, + useSelectInputValidate, +} from "./tourInputConstants"; +import { EllipsisTextCss, ValueFromOption } from "lowcoder-design"; +import { trans } from "i18n"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { migrateOldData } from "comps/generators/simpleGenerators"; + +const getStyle = (style: RadioStyleType) => { + return css` + .ant-radio-wrapper:not(.ant-radio-wrapper-disabled) { + color: ${style.staticText}; + // height: 22px; + max-width: calc(100% - 8px); + padding: ${style.padding}; + span:not(.ant-radio) { + ${EllipsisTextCss}; + font-family:${style.fontFamily}; + font-size:${style.textSize}; + font-weight:${style.textWeight}; + font-style:${style.fontStyle}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; + } + + .ant-radio-checked { + .ant-radio-inner { + background-color: ${style.checkedBackground}; + border-color: ${style.checkedBackground}; + } + + &::after { + border-color: ${style.checkedBackground}; + } + } + + .ant-radio-inner { + background-color: ${style.uncheckedBackground}; + border-color: ${style.uncheckedBorder}; + border-width:${style.borderWidth}; + &::after { + background-color: ${style.checked}; + } + } + + &:hover .ant-radio-inner, + .ant-radio:hover .ant-radio-inner, + .ant-radio-input + ant-radio-inner { + background-color:${style.hoverBackground ? style.hoverBackground:'#ffff'}; + } + + &:hover .ant-radio-inner, + .ant-radio:hover .ant-radio-inner, + .ant-radio-input:focus + .ant-radio-inner { + border-color: ${style.checkedBackground}; + } + } + `; +}; + +const Radio = styled(AntdRadioGroup)<{ + $style: RadioStyleType; + $layout: ValueFromOption; +}>` + width: 100%; + min-height: 32px; + + ${(props) => props.$style && getStyle(props.$style)} + ${(props) => { + if (props.$layout === "horizontal") { + return css` + display: flex; + align-items: center; + flex-wrap: wrap; + `; + } else if (props.$layout === "vertical") { + return css` + display: flex; + flex-direction: column; + `; + } else if (props.$layout === "auto_columns") { + return css` + break-inside: avoid; + columns: 160px; + `; + } + }} +`; + +let RadioBasicComp = (function () { + return new UICompBuilder(RadioChildrenMap, (props) => { + const [ + validateState, + handleChange, + ] = useSelectInputValidate(props); + return props.label({ + required: props.required, + style: props.style, + children: ( + { + handleChange(e.target.value); + }} + options={props.options + .filter((option) => option.value !== undefined && !option.hidden) + .map((option) => ({ + label: option.label, + value: option.value, + disabled: option.disabled, + }))} + /> + ), + ...validateState, + }); + }) + .setPropertyViewFn((children) => ) + .setExposeMethodConfigs(selectDivRefMethods) + .build(); +})(); + +RadioBasicComp = migrateOldData(RadioBasicComp, fixOldInputCompData); + +export const RadioComp = withExposingConfigs(RadioBasicComp, [ + new NameConfig("value", trans("selectInput.valueDesc")), + TourInputInvalidConfig, + ...CommonNameConfig, +]); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/radioCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/radioCompConstants.tsx new file mode 100644 index 000000000..937295071 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/radioCompConstants.tsx @@ -0,0 +1,98 @@ +import { RecordConstructorToComp } from "lowcoder-core"; +import { BoolCodeControl } from "../../controls/codeControl"; +import { LabelControl } from "../../controls/labelControl"; +import { + arrayStringExposingStateControl, + stringExposingStateControl, +} from "../../controls/codeStateControl"; +import { Section, sectionNames } from "lowcoder-design"; +import { SelectInputOptionControl } from "../../controls/optionsControl"; +import { ChangeEventHandlerControl } from "../../controls/eventHandlerControl"; +import { + SelectInputValidationChildren, + SelectInputValidationSection, +} from "./tourInputConstants"; +import { formDataChildren, FormDataPropertyView } from "../formComp/formDataConstants"; +import { styleControl } from "comps/controls/styleControl"; +import { RadioStyle } from "comps/controls/styleControlConstants"; +import { dropdownControl } from "../../controls/dropdownControl"; +import { hiddenPropertyView, disabledPropertyView } from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import { RefControl } from "comps/controls/refControl"; + +import { useContext } from "react"; +import { EditorContext } from "comps/editorState"; + +export const RadioLayoutOptions = [ + { label: trans("radio.horizontal"), value: "horizontal" }, + { label: trans("radio.vertical"), value: "vertical" }, + { label: trans("radio.autoColumns"), value: "auto_columns" }, +] as const; + +export const RadioChildrenMap = { + defaultValue: stringExposingStateControl("value"), + value: stringExposingStateControl("value"), + label: LabelControl, + disabled: BoolCodeControl, + onEvent: ChangeEventHandlerControl, + options: SelectInputOptionControl, + style: styleControl(RadioStyle), + layout: dropdownControl(RadioLayoutOptions, "horizontal"), + viewRef: RefControl, + + ...SelectInputValidationChildren, + ...formDataChildren, +}; + +export const RadioPropertyView = ( + children: RecordConstructorToComp< + typeof RadioChildrenMap & { hidden: typeof BoolCodeControl } & { + defaultValue: + | ReturnType + | ReturnType; + value: + | ReturnType + | ReturnType; + } + > +) => ( + <> +
+ {children.options.propertyView({})} + {children.defaultValue.propertyView({ label: trans("prop.defaultValue") })} +
+ + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + <> + +
+ {children.onEvent.getPropertyView()} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
+ )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {children.layout.propertyView({ + label: trans("radio.options"), + tooltip: ( +
+
{trans("radio.horizontalTooltip")}
+
{trans("radio.verticalTooltip")}
+
{trans("radio.autoColumnsTooltip")}
+
+ ), + })} +
+ )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + children.label.getPropertyView() + )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
{children.style.getPropertyView()}
+ )} + +); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/segmentedControl.tsx new file mode 100644 index 000000000..8478f002a --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/segmentedControl.tsx @@ -0,0 +1,159 @@ +import { default as AntdSegmented } from "antd/es/segmented"; +import { BoolCodeControl } from "comps/controls/codeControl"; +import { stringExposingStateControl } from "comps/controls/codeStateControl"; +import { ChangeEventHandlerControl } from "comps/controls/eventHandlerControl"; +import { LabelControl } from "comps/controls/labelControl"; +import { SelectOptionControl } from "comps/controls/optionsControl"; +import { styleControl } from "comps/controls/styleControl"; +import { SegmentStyle, SegmentStyleType } from "comps/controls/styleControlConstants"; +import styled, { css } from "styled-components"; +import { UICompBuilder } from "../../generators"; +import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generators/withExposing"; +import { formDataChildren, FormDataPropertyView } from "../formComp/formDataConstants"; +import { + selectDivRefMethods, + TourInputInvalidConfig, + SelectInputValidationChildren, + SelectInputValidationSection, + useSelectInputValidate, +} from "./tourInputConstants"; +import { Section, sectionNames } from "lowcoder-design"; +import { hiddenPropertyView, disabledPropertyView } from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import { hasIcon } from "comps/utils"; +import { RefControl } from "comps/controls/refControl"; + +import { useContext } from "react"; +import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; + + +const getStyle = (style: SegmentStyleType) => { + return css` + &.ant-segmented:not(.ant-segmented-disabled) { + background-color: ${style.background}; + + &, + .ant-segmented-item-selected, + .ant-segmented-thumb, + .ant-segmented-item:hover, + .ant-segmented-item:focus { + color: ${style.text}; + border-radius: ${style.radius}; + } + .ant-segmented-item { + padding: ${style.padding}; + } + .ant-segmented-item-selected, + .ant-segmented-thumb { + background-color: ${style.indicatorBackground}; + } + } + + &.ant-segmented, + .ant-segmented-item-selected { + border-radius: ${style.radius}; + } + &.ant-segmented, .ant-segmented-item-label { + font-family:${style.fontFamily}; + font-style:${style.fontStyle}; + font-size:${style.textSize}; + font-weight:${style.textWeight}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; + } + `; +}; + +const Segmented = styled(AntdSegmented)<{ $style: SegmentStyleType }>` + width: 100%; + min-height: 24px; // keep the height unchanged when there are no options + ${(props) => props.$style && getStyle(props.$style)} +`; + +const SegmentChildrenMap = { + defaultValue: stringExposingStateControl("value"), + value: stringExposingStateControl("value"), + label: LabelControl, + disabled: BoolCodeControl, + onEvent: ChangeEventHandlerControl, + options: SelectOptionControl, + style: styleControl(SegmentStyle), + viewRef: RefControl, + + ...SelectInputValidationChildren, + ...formDataChildren, +}; + +let SegmentedControlBasicComp = (function () { + return new UICompBuilder(SegmentChildrenMap, (props) => { + const [ + validateState, + handleChange, + ] = useSelectInputValidate(props); + return props.label({ + required: props.required, + style: props.style, + children: ( + { + handleChange(value.toString()); + }} + options={props.options + .filter((option) => option.value !== undefined && !option.hidden) + .map((option) => ({ + label: option.label, + value: option.value, + disabled: option.disabled, + icon: hasIcon(option.prefixIcon) && option.prefixIcon, + }))} + /> + ), + ...validateState, + }); + }) + .setPropertyViewFn((children) => ( + <> +
+ {children.options.propertyView({})} + {children.defaultValue.propertyView({ label: trans("prop.defaultValue") })} +
+ + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + <> + +
+ {children.onEvent.getPropertyView()} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
+ )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + children.label.getPropertyView() + )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {children.style.getPropertyView()} +
+ )} + + )) + .setExposeMethodConfigs(selectDivRefMethods) + .build(); +})(); + +SegmentedControlBasicComp = migrateOldData(SegmentedControlBasicComp, fixOldInputCompData); + +export const SegmentedControlComp = withExposingConfigs(SegmentedControlBasicComp, [ + new NameConfig("value", trans("selectInput.valueDesc")), + TourInputInvalidConfig, + ...CommonNameConfig, +]); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/tourComp.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/tourComp.tsx new file mode 100644 index 000000000..60b56ef4a --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourComp.tsx @@ -0,0 +1,65 @@ +import { styleControl } from "comps/controls/styleControl"; +import { SelectStyle } from "comps/controls/styleControlConstants"; +import { trans } from "i18n"; +import { stringExposingStateControl } from "lowcoder-sdk"; +import { UICompBuilder } from "lowcoder-sdk"; +import { CommonNameConfig, NameConfig, withExposingConfigs , withMethodExposing} from "lowcoder-sdk"; +import { + baseSelectRefMethods, + TourChildrenMap, + TourPropertyView, +} from "./tourCompConstants"; +import { + TourInputCommonConfig, + TourInputInvalidConfig, +} from "./tourInputConstants"; +import { Tour, TourProps } from "antd"; + +let TourBasicComp = (function () { + const childrenMap = { + ...TourChildrenMap, + defaultValue: stringExposingStateControl("defaultValue"), + value: stringExposingStateControl("value"), + style: styleControl(SelectStyle), + }; + return new UICompBuilder(childrenMap, (props, dispatch) => { + + const steps: TourProps['steps'] = props.options.map((step) => { + return { + title: step.title, + description: step.description, + target: null, + } + }) + + return ( props.open.onChange(false)} + />) + }) + .setPropertyViewFn((children) => ) + .setExposeMethodConfigs(baseSelectRefMethods) + .build(); +})(); + +TourBasicComp = withMethodExposing(TourBasicComp, [ + { + method: { + name: "startTour", + description: "Triggers the tour to start", + params: [], + }, + execute: (comp, values) => { + comp.children.open.getView().onChange(true) + } + } +]) + +export const TourComp = withExposingConfigs(TourBasicComp, [ + new NameConfig("value", trans("selectInput.valueDesc")), + new NameConfig("inputValue", trans("select.inputValueDesc")), + TourInputInvalidConfig, + ...TourInputCommonConfig, + ...CommonNameConfig, +]); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/tourCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/tourCompConstants.tsx new file mode 100644 index 000000000..cff31bf5f --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourCompConstants.tsx @@ -0,0 +1,227 @@ +import { + changeChildAction, + DispatchType, + RecordConstructorToComp, + RecordConstructorToView, +} from "lowcoder-core"; +import { BoolControl } from "../../controls/boolControl"; +import { LabelControl } from "../../controls/labelControl"; +import { BoolCodeControl, StringControl } from "../../controls/codeControl"; +import { PaddingControl } from "../../controls/paddingControl"; +import { MarginControl } from "../../controls/marginControl"; +import { + ControlNode, + isDarkColor, + lightenColor, + MultiselectTagIcon, + Section, + sectionNames, +} from "lowcoder-design"; +import { SelectOptionControl } from "../../controls/optionsControl"; +import { SelectEventHandlerControl } from "../../controls/eventHandlerControl"; +import { default as AntdSelect } from "antd/es/select"; +import { ControlParams } from "../../controls/controlParams"; +import { ReactNode } from "react"; +import styled, { css } from "styled-components"; +import { + SelectInputValidationChildren, + SelectInputValidationSection, +} from "./tourInputConstants"; +import { + formDataChildren, + FormDataPropertyView, +} from "../formComp/formDataConstants"; +import { + CascaderStyleType, + MultiSelectStyleType, + SelectStyleType, + TreeSelectStyleType, + widthCalculator, + heightCalculator, +} from "comps/controls/styleControlConstants"; +import { stateComp, withDefault } from "../../generators"; +import { + allowClearPropertyView, + disabledPropertyView, + hiddenPropertyView, + placeholderPropertyView, + showSearchPropertyView, +} from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import { hasIcon } from "comps/utils"; +import { RefControl } from "comps/controls/refControl"; +import { BaseSelectRef } from "rc-select"; +import { refMethods } from "comps/generators/withMethodExposing"; +import { blurMethod, focusMethod } from "comps/utils/methodUtils"; + +import { useContext } from "react"; +import { EditorContext } from "comps/editorState"; +import { TourStepControl } from "@lowcoder-ee/comps/controls/tourStepControl"; +import { booleanExposingStateControl } from "lowcoder-sdk"; + +export const getStyle = ( + style: + | SelectStyleType + | MultiSelectStyleType + | CascaderStyleType + | TreeSelectStyleType +) => { + return css` + &.ant-select .ant-select-selector, + &.ant-select-multiple .ant-select-selection-item { + border-radius: ${style.radius}; + padding: ${style.padding}; + height: auto; + } + .ant-select-selection-search { + padding: ${style.padding}; + } + .ant-select-selection-search-input { + font-family:${(style as SelectStyleType).fontFamily} !important; + text-transform:${(style as SelectStyleType).textTransform} !important; + text-decoration:${(style as SelectStyleType).textDecoration} !important; + font-size:${(style as SelectStyleType).textSize} !important; + font-weight:${(style as SelectStyleType).textWeight}; + color:${(style as SelectStyleType).text} !important; + font-style:${(style as SelectStyleType).fontStyle}; + } + .ant-select-selector::after, + .ant-select-selection-placeholder, + .ant-select-selection-item { + line-height: 1.5715 !important; + } + + &.ant-select:not(.ant-select-disabled) { + color: ${style.text}; + .ant-select-selection-placeholder, + .ant-select-selection-item { + line-height: 1.5715 !important; + } + .ant-select-selection-placeholder, + &.ant-select-single.ant-select-open .ant-select-selection-item { + color: ${style.text}; + opacity: 0.4; + width: 100%; + } + + .ant-select-selector { + background-color: ${style.background}; + border-color: ${style.border}; + border-width:${(style as SelectStyleType).borderWidth}; + } + + &.ant-select-focused, + &:hover { + .ant-select-selector { + border-color: ${style.accent}; + } + } + + .ant-select-arrow, + .ant-select-clear { + background-color: ${style.background}; + color: ${style.text === "#222222" + ? "#8B8FA3" + : isDarkColor(style.text) + ? lightenColor(style.text, 0.2) + : style.text}; + } + + .ant-select-clear:hover { + color: ${style.text === "#222222" + ? "#8B8FA3" + : isDarkColor(style.text) + ? lightenColor(style.text, 0.1) + : style.text}; + } + + &.ant-select-multiple .ant-select-selection-item { + border: none; + background-color: ${(style as MultiSelectStyleType).tags}; + color: ${(style as MultiSelectStyleType).tagsText}; + border-radius: ${style.radius}; + + .ant-select-selection-item-remove { + color: ${(style as MultiSelectStyleType).tagsText}; + opacity: 0.5; + } + } + } + `; +}; + +export const TourChildrenMap = { + label: LabelControl, + placeholder: StringControl, + disabled: BoolCodeControl, + open: booleanExposingStateControl("open"), + onEvent: SelectEventHandlerControl, + options: TourStepControl, + allowClear: BoolControl, + inputValue: stateComp(""), // user's input value when search + showSearch: BoolControl.DEFAULT_TRUE, + viewRef: RefControl, + margin: MarginControl, + padding: PaddingControl, + ...SelectInputValidationChildren, + ...formDataChildren, +}; + +export const TourPropertyView = ( + children: RecordConstructorToComp< + typeof TourChildrenMap & { + hidden: typeof BoolCodeControl; + } + > & { + defaultValue: { propertyView: (params: ControlParams) => ControlNode }; + value: { propertyView: (params: ControlParams) => ControlNode }; + style: { getPropertyView: () => ControlNode }; + } +) => ( + <> +
+ {children.options.propertyView({})} + {children.defaultValue.propertyView({ + label: trans("prop.defaultValue"), + })} + {placeholderPropertyView(children)} +
+ + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + <> + <> + + + +
+ {children.onEvent.getPropertyView()} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
+ + )} + + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && + children.label.getPropertyView()} + + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {allowClearPropertyView(children)} + {showSearchPropertyView(children)} +
+ )} + + {["layout", "both"].includes( + useContext(EditorContext).editorModeStatus + ) && ( +
+ {children.style.getPropertyView()} +
+ )} + +); + +export const baseSelectRefMethods = refMethods([ + focusMethod, + blurMethod, +]); diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/tourInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/tourInputConstants.tsx new file mode 100644 index 000000000..cc122d1cb --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourInputConstants.tsx @@ -0,0 +1,149 @@ +import { Section, sectionNames } from "lowcoder-design"; +import { ChildrenTypeToDepsKeys, depsConfig } from "../../generators/withExposing"; +import { BoolControl } from "../../controls/boolControl"; +import { CustomRuleControl } from "../../controls/codeControl"; +import { isEmpty } from "lodash"; +import { ConstructorToComp, RecordConstructorToComp } from "lowcoder-core"; +import { + arrayStringExposingStateControl, + jsonExposingStateControl, + stringExposingStateControl, +} from "../../controls/codeStateControl"; +import { requiredPropertyView } from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import { useEffect, useRef, useState } from "react"; +import { SelectInputOptionControl } from "../../controls/optionsControl"; +import { refMethods } from "comps/generators/withMethodExposing"; +import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; +import { TourStepControl } from "@lowcoder-ee/comps/controls/tourStepControl"; + +export const SelectInputValidationChildren = { + required: BoolControl, + customRule: CustomRuleControl, +}; +type ValidationComp = RecordConstructorToComp; + +type SelectValue = string | (string | number)[]; +type ValidationParams = { + defaultValue?: { + value: SelectValue, + }; + value: { + value: SelectValue, + onChange?: (value: any) => Promise, + }; + required: boolean; + customRule: string; + onEvent?: (eventName: string) => Promise, +}; + +export const selectInputValidate = ( + props: ValidationParams +): { + validateStatus: "success" | "warning" | "error"; + help?: string; +} => { + if (props.customRule) { + return { validateStatus: "error", help: props.customRule }; + } + const value = props.value.value; + if (props.required && isEmpty(value)) { + return { validateStatus: "error", help: trans("prop.required") }; + } + return { validateStatus: "success" }; +}; + +export const useSelectInputValidate = (props: ValidationParams) => { + const [validateState, setValidateState] = useState({}); + const changeRef = useRef(false) + const propsRef = useRef(props); + propsRef.current = props; + + const selectValue = props.value.value; + const defaultValue = props.defaultValue?.value; + + const handleValidate = (value: string | (string | number)[]) => { + setValidateState( + selectInputValidate({ + ...propsRef.current, + value: { + value, + }, + }) + ); + }; + + useEffect(() => { + props.value.onChange?.(defaultValue) + }, [defaultValue]); + + useEffect(() => { + if (!changeRef.current) return; + + handleValidate(selectValue); + props.onEvent?.("change"); + changeRef.current = false; + }, [selectValue]); + + const handleChange = (value: any) => { + props.value.onChange?.(value); + changeRef.current = true; + }; + + return [ + validateState, + // handleValidate, + handleChange, + ] as const; +}; + +type ValidationCompWithValue = ValidationComp & { + value: ConstructorToComp< + ReturnType< + | typeof stringExposingStateControl + | typeof arrayStringExposingStateControl + | typeof jsonExposingStateControl<(string | number)[]> + > + >; +}; +export const TourInputInvalidConfig = depsConfig< + ValidationCompWithValue, + ChildrenTypeToDepsKeys +>({ + name: "invalid", + desc: trans("export.invalidDesc"), + depKeys: ["value", "required", "customRule"], + func: (input) => + selectInputValidate({ + ...input, + value: { value: input.value }, + }).validateStatus !== "success", +}); + +export const SelectInputValidationSection = (children: ValidationComp) => ( +
+ {requiredPropertyView(children)} + {children.customRule.propertyView({})} +
+); + +type ChildrenType = RecordConstructorToComp<{ + value: ReturnType; + options: typeof TourStepControl; +}>; +export const TourInputCommonConfig = [ + depsConfig>({ + name: "selectedIndex", + desc: trans("selectInput.selectedIndexDesc"), + depKeys: ["value", "options"], + func: (input) => input.options.findIndex?.((o: any) => o.value === input.value), + }), + depsConfig>({ + name: "selectedLabel", + desc: trans("selectInput.selectedLabelDesc"), + depKeys: ["value", "options"], + func: (input) => input.options.find?.((o: any) => o.value === input.value)?.title, + }), +]; + +export const selectDivRefMethods = refMethods([focusWithOptions, blurMethod]); diff --git a/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx b/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx index a14adb207..b6b81f324 100644 --- a/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx +++ b/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx @@ -121,6 +121,10 @@ export function executeCompAction(params: ExecuteCompActionOptions) { ); } + exposingNode() { + return this.node(); + } + propertyView() { return ( diff --git a/client/packages/lowcoder/src/comps/controls/tourStepControl.tsx b/client/packages/lowcoder/src/comps/controls/tourStepControl.tsx new file mode 100644 index 000000000..501e55ab0 --- /dev/null +++ b/client/packages/lowcoder/src/comps/controls/tourStepControl.tsx @@ -0,0 +1,516 @@ +import { ViewDocIcon } from "assets/icons"; +import { + ArrayControl, + BoolCodeControl, + FunctionControl, + RadiusControl, + StringControl +} from "comps/controls/codeControl"; +import { dropdownControl, LeftRightControl } from "comps/controls/dropdownControl"; +import { IconControl } from "comps/controls/iconControl"; +import { MultiCompBuilder, valueComp, withContext, withDefault } from "comps/generators"; +import { list } from "comps/generators/list"; +import { ToViewReturn } from "comps/generators/multi"; +import { genRandomKey } from "comps/utils/idGenerator"; +import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import _, { mapValues } from "lodash"; +import { + Comp, + CompAction, + CompActionTypes, + CompParams, + ConstructorToDataType, + ConstructorToView, + fromRecord, + MultiBaseComp, + withFunction, +} from "lowcoder-core"; +import { + AutoArea, + CompressIcon, + controlItem, + ExpandIcon, + IconRadius, + Option, + WidthIcon, + ImageCompIcon, +} from "lowcoder-design"; +import styled from "styled-components"; +import { lastValueIfEqual } from "util/objectUtils"; +import { getNextEntityName } from "util/stringUtils"; +import { JSONObject, JSONValue } from "util/jsonTypes"; +import { ButtonEventHandlerControl } from "./eventHandlerControl"; +import { ControlItemCompBuilder } from "comps/generators/controlCompBuilder"; +import { ColorControl } from "./colorControl"; +import { StringStateControl } from "./codeStateControl"; +import { reduceInContext } from "../utils/reduceContext"; +import { optionsControl } from "lowcoder-sdk"; +import type { TourProps as AntdTourProps } from 'antd'; +import { useRef, useState } from "react"; +import { EditorState } from "@lowcoder-ee/comps/editorState"; +import { executeCompAction } from "@lowcoder-ee/comps/controls/actionSelector/executeCompAction"; +import { TargetCompAction } from "@lowcoder-ee/comps/comps/tourComp/componentSelectorControl"; + +const OptionTypes = [ + { + label: trans("prop.manual"), + value: "manual", + }, + { + label: trans("prop.map"), + value: "map", + }, +] as const; + +// All options must contain label +type OptionChildType = { label: InstanceType }; +type TourControlType = new (params: CompParams) => MultiBaseComp< + OptionChildType, + any, + any +> & + Comp; +type OptionControlParam = { + // list title + title?: string; + // The new option's label name + newOptionLabel?: string; +}; + +type OptionPropertyParam = { + autoMap?: boolean; +}; + +interface TourStepCompProperty { + propertyView(param: OptionPropertyParam): React.ReactNode; +} + +function hasPropertyView(comp: any): comp is TourStepCompProperty { + return !!(comp as any).propertyView; +} + +// Add dataIndex to each comp, required for drag and drop sorting +function withDataIndex(VariantComp: T) { + // @ts-ignore + class WithDataIndexComp extends VariantComp { + dataIndex: string = genRandomKey(); + + getDataIndex() { + return this.dataIndex; + } + } + + return WithDataIndexComp as new ( + params: CompParams> + ) => WithDataIndexComp; +} + +// Deduplication, the same value takes the first one +function distinctValue>(data: T[], uniqField: keyof T) { + if (!data || data.length <= 0) { + return data; + } + const result: T[] = []; + data.reduce((uniqValSet, item) => { + const uniqVal = item[uniqField]; + if (!uniqValSet.has(uniqVal)) { + result.push(item); + } + uniqValSet.add(uniqVal); + return uniqValSet; + }, new Set()); + return result; +} + +type PickNumberFields = { + [key in keyof T]: T[key] extends number ? T[key] : never; +}; + +// Manually add options +export function manualTourStepsControl( + VariantComp: T, + config: { + // init value + initOptions?: ConstructorToDataType[]; + // Unique value field, used to deduplicate + uniqField?: keyof ConstructorToView; + // auto-increment field + autoIncField?: keyof PickNumberFields>; + } +) { + type OptionDataType = ConstructorToDataType; + const ManualComp = list(withDataIndex(VariantComp)); + const TmpManualOptionControl = new MultiCompBuilder( + { + manual: ManualComp, + }, + (props) => { + const view = props.manual.map((m) => m.getView()); + return config.uniqField ? distinctValue(view, config.uniqField) : view; + } + ) + .setPropertyViewFn(() => { + throw new Error("Method not implemented."); + }) + .build(); + + class ManualOptionControl extends TmpManualOptionControl { + exposingNode() { + return withFunction( + fromRecord( + mapValues(this.children.manual.children, (c1) => + fromRecord(mapValues(c1.children, (c2) => c2.exposingNode())) + ) + ), + (params) => Object.values(params) + ); + } + + private getNewId(): number { + const { autoIncField } = config; + if (!autoIncField) return 0; + const view = this.children.manual.getView().map((m) => m.getView()); + const ids = new Set(view.map((tab) => tab[autoIncField])); + let id = 0; + while (ids.has(id)) ++id; + return id; + } + + propertyView(param: OptionControlParam) { + const manualComp = this.children.manual; + const { autoIncField } = config; + const title = param.title ?? trans("optionsControl.optionList"); + + return controlItem( + { filterText: title }, +