diff --git a/client/packages/lowcoder-design/src/components/shapeSelect/index.tsx b/client/packages/lowcoder-design/src/components/shapeSelect/index.tsx new file mode 100644 index 000000000..a4a71964b --- /dev/null +++ b/client/packages/lowcoder-design/src/components/shapeSelect/index.tsx @@ -0,0 +1,487 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { IconDefinition } from "@fortawesome/free-regular-svg-icons"; +import { default as Popover } from "antd/lib/popover"; +import type { ActionType } from "@rc-component/trigger/lib/interface"; +import { TacoInput } from "components/tacoInput"; +import { Tooltip } from "components/toolTip"; +import { trans } from "i18n/design"; +import { upperFirst, sortBy } from "lodash"; +import { shapes } from "coolshapes-react"; +import { Coolshape } from "coolshapes-react"; + +import { + ReactNode, + useEffect, + useCallback, + useMemo, + useRef, + useState, + Suspense, +} from "react"; +import Draggable from "react-draggable"; +import { + default as List, + type ListRowProps, +} from "react-virtualized/dist/es/List"; +import styled from "styled-components"; +import { CloseIcon, SearchIcon } from "icons"; +import { ANTDICON } from "icons/antIcon"; +import { JSX } from "react/jsx-runtime"; + +const PopupContainer = styled.div` + width: 580px; + background: #ffffff; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); + border-radius: 8px; + box-sizing: border-box; +`; +const TitleDiv = styled.div` + height: 48px; + display: flex; + align-items: center; + padding: 0 16px; + justify-content: space-between; + user-select: none; +`; +const TitleText = styled.span` + font-size: 16px; + color: #222222; + line-height: 16px; +`; +const StyledCloseIcon = styled(CloseIcon)` + width: 16px; + height: 16px; + cursor: pointer; + color: #8b8fa3; + + &:hover g line { + stroke: #222222; + } +`; + +const SearchDiv = styled.div` + position: relative; + margin: 0px 16px; + padding-bottom: 8px; + display: flex; + justify-content: space-between; +`; +const StyledSearchIcon = styled(SearchIcon)` + position: absolute; + top: 6px; + left: 12px; +`; +const IconListWrapper = styled.div` + padding-left: 10px; + padding-right: 4px; + .gtujLP { + overflow: hidden; + } + .iconsDiv div { + float: left; + } +`; +const IconList = styled(List)` + scrollbar-gutter: stable; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + border-radius: 9999px; + background-color: rgba(139, 143, 163, 0.2); + } + + &::-webkit-scrollbar-thumb:hover { + background-color: rgba(139, 143, 163, 0.36); + } +`; + +const IconRow = styled.div` + padding: 0 6px; + display: flex; + align-items: flex-start; /* Align items to the start to allow different heights */ + justify-content: space-between; + + &:last-child { + gap: 8px; + justify-content: flex-start; + } +`; + +const IconItemContainer = styled.div` + width: 60px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + cursor: pointer; + font-size: 28px; + margin-bottom: 24px; + + &:hover { + border: 1px solid #315efb; + border-radius: 4px; + } + + &:focus { + border: 1px solid #315efb; + border-radius: 4px; + box-shadow: 0 0 0 2px #d6e4ff; + } +`; + +const IconWrapper = styled.div` + height: auto; + display: flex; + align-items: center; + justify-content: center; +`; + +const IconKeyDisplay = styled.div` + font-size: 8px; + color: #8b8fa3; + margin-top: 4px; /* Space between the icon and the text */ + text-align: center; + word-wrap: break-word; /* Ensure text wraps */ + width: 100%; /* Ensure the container can grow */ +`; + +class Icon { + readonly title: string; + constructor( + readonly def: IconDefinition | any, + readonly names: string[] + ) { + if (def?.iconName) { + this.title = def.iconName.split("-").map(upperFirst).join(" "); + } else { + this.title = names[0].slice(5); + this.def = def; + } + } + getView() { + if (this.names[0]?.startsWith("antd/")) return this.def; + else + return ( + + ); + } +} + +let allIcons: Record | undefined = undefined; + +function getAllIcons() { + if (allIcons !== undefined) { + return allIcons; + } + // console.log(shapes); + // const [{ far }, { fas }] = await Promise.all([ + // import("@fortawesome/free-regular-svg-icons"), + // import("@fortawesome/free-solid-svg-icons"), + // // import("@fontawesome/free-brands-svg-icons"), + // ]); + + const ret: Record = {}; + // for (const [type, pack] of Object.entries({ solid: fas, regular: far })) { + // const list = Object.entries(pack); + // // console.log("list of icons ", list); + + // for (const [k, def] of list) { + // ret[type + "/" + def.iconName] = new Icon(def, [def.iconName]); + // } + // for (const [k, def] of list) { + // const name = k.startsWith("fa") ? k.slice(2) : k; + // ret[type + "/" + def.iconName].names.push(name); + // // for compatibility of old data + // const key = type + "/" + name; + // if (ret[key] === undefined) { + // ret[key] = new Icon(def, []); + // } + // } + // } + //append ant icon + // for (let key of Object.keys(shapes: any)) { + // // ret["antd/" + key] = new Icon( + // // ANTDICON[key.toLowerCase() as keyof typeof ANTDICON], + // // ["antd/" + key] + // // ); + // // ret[key + "/"].names.push("test"); + // ret[key] = shapes[key].map((Shape: JSX.IntrinsicAttributes, index: any) => { + // return ; + // }); + // } + allIcons = ret; + console.log(ret); + + return ret; + + // { + // let all = Object.keys(shapes).map((shapeType: any, _) => { + // return shapes[shapeType].map((Shape: JSX.IntrinsicAttributes, index: any) => { + // return ; + // }); + // }); + // console.log(all); + + // } + + // const [{ far }, { fas }] = await Promise.all([ + // import("@fortawesome/free-regular-svg-icons"), + // import("@fortawesome/free-solid-svg-icons"), + // // import("@fontawesome/free-brands-svg-icons"), + // ]); + + // const ret: Record = {}; + // for (const [type, pack] of Object.entries({ solid: fas, regular: far })) { + // const list = Object.entries(pack); + // // console.log("list of icons ", list); + + // for (const [k, def] of list) { + // ret[type + "/" + def.iconName] = new Icon(def, [def.iconName]); + // } + // for (const [k, def] of list) { + // const name = k.startsWith("fa") ? k.slice(2) : k; + // ret[type + "/" + def.iconName].names.push(name); + // // for compatibility of old data + // const key = type + "/" + name; + // if (ret[key] === undefined) { + // ret[key] = new Icon(def, []); + // } + // } + // } + // //append ant icon + // for (let key of Object.keys(ANTDICON)) { + // ret["antd/" + key] = new Icon( + // ANTDICON[key.toLowerCase() as keyof typeof ANTDICON], + // ["antd/" + key] + // ); + // } + // allIcons = ret; + // console.log(ret); + + // return ret; +} + +export const sharePrefix = "/icon:"; + +export function removeShapeQuote(value?: string) { + return value + ? value.startsWith('"') && value.endsWith('"') + ? value.slice(1, -1) + : value + : ""; +} + +function getIconKey(value?: string) { + const v = removeShapeQuote(value); + return v.startsWith(sharePrefix) ? v.slice(sharePrefix.length) : ""; +} + +export function useShape(value?: string) { + const key = getIconKey(value); + const [icon, setIcon] = useState(undefined); + useEffect(() => { + let allshapes = getAllIcons(); + setIcon(allshapes[key]); + }, [key]); + return icon; +} + +function search( + allIcons: Record, + searchText: string, + searchKeywords?: Record, + IconType?: "OnlyAntd" | "All" | "default" | undefined +) { + const tokens = searchText + .toLowerCase() + .split(/\s+/g) + .filter((t) => t); + return sortBy( + Object.entries(allIcons).filter(([key, icon]) => { + if (icon.names.length === 0) { + return false; + } + if (IconType === "OnlyAntd" && !key.startsWith("antd/")) return false; + if (IconType === "default" && key.startsWith("antd/")) return false; + let text = icon.names + .flatMap((name) => [name, searchKeywords?.[name]]) + .filter((t) => t) + .join(" "); + text = (icon.title + " " + text).toLowerCase(); + return tokens.every((t) => text.includes(t)); + }), + ([key, icon]) => icon.title + ); +} + +const IconPopup = (props: { + onChange: (value: string) => void; + label?: ReactNode; + onClose: () => void; + searchKeywords?: Record; + IconType?: "OnlyAntd" | "All" | "default" | undefined; +}) => { + const [allIcons, setAllIcons] = useState>({}); + const onChangeRef = useRef(props.onChange); + onChangeRef.current = props.onChange; + const onChangeIcon = useCallback( + (key: string) => onChangeRef.current(key), + [] + ); + + useEffect(() => { + let shapes = getAllIcons(); + console.log("shapes ", shapes); + + setAllIcons(shapes); + }, []); + + // const rowRenderer = useCallback( + // (p: ListRowProps) => ( + // + // {searchResults + // .slice(p.index * columnNum, (p.index + 1) * columnNum) + // .map(([key, icon]) => ( + // + // { + // onChangeIcon(key); + // }} + // > + // + // {icon.getView()} + // + // {key} + // + // + // ))} + // + // ), + // [searchResults, allIcons, onChangeIcon] + // ); + return ( + + + + {trans("shapeSelect.title")} + + + {/* + setSearchText(e.target.value)} + placeholder={trans("shapeSelect.searchPlaceholder")} + /> + + */} + + <> + {Object.keys(shapes).map((shapeType: string, _i: number) => { + return shapes[shapeType as keyof typeof shapes].map( + (Shape: any, index: any) => { + return ( +
{ + onChangeIcon(index + "_" + shapeType); + }} + > + { + onChangeIcon(index + "_" + shapeType); + }} + /> +

{shapeType}

+
+ ); + } + ); + })} + +
+
+
+ ); +}; + +export const ShapeSelectBase = (props: { + onChange: (value: string) => void; + label?: ReactNode; + children?: ReactNode; + visible?: boolean; + setVisible?: (v: boolean) => void; + trigger?: ActionType; + leftOffset?: number; + parent?: HTMLElement | null; + searchKeywords?: Record; + IconType?: "OnlyAntd" | "All" | "default" | undefined; +}) => { + const { setVisible, parent } = props; + return ( + parent : undefined} + // hide the original background when dragging the popover is allowed + overlayInnerStyle={{ + border: "none", + boxShadow: "none", + background: "transparent", + }} + // when dragging is allowed, always re-location to avoid the popover exceeds the screen + destroyTooltipOnHide + content={ + setVisible?.(false)} + searchKeywords={props.searchKeywords} + IconType={props.IconType} + /> + } + > + {props.children} + + ); +}; + +export const ShapeSelect = (props: { + onChange: (value: string) => void; + label?: ReactNode; + children?: ReactNode; + searchKeywords?: Record; + IconType?: "OnlyAntd" | "All" | "default" | undefined; +}) => { + const [visible, setVisible] = useState(false); + return ( + + ); +}; diff --git a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts index acd565c68..b15621be1 100644 --- a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts +++ b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts @@ -23,7 +23,7 @@ export const en = { advanced: "Advanced", validation: "Validation", layout: "Layout", - labelStyle:"Label Style", + labelStyle: "Label Style", style: "Style", meetings: "Meeting Settings", data: "Data", @@ -54,6 +54,10 @@ export const en = { title: "Select icon", searchPlaceholder: "Search icon", }, + shapeSelect: { + title: "Select shape", + searchPlaceholder: "Search shape", + }, eventHandler: { advanced: "Advanced", }, diff --git a/client/packages/lowcoder-design/src/index.ts b/client/packages/lowcoder-design/src/index.ts index 92a05fb78..9298929c5 100644 --- a/client/packages/lowcoder-design/src/index.ts +++ b/client/packages/lowcoder-design/src/index.ts @@ -47,6 +47,7 @@ export * from "./components/tacoInput"; export * from "./components/tacoPagination"; export * from "./components/toolTip"; export * from "./components/video"; +export * from "./components/shapeSelect"; export * from "./icons"; diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index 23f997f97..e09d50a30 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -41,6 +41,7 @@ "buffer": "^6.0.3", "clsx": "^2.0.0", "cnchar": "^3.2.4", + "coolshapes-react": "lowcoder-org/coolshapes-react", "copy-to-clipboard": "^3.3.3", "core-js": "^3.25.2", "echarts": "^5.4.3", @@ -129,4 +130,4 @@ "vite-plugin-svgr": "^2.2.2", "vite-tsconfig-paths": "^3.6.0" } -} +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/shapeComp/shapeComp.tsx b/client/packages/lowcoder/src/comps/comps/shapeComp/shapeComp.tsx new file mode 100644 index 000000000..7d45c57eb --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/shapeComp/shapeComp.tsx @@ -0,0 +1,159 @@ +import { CompParams } from "lowcoder-core"; +import { ToDataType } from "comps/generators/multi"; +import { + NameConfigHidden, + withExposingConfigs, +} from "comps/generators/withExposing"; +import { NameGenerator } from "comps/utils/nameGenerator"; +import { Section, sectionNames } from "lowcoder-design"; +import { oldContainerParamsToNew } from "../containerBase"; +import { toSimpleContainerData } from "../containerBase/simpleContainerComp"; +import { ShapeTriContainer } from "./shapeTriContainer"; +import { ShapeControl } from "comps/controls/shapeControl"; +import { withDefault } from "../../generators"; +import { + ContainerChildren, + ContainerCompBuilder, +} from "../triContainerComp/triContainerCompBuilder"; +import { + disabledPropertyView, + hiddenPropertyView, +} from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import { BoolCodeControl } from "comps/controls/codeControl"; +import { DisabledContext } from "comps/generators/uiCompBuilder"; +import React, { useContext, useEffect, useState } from "react"; +import { EditorContext } from "comps/editorState"; + +export const ContainerBaseComp = (function () { + const childrenMap = { + disabled: BoolCodeControl, + icon: withDefault(ShapeControl, ""), + }; + return new ContainerCompBuilder(childrenMap, (props, dispatch) => { + + + return ( + + + + ); + }) + .setPropertyViewFn((children) => { + return ( + <> +
+ {children.icon.propertyView({ + label: trans("iconComp.icon"), + IconType: "All", + })} +
+ {(useContext(EditorContext).editorModeStatus === "logic" || + useContext(EditorContext).editorModeStatus === "both") && ( +
+ {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
+ )} + + {(useContext(EditorContext).editorModeStatus === "layout" || + useContext(EditorContext).editorModeStatus === "both") && ( + <> +
+ {children.container.getPropertyView()} +
+
+ {children.container.stylePropertyView()} +
+ {children.container.children.showHeader.getView() && ( +
+ {children.container.headerStylePropertyView()} +
+ )} + {children.container.children.showBody.getView() && ( +
+ {children.container.bodyStylePropertyView()} +
+ )} + {children.container.children.showFooter.getView() && ( +
+ {children.container.footerStylePropertyView()} +
+ )} + + )} + + ); + }) + .build(); +})(); + +// Compatible with old data +function convertOldContainerParams(params: CompParams) { + // convert older params to old params + let tempParams = oldContainerParamsToNew(params); + + if (tempParams.value) { + const container = tempParams.value.container; + // old params + if ( + container && + (container.hasOwnProperty("layout") || container.hasOwnProperty("items")) + ) { + const autoHeight = tempParams.value.autoHeight; + const scrollbars = tempParams.value.scrollbars; + return { + ...tempParams, + value: { + container: { + showHeader: true, + body: { 0: { view: container } }, + showBody: true, + showFooter: false, + autoHeight: autoHeight, + scrollbars: scrollbars, + }, + }, + }; + } + } + return tempParams; +} + +class ContainerTmpComp extends ContainerBaseComp { + constructor(params: CompParams) { + super(convertOldContainerParams(params)); + } +} + +export const ShapeComp = withExposingConfigs(ContainerTmpComp, [NameConfigHidden]); + +type ContainerDataType = ToDataType>; + +export function defaultContainerData( + compName: string, + nameGenerator: NameGenerator +): ContainerDataType { + return { + container: { + header: toSimpleContainerData([ + { + item: { + compType: "text", + name: nameGenerator.genItemName("containerTitle"), + comp: { + text: "### " + trans("container.title"), + }, + }, + layoutItem: { + i: "", + h: 5, + w: 24, + x: 0, + y: 0, + }, + }, + ]), + }, + }; +} diff --git a/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx b/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx new file mode 100644 index 000000000..b7f396e05 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx @@ -0,0 +1,160 @@ +import { ContainerStyleType } from "comps/controls/styleControlConstants"; +import { EditorContext } from "comps/editorState"; +import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; +import { HintPlaceHolder, ScrollBar } from "lowcoder-design"; +import { ReactNode, useContext, useEffect, useState } from "react"; +import styled, { css } from "styled-components"; +import { checkIsMobile } from "util/commonUtils"; +import { + gridItemCompToGridItems, + InnerGrid, +} from "../containerComp/containerView"; +import { TriContainerViewProps } from "../triContainerComp/triContainerCompBuilder"; +import { Coolshape } from "coolshapes-react"; + +const getStyle = (style: ContainerStyleType) => { + return css` + border-color: ${style.border}; + border-width: ${style.borderWidth}; + border-radius: ${style.radius}; + overflow: hidden; + padding: ${style.padding}; + ${style.background && `background-color: ${style.background};`} + ${style.backgroundImage && `background-image: ${style.backgroundImage};`} + ${style.backgroundImageRepeat && + `background-repeat: ${style.backgroundImageRepeat};`} + ${style.backgroundImageSize && + `background-size: ${style.backgroundImageSize};`} + ${style.backgroundImagePosition && + `background-position: ${style.backgroundImagePosition};`} + ${style.backgroundImageOrigin && + `background-origin: ${style.backgroundImageOrigin};`} + `; +}; + +const Wrapper = styled.div<{ $style: ContainerStyleType }>` + display: flex; + flex-flow: column; + height: 100%; + border: 1px solid #d7d9e0; + border-radius: 4px; + ${(props) => props.$style && getStyle(props.$style)} +`; + +const BodyInnerGrid = styled(InnerGrid)<{ + $showBorder: boolean; + $backgroundColor: string; + $borderColor: string; + $borderWidth: string; + $backgroundImage: string; + $backgroundImageRepeat: string; + $backgroundImageSize: string; + $backgroundImagePosition: string; + $backgroundImageOrigin: string; +}>` + border-top: ${(props) => + `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; + flex: 1; + ${(props) => + props.$backgroundColor && `background-color: ${props.$backgroundColor};`} + border-radius: 0; + ${(props) => + props.$backgroundImage && `background-image: ${props.$backgroundImage};`} + ${(props) => + props.$backgroundImageRepeat && + `background-repeat: ${props.$backgroundImageRepeat};`} + ${(props) => + props.$backgroundImageSize && + `background-size: ${props.$backgroundImageSize};`} + ${(props) => + props.$backgroundImagePosition && + `background-position: ${props.$backgroundImagePosition};`} + ${(props) => + props.$backgroundImageOrigin && + `background-origin: ${props.$backgroundImageOrigin};`} +`; + +export type TriContainerProps = TriContainerViewProps & { + hintPlaceholder?: ReactNode; + icon: any; +}; + +export function ShapeTriContainer(props: TriContainerProps) { + const { container, icon } = props; + const { showHeader, showFooter } = container; + // When the header and footer are not displayed, the body must be displayed + const showBody = container.showBody || (!showHeader && !showFooter); + const scrollbars = container.scrollbars; + + const { items: headerItems, ...otherHeaderProps } = container.header; + const { items: bodyItems, ...otherBodyProps } = + container.body["0"].children.view.getView(); + const { items: footerItems, ...otherFooterProps } = container.footer; + const { style, headerStyle, bodyStyle, footerStyle } = container; + + const editorState = useContext(EditorContext); + const maxWidth = editorState.getAppSettings().maxWidth; + const isMobile = checkIsMobile(maxWidth); + const paddingWidth = isMobile ? 8 : 0; + + let [shape, setShape] = useState({ value: "star", index: 0 }); + useEffect(() => { + if (icon.props?.value) { + let shapeDetails = icon.props?.value; + setShape({ + index: parseInt(shapeDetails?.split("_")[0]), + value: shapeDetails?.split("_")[1], + }); + } + }, [icon.props]); + + return ( +
+ + + +
+ + +
+
+
+
+
+ ); +} diff --git a/client/packages/lowcoder/src/comps/controls/shapeControl.tsx b/client/packages/lowcoder/src/comps/controls/shapeControl.tsx new file mode 100644 index 000000000..fce1dd6b0 --- /dev/null +++ b/client/packages/lowcoder/src/comps/controls/shapeControl.tsx @@ -0,0 +1,297 @@ +import type { EditorState, EditorView } from "base/codeEditor/codeMirror"; +import { + iconRegexp, + iconWidgetClass, +} from "base/codeEditor/extensions/iconExtension"; +import { i18nObjs, trans } from "i18n"; +import { Coolshape } from "coolshapes-react"; +import { + AbstractComp, + CompAction, + CompActionTypes, + CompParams, + customAction, + DispatchType, + Node, + ValueAndMsg, +} from "lowcoder-core"; +import { + BlockGrayLabel, + controlItem, + ControlPropertyViewWrapper, + DeleteInputIcon, + iconPrefix, + ShapeSelect, + IconSelectBase, + removeQuote, + SwitchJsIcon, + SwitchWrapper, + TacoButton, + wrapperToControlItem, + useShape, +} from "lowcoder-design"; +import { ReactNode, useCallback, useState } from "react"; +import styled from "styled-components"; +import { setFieldsNoTypeCheck } from "util/objectUtils"; +import { StringControl } from "./codeControl"; +import { ControlParams } from "./controlParams"; + +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; +`; +const ButtonIconWrapper = styled.div` + display: flex; + font-size: 16px; +`; +const ButtonText = styled.div` + margin: 0 4px; + flex: 1; + width: 0px; + line-height: 20px; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; +`; +const StyledDeleteInputIcon = styled(DeleteInputIcon)` + margin-left: auto; + cursor: pointer; + + &:hover circle { + fill: #8b8fa3; + } +`; + +const StyledImage = styled.img` + height: 1em; + color: currentColor; +`; + +const Wrapper = styled.div` + > div:nth-of-type(1) { + margin-bottom: 4px; + } +`; + +const IconPicker = (props: { + value: string; + onChange: (value: string) => void; + label?: ReactNode; + IconType?: "OnlyAntd" | "All" | "default" | undefined; +}) => { + const icon = useShape(props.value); + console.log(props); + let shapeDetails = props.value; + console.log("shapeDetails ", shapeDetails); + + return ( + + + {props.value ? ( + + + + + + { + props.onChange(""); + e.stopPropagation(); + }} + /> + + ) : ( + + )} + + + ); +}; + +function onClickIcon(e: React.MouseEvent, v: EditorView) { + for (let t = e.target as HTMLElement | null; t; t = t.parentElement) { + if (t.classList.contains(iconWidgetClass)) { + const pos = v.posAtDOM(t); + const result = iconRegexp.exec(v.state.doc.sliceString(pos)); + if (result) { + const from = pos + result.index; + return { from, to: from + result[0].length }; + } + } + } +} + +function IconSpan(props: { value: string }) { + const icon = useShape(props.value); + return {icon?.getView() ?? props.value}; +} + +function cardRichContent(s: string) { + let result = s.match(iconRegexp); + if (result) { + const nodes: React.ReactNode[] = []; + let pos = 0; + for (const iconStr of result) { + const i = s.indexOf(iconStr, pos); + if (i >= 0) { + nodes.push(s.slice(pos, i)); + nodes.push(); + pos = i + iconStr.length; + } + } + nodes.push(s.slice(pos)); + return nodes; + } + return s; +} + +type Range = { + from: number; + to: number; +}; + + + +function isSelectValue(value: any) { + return !value || (typeof value === "string" && value.startsWith(iconPrefix)); +} + +type ChangeModeAction = { + useCodeEditor: boolean; +}; + +function ShapeControlView(props: { value: any }) { + const { value } = props; + console.log("ShapeControlView ", value); + const icon = useShape(value); + if (icon) { + return icon.getView(); + } + return ; +} + +export class ShapeControl extends AbstractComp< + ReactNode, + string, + Node> +> { + private readonly useCodeEditor: boolean; + private readonly codeControl: InstanceType; + + constructor(params: CompParams) { + super(params); + this.useCodeEditor = !isSelectValue(params.value); + this.codeControl = new StringControl(params); + } + + override getView(): ReactNode { + const value = this.codeControl.getView(); + return ; + } + + override getPropertyView(): ReactNode { + const value = this.codeControl.getView(); + return ; + } + + changeModeAction() { + return customAction( + { useCodeEditor: !this.useCodeEditor }, + true + ); + } + + propertyView(params: ControlParams) { + const jsContent = ( + this.dispatch(this.changeModeAction())} + /> + ); + return wrapperToControlItem( + + this.dispatchChangeValueAction(x)} + label={params.label} + IconType={params.IconType} + /> + + ); + } + + readonly IGNORABLE_DEFAULT_VALUE = ""; + override toJsonValue(): string { + if (this.useCodeEditor) { + return this.codeControl.toJsonValue(); + } + // in select mode, don't save editor's original value when saving + const v = removeQuote(this.codeControl.getView()); + return isSelectValue(v) ? v : ""; + } + + override reduce(action: CompAction): this { + switch (action.type) { + case CompActionTypes.CUSTOM: { + const useCodeEditor = (action.value as ChangeModeAction).useCodeEditor; + let codeControl = this.codeControl; + if (!this.useCodeEditor && useCodeEditor) { + // value should be transformed when switching to editor from select mode + const value = this.codeControl.toJsonValue(); + if (value && isSelectValue(value)) { + codeControl = codeControl.reduce( + codeControl.changeValueAction(`{{ "${value}" }}`) + ); + } + } + return setFieldsNoTypeCheck(this, { useCodeEditor, codeControl }); + } + case CompActionTypes.CHANGE_VALUE: { + const useCodeEditor = this.useCodeEditor + ? true + : !isSelectValue(action.value); + const codeControl = this.codeControl.reduce(action); + if ( + useCodeEditor !== this.useCodeEditor || + codeControl !== this.codeControl + ) { + return setFieldsNoTypeCheck(this, { useCodeEditor, codeControl }); + } + return this; + } + } + const codeControl = this.codeControl.reduce(action); + if (codeControl !== this.codeControl) { + return setFieldsNoTypeCheck(this, { codeControl }); + } + return this; + } + + override nodeWithoutCache() { + return this.codeControl.nodeWithoutCache(); + } + + exposingNode() { + return this.codeControl.exposingNode(); + } + + override changeDispatch(dispatch: DispatchType): this { + const result = setFieldsNoTypeCheck( + super.changeDispatch(dispatch), + { codeControl: this.codeControl.changeDispatch(dispatch) }, + { keepCacheKeys: ["node"] } + ); + return result; + } +} diff --git a/client/packages/lowcoder/src/comps/index-test.tsx b/client/packages/lowcoder/src/comps/index-test.tsx index 02c71382e..ed770ca67 100644 --- a/client/packages/lowcoder/src/comps/index-test.tsx +++ b/client/packages/lowcoder/src/comps/index-test.tsx @@ -7,7 +7,10 @@ import { ModalComp } from "comps/hooks/modalComp"; import { ButtonComp } from "./comps/buttonComp/buttonComp"; import { DropdownComp } from "./comps/buttonComp/dropdownComp"; import { LinkComp } from "./comps/buttonComp/linkComp"; -import { ContainerComp, defaultContainerData } from "./comps/containerComp/containerComp"; +import { + ContainerComp, + defaultContainerData, +} from "./comps/containerComp/containerComp"; import { CustomComp } from "./comps/customComp/customComp"; import { DatePickerComp, DateRangeComp } from "./comps/dateComp/dateComp"; import { DividerComp } from "./comps/dividerComp"; @@ -38,7 +41,12 @@ import { TextAreaComp } from "./comps/textInputComp/textAreaComp"; import { TimePickerComp, TimeRangeComp } from "./comps/dateComp/timeComp"; import { defaultFormData, FormComp } from "./comps/formComp/formComp"; import { IFrameComp } from "./comps/iframeComp"; -import { defaultGridData, defaultListViewData, GridComp, ListViewComp,} from "./comps/listViewComp"; +import { + defaultGridData, + defaultListViewData, + GridComp, + ListViewComp, +} from "./comps/listViewComp"; import { ModuleComp } from "./comps/moduleComp/moduleComp"; import { NavComp } from "./comps/navComp/navComp"; import { TableComp } from "./comps/tableComp"; @@ -63,7 +71,7 @@ import { TimeLineComp } from "./comps/timelineComp/timelineComp"; import { CommentComp } from "./comps/commentComp/commentComp"; import { MentionComp } from "./comps/textInputComp/mentionComp"; import { AutoCompleteComp } from "./comps/autoCompleteComp/autoCompleteComp"; -import { JsonLottieComp } from "./comps/jsonComp/jsonLottieComp"; +import { JsonLottieComp } from "./comps/jsonComp/jsonLottieComp"; import { ResponsiveLayoutComp } from "./comps/responsiveLayout"; import { ControlButton } from "./comps/meetingComp/controlButton"; @@ -145,7 +153,6 @@ const builtInRemoteComps: Omit = { var uiCompMap: Registry = { // Dashboards - chart: { name: trans("uiComp.chartCompName"), enName: "Chart", @@ -1121,7 +1128,7 @@ var uiCompMap: Registry = { }; export function loadComps() { - if(!uiCompMap) return; + if (!uiCompMap) return; const entries = Object.entries(uiCompMap); for (const [compType, manifest] of entries) { registerComp(compType as UICompType, manifest); diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 6927197fa..9fda66bad 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -1,5 +1,3 @@ -// import "comps/comps/layout/navLayout"; -// import "comps/comps/layout/mobileTabLayout"; import cnchar from "cnchar"; import { trans } from "i18n"; import { remoteComp } from "./comps/remoteComp/remoteComp"; @@ -111,6 +109,7 @@ import { GraphChartCompIcon } from "lowcoder-design"; +import { ShapeComp } from "./comps/shapeComp/shapeComp"; type Registry = { [key in UICompType]?: UICompManifest; @@ -125,8 +124,23 @@ const builtInRemoteComps: Omit = { export var uiCompMap: Registry = { // Dashboards - // charts + shape: { + name: trans("uiComp.shapeCompName"), + enName: "Shape", + description: trans("uiComp.shapeCompDesc"), + categories: ["dashboards"], + icon: ChartCompIcon, + keywords: trans("uiComp.shapeCompKeywords"), + lazyLoad: true, + compName: "ShapeComp", + compPath: "comps/shapeComp/shapeComp", + layoutInfo: { + w: 12, + h: 40, + }, + }, + // charts chart: { name: trans("uiComp.chartCompName") + " (legacy)", enName: "Chart", @@ -840,7 +854,6 @@ export var uiCompMap: Registry = { compPath: "comps/textInputComp/mentionComp", }, - // Forms form: { diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index 5bc1216f0..06faba379 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -114,6 +114,7 @@ export type UICompType = | "custom" | "jsonExplorer" | "jsonEditor" + | "shape" | "tree" | "treeSelect" | "audio" diff --git a/client/packages/lowcoder/src/constants/apiConstants.ts b/client/packages/lowcoder/src/constants/apiConstants.ts index 3b2f65bad..9f7b724d9 100644 --- a/client/packages/lowcoder/src/constants/apiConstants.ts +++ b/client/packages/lowcoder/src/constants/apiConstants.ts @@ -9,7 +9,7 @@ export const DEFAULT_TEST_DATA_SOURCE_TIMEOUT_MS = 30000; export const SHARE_TITLE = trans("share.title"); export enum API_STATUS_CODES { - SUCCESS = 200, + SUCCESS = 200, REQUEST_NOT_AUTHORISED = 401, SERVER_FORBIDDEN = 403, RESOURCE_NOT_FOUND = 404, diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index ac96506e2..735888f91 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1216,6 +1216,10 @@ export const en = { "basicChartCompDesc": "A versatile component for visualizing data through various types of charts and graphs.", "basicChartCompKeywords": "chart, graph, data, visualization", + "shapeCompName": "Shapes", + "shapeCompDesc": "A collection of geometric shapes for use with diagrams, illustrations, and visualizations.", + "shapeCompKeywords": "shapes, geometric, diagrams, illustrations", + // by mousheng "colorPickerCompName": "Color Picker", @@ -2085,6 +2089,11 @@ export const en = { "insertImage": "Insert an Image or " }, + "shapeControl": { + "selectShape": "Select an Shape", + "insertShape": "Insert an Shape", + "insertImage": "Insert an Image or ", + }, // twelfth part diff --git a/client/packages/lowcoder/src/i18n/locales/translation_files/en.json b/client/packages/lowcoder/src/i18n/locales/translation_files/en.json index c8cfbfb5d..088f531d6 100644 --- a/client/packages/lowcoder/src/i18n/locales/translation_files/en.json +++ b/client/packages/lowcoder/src/i18n/locales/translation_files/en.json @@ -861,6 +861,10 @@ "chartCompDesc": "A versatile component for visualizing data through various types of charts and graphs.", "chartCompKeywords": "chart, graph, data, visualization", + "shapeCompName": "Share", + "shapeCompDesc": "", + "shapeCompKeywords": "share", + "carouselCompName": "Image Carousel", "carouselCompDesc": "A rotating carousel component for showcasing images, banners, or content slides.", "carouselCompKeywords": "carousel, images, rotation, showcase", diff --git a/client/packages/lowcoder/src/index.sdk.ts b/client/packages/lowcoder/src/index.sdk.ts index fc39b85fa..1c4f68599 100644 --- a/client/packages/lowcoder/src/index.sdk.ts +++ b/client/packages/lowcoder/src/index.sdk.ts @@ -85,6 +85,7 @@ export * from "comps/controls/dropdownInputSimpleControl"; export * from "comps/controls/eventHandlerControl"; export * from "comps/controls/actionSelector/actionSelectorControl"; export * from "comps/controls/iconControl"; +export * from "comps/controls/shapeControl"; export * from "comps/controls/keyValueControl"; export * from "comps/controls/labelControl"; export * from "comps/controls/millisecondControl"; diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index c892f9690..39ef64679 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -207,4 +207,6 @@ export const CompStateIcon: { transfer: , card: , timer: , + + shape: , }; diff --git a/client/yarn.lock b/client/yarn.lock index b56c97b28..ce832f41b 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -7425,6 +7425,16 @@ __metadata: languageName: node linkType: hard +coolshapes-react@lowcoder-org/coolshapes-react: + version: 1.0.1 + resolution: "coolshapes-react@https://github.com/lowcoder-org/coolshapes-react.git#commit=0530e0e01feeba965286c1321f9c1cacb47bf587" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: e1b199f0325e87865cf6d89d4891ee84f4930196d21eed2356f4f2d2af8e748b8ad3889d1c9da2f5db6971913f75bd0f800b67a443ab7f4e972678031ce5c717 + languageName: node + linkType: hard + "copy-anything@npm:^2.0.1": version: 2.0.6 resolution: "copy-anything@npm:2.0.6" @@ -13665,6 +13675,7 @@ __metadata: buffer: ^6.0.3 clsx: ^2.0.0 cnchar: ^3.2.4 + coolshapes-react: lowcoder-org/coolshapes-react copy-to-clipboard: ^3.3.3 core-js: ^3.25.2 dotenv: ^16.0.3