props.open.onChange(false)}
+ // indicatorsRender={(current, total) => props.indicatorsRender(current, total)} // todo enable later
+ disabledInteraction={props.disabledInteraction}
+ arrow={props.arrow}
+ placement={props.placement === "" ? undefined : props.placement}
+ type={props.type === "" ? undefined : props.type}
+ mask={props.mask}
+ />
+ );
+ })
+ .setPropertyViewFn((children) => )
+ .build();
+})();
+
+export const TourComp = withMethodExposing(TourBasicComp, [
+ {
+ method: {
+ name: "startTour",
+ description: "Triggers the tour to start",
+ params: []
+ },
+ execute: (comp, values) => {
+ comp.children.open.getView().onChange(true);
+ }
+ }
+]);
diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/tourControlConstants.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/tourControlConstants.tsx
new file mode 100644
index 000000000..418e44c5e
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourControlConstants.tsx
@@ -0,0 +1,27 @@
+export type PlacementType = 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom' | 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'center' | '';
+export type TourStepType = 'default' | 'primary' | '';
+
+export const PlacementOptions: {label: string, value: PlacementType}[] = [
+ { label: "", value: ""},
+ { label: "Center", value: "center"},
+ { label: "Left", value: "left"},
+ { label: "Left Top", value: "leftTop"},
+ { label: "Left Bottom", value: "leftBottom"},
+ { label: "Right", value: "right"},
+ { label: "Right Top", value: "rightTop"},
+ { label: "Right Bottom", value: "rightBottom"},
+ { label: "Top", value: "top"},
+ { label: "Top Left", value: "topLeft"},
+ { label: "Top Right", value: "topRight"},
+ { label: "Bottom", value: "bottom"},
+ { label: "Bottom Left", value: "bottomLeft"},
+ { label: "Bottom Right", value: "bottomRight"},
+];
+
+export const TypeOptions: {label: string, value: TourStepType}[] = [
+ { label: "", value: ""},
+ { label: "Default", value: "default"},
+ { label: "Primary", value: "primary"},
+];
+
+export {};
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/tourPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/tourPropertyView.tsx
new file mode 100644
index 000000000..4def9a3f1
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourPropertyView.tsx
@@ -0,0 +1,77 @@
+import { RecordConstructorToComp } from "lowcoder-core";
+import { BoolControl } from "../../controls/boolControl";
+import { ArrowControl, BoolCodeControl, MaskControl } from "../../controls/codeControl";
+import { Section } from "lowcoder-design";
+import { TourStepControl } from "@lowcoder-ee/comps/controls/tourStepControl";
+import { booleanExposingStateControl, dropdownControl } from "lowcoder-sdk";
+import { trans } from "i18n";
+import { PlacementOptions, TypeOptions } from "@lowcoder-ee/comps/comps/tourComp/tourControlConstants";
+import {
+ TourArrowTooltip,
+ TourMaskTooltip,
+ TourPlacementTooltip
+} from "@lowcoder-ee/comps/comps/tourComp/tourTooltips";
+
+export const TourChildrenMap = {
+ open: booleanExposingStateControl("open"),
+ options: TourStepControl,
+ // indicatorsRender: AlkjdfControl, // todo get this working later
+ disabledInteraction: BoolControl,
+ mask: MaskControl,
+ placement: dropdownControl(PlacementOptions, "bottom"),
+ arrow: ArrowControl,
+ type: dropdownControl(TypeOptions, "default"),
+};
+
+export const TourPropertyView = (
+ children: RecordConstructorToComp<
+ typeof TourChildrenMap & {
+ hidden: typeof BoolCodeControl;
+ }
+ > //& {
+ // style: { getPropertyView: () => ControlNode };
+ // }
+) => (
+ <>
+
+ {children.options.propertyView({})}
+
+
+
+ {/*{children.indicatorsRender.propertyView({*/}
+ {/* label: trans("tour.indicatorsRender.label"),*/}
+ {/* tooltip: IndicatorsRenderTooltip,*/}
+ {/*})}*/}
+ {children.disabledInteraction.propertyView({
+ label: trans("tour.disabledInteraction.label"),
+ tooltip: trans("tour.disabledInteraction.tooltip")
+ })}
+ {children.mask.propertyView({
+ label: trans("tour.mask.label"),
+ tooltip: TourMaskTooltip,
+ })}
+ {children.placement.propertyView({
+ label: trans("tour.placement.label"),
+ tooltip: TourPlacementTooltip,
+ radioButton: false
+ })}
+ {children.arrow.propertyView({
+ label: trans("tour.arrow.label"),
+ tooltip: TourArrowTooltip,
+ })}
+ {children.type.propertyView({
+ label: trans("tour.type.label"),
+ tooltip: trans("tour.type.tooltip")
+ })}
+
+
+ {/*{["layout", "both"].includes(*/}
+ {/* useContext(EditorContext).editorModeStatus*/}
+ {/*) && (*/}
+ {/* */}
+ {/* {children.style.getPropertyView()}*/}
+ {/* */}
+ {/*)}*/}
+ >
+);
+
diff --git a/client/packages/lowcoder/src/comps/comps/tourComp/tourTooltips.tsx b/client/packages/lowcoder/src/comps/comps/tourComp/tourTooltips.tsx
new file mode 100644
index 000000000..63147c6ce
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourTooltips.tsx
@@ -0,0 +1,121 @@
+import { trans } from "@lowcoder-ee/i18n";
+
+const indicatorsRenderExample = `(current, total) => (
+
+ {current + 1} / {total}
+
+)`;
+export const IndicatorsRenderTooltip = (
+
+ {trans("tour.indicatorsRender.tooltip")}
+
+
+ {trans("tour.indicatorsRender.tooltipValidTypes")}
+
+
+
{trans("tour.tooltipSignatureHeader")}
+
+ {trans("tour.indicatorsRender.tooltipFunctionSignature")}
+
+
+
+ {trans("tour.tooltipExampleHeader")}
+
+ {indicatorsRenderExample}
+
+
+);
+
+let styleExample = {
+ "style": { "boxShadow": "inset 0 0 15px #fff" },
+ "color": "rgba(40, 0, 255, .4)"
+};
+
+export const TourStepMaskTooltip = (
+
+ {trans("tour.options.mask.tooltip")}
+
+
+ {trans("tour.options.mask.tooltipValidTypes")}
+
+
+
Example:
+
+ {JSON.stringify(styleExample, null, 1)}
+
+
+);
+
+export const TourMaskTooltip = (
+
+ {trans("tour.mask.tooltip")}
+
+
+ {trans("tour.mask.tooltipValidTypes")}
+
+
+
Example:
+
+ {JSON.stringify(styleExample, null, 1)}
+
+
+);
+
+export const TourPlacementTooltip = (
+
+ {trans("tour.placement.tooltip")}
+
+
+
{trans("tour.placement.tooltipValidOptions")}
+
{trans("tour.placement.tooltipValidOptionsAbove")}
+
+ topLeft
+ top
+ topRight
+
+
{trans("tour.placement.tooltipValidOptionsLeft")}
+
+ leftTop
+ left
+ leftBottom
+
+
{trans("tour.placement.tooltipValidOptionsRight")}
+
+ rightTop
+ right
+ rightBottom
+
+
{trans("tour.placement.tooltipValidOptionsBelow")}
+
+ bottomLeft
+ bottom
+ bottomRight
+
+
{trans("tour.placement.tooltipValidOptionsOnTop")}
+
+
+);
+
+const arrowTooltipSignature = `boolean | { pointAtCenter: boolean }`;
+export const TourStepArrowTooltip = (
+
+ {trans("tour.options.arrow.tooltip")}
+
+
+
{trans("tour.tooltipSignatureHeader")}
+ {arrowTooltipSignature}
+
+);
+export const TourArrowTooltip = (
+
+ {trans("tour.arrow.tooltip")}
+
+
+
{trans("tour.tooltipSignatureHeader")}
+ {arrowTooltipSignature}
+
+);
+
+export {};
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx b/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx
index a14adb207..239602c50 100644
--- a/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx
+++ b/client/packages/lowcoder/src/comps/controls/actionSelector/executeCompAction.tsx
@@ -1,5 +1,4 @@
-import { customAction, routeByNameAction } from "lowcoder-core";
-import { CompParams, ConstructorToDataType } from "lowcoder-core";
+import { CompParams, ConstructorToDataType, customAction, routeByNameAction } from "lowcoder-core";
import { GridItemComp } from "comps/comps/gridItemComp";
import { SimpleNameComp } from "comps/comps/simpleNameComp";
import { TemporaryStateItemComp } from "comps/comps/temporaryStateComp";
diff --git a/client/packages/lowcoder/src/comps/controls/codeControl.tsx b/client/packages/lowcoder/src/comps/controls/codeControl.tsx
index 480c62398..cc261cc8c 100644
--- a/client/packages/lowcoder/src/comps/controls/codeControl.tsx
+++ b/client/packages/lowcoder/src/comps/controls/codeControl.tsx
@@ -30,11 +30,11 @@ import {
toHex,
wrapperToControlItem,
} from "lowcoder-design";
-import { lazy, ReactNode, Suspense } from "react";
+import { CSSProperties, lazy, ReactNode, Suspense } from "react";
import {
showTransform,
toArrayJSONObject,
- toBoolean,
+ toBoolean, toBooleanOrCss, toBooleanOrJsonObject,
toJSONArray,
toJSONObject,
toJSONObjectArray,
@@ -318,6 +318,8 @@ export type CodeControlJSONType = ReturnType;
export const StringControl = codeControl(toString);
export const NumberControl = codeControl(toNumber);
export const StringOrNumberControl = codeControl(toStringOrNumber);
+export const MaskControl = codeControl(toBooleanOrCss);
+export const ArrowControl = codeControl(toBooleanOrJsonObject);
// rangeCheck, don't support Infinity temporarily
export class RangeControl {
@@ -506,6 +508,16 @@ export const FunctionControl = codeControl(
{ codeType: "Function", evalWithMethods: true }
);
+// export const AlkjdfControl = codeControl<(current: number, total: number)=>ReactNode>(
+// (value) => {
+// if (typeof value === "function") {
+// return value;
+// }
+// return (current,total) => (value as (current: number, total: number)=>ReactNode)(current,total);
+// },
+// { codeType: "Function", evalWithMethods: true }
+// );
+
export const TransformerCodeControl = codeControl(
(value) => {
if (typeof value === "function") {
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..86e10f337
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/controls/tourStepControl.tsx
@@ -0,0 +1,427 @@
+import { ArrowControl, BoolCodeControl, MaskControl, StringControl } from "comps/controls/codeControl";
+import { dropdownControl } from "comps/controls/dropdownControl";
+import { MultiCompBuilder, withDefault } from "comps/generators";
+import { list } from "comps/generators/list";
+import { ToViewReturn } from "comps/generators/multi";
+import { genRandomKey } from "comps/utils/idGenerator";
+import { hiddenPropertyView } from "comps/utils/propertyUtils";
+import { trans } from "i18n";
+import { mapValues } from "lodash";
+import {
+ Comp,
+ CompParams,
+ ConstructorToDataType,
+ ConstructorToView,
+ fromRecord,
+ MultiBaseComp,
+ SimpleAbstractComp,
+ withFunction
+} from "lowcoder-core";
+import { controlItem, Dropdown, Option, OptionsType, ValueFromOption } from "lowcoder-design";
+import { getNextEntityName } from "util/stringUtils";
+import { BoolControl, ControlParams } from "lowcoder-sdk";
+import { ReactNode, useContext, useEffect, useState } from "react";
+import { EditorContext, EditorState } from "@lowcoder-ee/comps/editorState";
+import { PlacementOptions, TypeOptions } from "@lowcoder-ee/comps/comps/tourComp/tourControlConstants";
+import {
+ TourPlacementTooltip,
+ TourStepArrowTooltip,
+ TourStepMaskTooltip
+} from "@lowcoder-ee/comps/comps/tourComp/tourTooltips";
+
+// All options must contain label
+type OptionChildType = { label: InstanceType };
+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("tour.section1Subtitle");
+
+ return controlItem(
+ { filterText: title },
+