diff --git a/client/packages/lowcoder-core/lib/index.d.ts b/client/packages/lowcoder-core/lib/index.d.ts index b29a693f4..8b90f47e0 100644 --- a/client/packages/lowcoder-core/lib/index.d.ts +++ b/client/packages/lowcoder-core/lib/index.d.ts @@ -1,5 +1,5 @@ /// -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; type EvalMethods = Record>; type CodeType = undefined | "JSON" | "Function" | "PureJSON"; @@ -613,6 +613,7 @@ declare abstract class MultiBaseComp ChildrenType[typeof childName]>): CompAction; + getRef(): React.RefObject; } declare function mergeExtra(e1: ExtraNodeType | undefined, e2: ExtraNodeType): ExtraNodeType; diff --git a/client/packages/lowcoder-design/src/icons/index.ts b/client/packages/lowcoder-design/src/icons/index.ts index 138c14b42..e35041780 100644 --- a/client/packages/lowcoder-design/src/icons/index.ts +++ b/client/packages/lowcoder-design/src/icons/index.ts @@ -106,6 +106,7 @@ export { ReactComponent as TextCompIcon } from "./icon-text-display.svg"; export { ReactComponent as SwitchCompIcon } from "./icon-switch.svg"; export { ReactComponent as TableCompIcon } from "./icon-table-comp.svg"; export { ReactComponent as SelectCompIcon } from "./icon-insert-select.svg"; +export { ReactComponent as IconModal } from "./icon-modal.svg"; export { ReactComponent as CheckboxCompIcon } from "./icon-checkboxes.svg"; export { ReactComponent as RadioCompIcon } from "./icon-radio.svg"; export { ReactComponent as TimeCompIcon } from "./icon-time.svg"; 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..ce5b4c7ab --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tourComp/tourComp.tsx @@ -0,0 +1,95 @@ +import { trans } from "i18n"; +import { + CommonNameConfig, + MultiBaseComp, + NameConfig, + stringExposingStateControl, + UICompBuilder, + withExposingConfigs, + withMethodExposing +} from "lowcoder-sdk"; +import { TourChildrenMap, TourPropertyView } from "./tourPropertyView"; +import { Tour, TourProps } from "antd"; +import React, { useContext } from "react"; +import { EditorContext } from "@lowcoder-ee/comps/editorState"; +import { GridItemComp } from "@lowcoder-ee/comps/comps/gridItemComp"; +import { HookComp } from "@lowcoder-ee/comps/hooks/hookComp"; +import { TemporaryStateItemComp } from "@lowcoder-ee/comps/comps/temporaryStateComp"; + +/** + * This component builds the Property Panel and the fake 'UI' for the Tour component + */ +let TourBasicComp = (function() { + const childrenMap = { + ...TourChildrenMap, + defaultValue: stringExposingStateControl("defaultValue"), + value: stringExposingStateControl("value") + // style: styleControl(SelectStyle), + }; + return new UICompBuilder(childrenMap, (props, dispatch) => { + const editorState = useContext(EditorContext); + const compMap: (GridItemComp | HookComp | InstanceType)[] = Object.values(editorState.getAllUICompMap()); + + const steps: TourProps["steps"] = props.options.map((step) => { + const targetName = step.target; + let target = undefined; + const compListItem = compMap.find((compItem) => compItem.children.name.getView() === targetName); + if (compListItem) { + console.log(`setting selected comp to ${compListItem}`); + try { + target = ((compListItem as MultiBaseComp).children.comp as GridItemComp).getRef?.(); + } catch (e) { + target = ((compListItem as MultiBaseComp).children.comp as HookComp).getRef?.(); + } + } + + return { + /** + * I'm pretty sure it's safe to use dangerouslySetInnerHTML here as any creator of an app + * will have unrestricted access to the data of any user anyway. E.g. have a button that + * just sends the current cookies wherever, thus the developer of the app must be trusted + * in all cases + * This even applies to things like , because the + * app creator might desire functionality like this. + */ + title: (
), + description: (
), + target: target?.current, + arrow: step.arrow, + placement: step.placement === "" ? undefined : step.placement, + mask: step.mask, + cover: step.cover ? () : undefined, + type: step.type === "" ? undefined : step.type, + }; + }); + + return ( + 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")}
+
    +
  • center
  • +
+
+); + +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 }, +