diff --git a/client/packages/lowcoder-design/src/icons/index.tsx b/client/packages/lowcoder-design/src/icons/index.tsx index b033d52e92..9c00866feb 100644 --- a/client/packages/lowcoder-design/src/icons/index.tsx +++ b/client/packages/lowcoder-design/src/icons/index.tsx @@ -355,6 +355,7 @@ export { ReactComponent as VideoCameraStreamCompIconSmall } from "./v2/camera-st export { ReactComponent as VideoScreenshareCompIconSmall } from "./v2/screen-share-stream-s.svg"; // new export { ReactComponent as SignatureCompIconSmall } from "./v2/signature-s.svg"; export { ReactComponent as StepCompIconSmall } from "./v2/steps-s.svg"; +export { ReactComponent as TagsCompIconSmall } from "./v2/tags-s.svg" export { ReactComponent as CandlestickChartCompIconSmall } from "./v2/candlestick-chart-s.svg"; // new @@ -468,6 +469,7 @@ export { ReactComponent as SignatureCompIcon } from "./v2/signature-m.svg"; export { ReactComponent as GanttCompIcon } from "./v2/gantt-chart-m.svg"; export { ReactComponent as KanbanCompIconSmall } from "./v2/kanban-s.svg"; export { ReactComponent as KanbanCompIcon } from "./v2/kanban-m.svg"; +export { ReactComponent as TagsCompIcon } from "./v2/tags-l.svg"; export { ReactComponent as CandlestickChartCompIcon } from "./v2/candlestick-chart-m.svg"; export { ReactComponent as FunnelChartCompIcon } from "./v2/funnel-chart-m.svg"; diff --git a/client/packages/lowcoder-design/src/icons/v2/tags-l.svg b/client/packages/lowcoder-design/src/icons/v2/tags-l.svg new file mode 100644 index 0000000000..cd1d0368c3 --- /dev/null +++ b/client/packages/lowcoder-design/src/icons/v2/tags-l.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/client/packages/lowcoder-design/src/icons/v2/tags-s.svg b/client/packages/lowcoder-design/src/icons/v2/tags-s.svg new file mode 100644 index 0000000000..d45fcb0aa8 --- /dev/null +++ b/client/packages/lowcoder-design/src/icons/v2/tags-s.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx index 8380c56722..2527d57bd4 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx @@ -30,7 +30,7 @@ let MultiSelectBasicComp = (function () { padding: PaddingControl, }; return new UICompBuilder(childrenMap, (props, dispatch) => { - const valueSet = new Set(props.options.map((o) => o.value)); // Filter illegal default values entered by the user + const valueSet = new Set((props.options as any[]).map((o: any) => o.value)); // Filter illegal default values entered by the user const [ validateState, handleChange, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index eef8cad608..a2415b4a58 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -39,7 +39,7 @@ let SelectBasicComp = (function () { const propsRef = useRef>(props); propsRef.current = props; - const valueSet = new Set(props.options.map((o) => o.value)); // Filter illegal default values entered by the user + const valueSet = new Set((props.options as any[]).map((o: any) => o.value)); // Filter illegal default values entered by the user return props.label({ required: props.required, diff --git a/client/packages/lowcoder/src/comps/comps/tagsComp/tagsCompView.tsx b/client/packages/lowcoder/src/comps/comps/tagsComp/tagsCompView.tsx new file mode 100644 index 0000000000..f59898964c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tagsComp/tagsCompView.tsx @@ -0,0 +1,179 @@ +import { + BoolCodeControl, + ButtonEventHandlerControl, + InputLikeStyle, + NameConfig, + Section, + UICompBuilder, + hiddenPropertyView, + sectionNames, + showDataLoadingIndicatorsPropertyView, + styleControl, + withExposingConfigs +} from "@lowcoder-ee/index.sdk"; +import styled from "styled-components"; +import React, { useContext } from "react"; +import { trans } from "i18n"; +import { Tag } from "antd"; +import { EditorContext } from "comps/editorState"; +import { PresetStatusColorTypes } from "antd/es/_util/colors"; +import { hashToNum } from "util/stringUtils"; +import { TagsCompOptionsControl } from "comps/controls/optionsControl"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; + +const colors = PresetStatusColorTypes; + +// These functions are used for individual tag styling +function getTagColor(tagText : any, tagOptions: any[]) { + const foundOption = tagOptions.find((option: { label: any; }) => option.label === tagText); + if (foundOption) { + if (foundOption.colorType === "preset") { + return foundOption.presetColor; + } else if (foundOption.colorType === "custom") { + return undefined; + } + return foundOption.color; + } + const index = Math.abs(hashToNum(tagText)) % colors.length; + return colors[index]; +} + +const getTagStyle = (tagText: any, tagOptions: any[], baseStyle: any = {}) => { + const foundOption = tagOptions.find((option: { label: any; }) => option.label === tagText); + if (foundOption) { + const style: any = { ...baseStyle }; + + if (foundOption.colorType === "custom") { + style.backgroundColor = foundOption.color; + style.color = foundOption.textColor; + style.border = `1px solid ${foundOption.color}`; + } + + if (foundOption.border) { + style.borderColor = foundOption.border; + if (!foundOption.colorType || foundOption.colorType !== "custom") { + style.border = `1px solid ${foundOption.border}`; + } + } + + if (foundOption.radius) { + style.borderRadius = foundOption.radius; + } + + if (foundOption.margin) { + style.margin = foundOption.margin; + } + + if (foundOption.padding) { + style.padding = foundOption.padding; + } + + return style; + } + return baseStyle; +}; + +function getTagIcon(tagText: any, tagOptions: any[]) { + const foundOption = tagOptions.find(option => option.label === tagText); + return foundOption ? foundOption.icon : undefined; +} + +const multiTags = (function () { + + const StyledTag = styled(Tag)<{ $style: any, $bordered: boolean, $customStyle: any }>` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + background: ${(props) => props.$customStyle?.backgroundColor || props.$style?.background}; + color: ${(props) => props.$customStyle?.color || props.$style?.text}; + border-radius: ${(props) => props.$customStyle?.borderRadius || props.$style?.borderRadius}; + border: ${(props) => { + if (props.$customStyle?.border) return props.$customStyle.border; + return props.$bordered ? `${props.$style?.borderStyle} ${props.$style?.borderWidth} ${props.$style?.border}` : 'none'; + }}; + padding: ${(props) => props.$customStyle?.padding || props.$style?.padding}; + margin: ${(props) => props.$customStyle?.margin || props.$style?.margin}; + font-size: ${(props) => props.$style?.textSize}; + font-weight: ${(props) => props.$style?.fontWeight}; + cursor: pointer; + `; + + const StyledTagContainer = styled.div` + display: flex; + gap: 5px; + padding: 5px; + `; + + const childrenMap = { + options: TagsCompOptionsControl, + style: styleControl(InputLikeStyle, 'style'), + onEvent: ButtonEventHandlerControl, + borderless: BoolCodeControl, + enableIndividualStyling: BoolCodeControl, + }; + + return new UICompBuilder(childrenMap, (props) => { + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}); + + return ( + + {props.options.map((tag, index) => { + + // Use individual styling only if enableIndividualStyling is true + const tagColor = props.enableIndividualStyling ? getTagColor(tag.label, props.options) : undefined; + const tagIcon = props.enableIndividualStyling ? getTagIcon(tag.label, props.options) : tag.icon; + const tagStyle = props.enableIndividualStyling ? getTagStyle(tag.label, props.options, props.style) : {}; + + return ( + handleClickEvent()} + > + {tag.label} + + ); + })} + + ); + }) + .setPropertyViewFn((children: any) => { + return ( + <> +
+ {children.options.propertyView({})} +
+ + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {children.onEvent.getPropertyView()} + {hiddenPropertyView(children)} + {showDataLoadingIndicatorsPropertyView(children)} +
+ )} + + {["layout", "both"].includes( + useContext(EditorContext).editorModeStatus + ) && ( +
+ {children.enableIndividualStyling.propertyView({ + label: trans("style.individualStyling"), + tooltip: trans("style.individualStylingTooltip") + })} + {children.borderless.propertyView({ label: trans("style.borderless") })} + {children.style.getPropertyView()} +
+ )} + + ) + }) + .build(); +})() + +export const MultiTagsComp = withExposingConfigs(multiTags, [new NameConfig("options", "")]); + diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index 1d36ec52c5..1186057d9c 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -557,6 +557,7 @@ const TabsOption = new MultiCompBuilder( )) .build(); + export const TabsOptionControl = manualOptionsControl(TabsOption, { initOptions: [ @@ -567,6 +568,37 @@ export const TabsOptionControl = manualOptionsControl(TabsOption, { autoIncField: "id", }); +const TagsOption = new MultiCompBuilder( + { + id: valueComp(-1), + label: StringControl, + icon: IconControl, + iconPosition: withDefault(LeftRightControl, "left"), + hidden: BoolCodeControl, + }, + (props) => props +) + .setPropertyViewFn((children) => ( + <> + {children.label.propertyView({ label: trans("label") })} + {children.icon.propertyView({ label: trans("icon") })} + {children.iconPosition.propertyView({ + label: trans("tabbedContainer.iconPosition"), + radioButton: true, + })} + {hiddenPropertyView(children)} + + )) + .build(); + +export const TagsOptionControl = optionsControl(TagsOption, { + initOptions: [ + { id: 0, label: "Option 1" }, + { id: 1, label: "Option 2" }, + ], + autoIncField: "id", +}); + const StyledIcon = styled.span` margin: 0 4px 0 14px; `; @@ -750,14 +782,83 @@ export const StepOptionControl = optionsControl(StepOption, { uniqField: "label", }); +let TagsCompOptions = new MultiCompBuilder( + { + label: StringControl, + icon: IconControl, + colorType: withDefault(dropdownControl([ + { label: trans("style.preset"), value: "preset" }, + { label: trans("style.custom"), value: "custom" }, + ] as const, "preset"), "preset"), + presetColor: withDefault(dropdownControl(TAG_PRESET_COLORS, "blue"), "blue"), + color: withDefault(ColorControl, "#1890ff"), + textColor: withDefault(ColorControl, "#ffffff"), + border: withDefault(ColorControl, ""), + radius: withDefault(RadiusControl, ""), + margin: withDefault(StringControl, ""), + padding: withDefault(StringControl, ""), + }, + (props) => props +).build(); + +TagsCompOptions = class extends TagsCompOptions implements OptionCompProperty { + propertyView(param: { autoMap?: boolean }) { + const colorType = this.children.colorType.getView(); + return ( + <> + {this.children.label.propertyView({ label: trans("coloredTagOptionControl.tag") })} + {this.children.icon.propertyView({ label: trans("coloredTagOptionControl.icon") })} + {this.children.colorType.propertyView({ + label: trans("style.colorType"), + radioButton: true + })} + {colorType === "preset" && this.children.presetColor.propertyView({ + label: trans("style.presetColor") + })} + {colorType === "custom" && ( + <> + {this.children.color.propertyView({ label: trans("coloredTagOptionControl.color") })} + {this.children.textColor.propertyView({ label: trans("style.textColor") })} + + )} + {this.children.border.propertyView({ + label: trans('style.border') + })} + {this.children.radius.propertyView({ + label: trans('style.borderRadius'), + preInputNode: , + placeholder: '3px', + })} + {this.children.margin.propertyView({ + label: trans('style.margin'), + preInputNode: , + placeholder: '3px', + })} + {this.children.padding.propertyView({ + label: trans('style.padding'), + preInputNode: , + placeholder: '3px', + })} + + ); + } +}; + +export const TagsCompOptionsControl = optionsControl(TagsCompOptions, { + initOptions: [ + { label: "Option 1", colorType: "preset", presetColor: "blue" }, + { label: "Option 2", colorType: "preset", presetColor: "green" } + ], + uniqField: "label", +}); let ColoredTagOption = new MultiCompBuilder( { label: StringControl, icon: IconControl, colorType: withDefault(dropdownControl([ - { label: "Preset", value: "preset" }, - { label: "Custom", value: "custom" }, + { label: trans("style.preset"), value: "preset" }, + { label: trans("style.custom"), value: "custom" }, ] as const, "preset"), "preset"), presetColor: withDefault(dropdownControl(TAG_PRESET_COLORS, "blue"), "blue"), color: withDefault(ColorControl, "#1890ff"), @@ -779,16 +880,16 @@ ColoredTagOption = class extends ColoredTagOption implements OptionCompProperty {this.children.label.propertyView({ label: trans("coloredTagOptionControl.tag") })} {this.children.icon.propertyView({ label: trans("coloredTagOptionControl.icon") })} {this.children.colorType.propertyView({ - label: "Color Type", + label: trans("style.colorType"), radioButton: true })} {colorType === "preset" && this.children.presetColor.propertyView({ - label: "Preset Color" + label: trans("style.presetColor") })} {colorType === "custom" && ( <> {this.children.color.propertyView({ label: trans("coloredTagOptionControl.color") })} - {this.children.textColor.propertyView({ label: "Text Color" })} + {this.children.textColor.propertyView({ label: trans("style.textColor") })} )} {this.children.border.propertyView({ diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 2395f4f290..609ddf5b0a 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -94,7 +94,7 @@ import { TourCompIcon, StepCompIcon, ShapesCompIcon, - + TagsCompIcon, CandlestickChartCompIcon, FunnelChartCompIcon, HeatmapChartCompIcon, @@ -193,6 +193,7 @@ import { DrawerComp } from "./hooks/drawerComp"; import { ModalComp } from "./hooks/modalComp"; import { defaultCollapsibleContainerData } from "./comps/containerComp/collapsibleContainerComp"; import { ContainerComp as FloatTextContainerComp } from "./comps/containerComp/textContainerComp"; +import { MultiTagsComp } from "./comps/tagsComp/tagsCompView"; type Registry = { [key in UICompType]?: UICompManifest; @@ -709,6 +710,19 @@ export var uiCompMap: Registry = { }, defaultDataFn: defaultGridData, }, + multiTags: { + name: trans("tags"), + enName: "tags", + description: "Desc of Tags", + categories: ["layout"], + icon: TagsCompIcon, + keywords: trans("uiComp.floatButtonCompKeywords"), + comp: MultiTagsComp, + layoutInfo: { + w: 9, + h: 5, + }, + }, modal: { name: trans("uiComp.modalCompName"), enName: "Modal", diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index 4c320de479..07f0e54b43 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -106,6 +106,7 @@ export type UICompType = | "container" | "pageLayout" // added by Falk Wolsky | "floatTextContainer" + | "multiTags" // Added by Kamal Qureshi | "tabbedContainer" | "modal" | "listView" diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index f644df665a..1ff980d532 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -45,6 +45,7 @@ export const en = { "accessControl": "Access Control", "copySuccess": "Copied Successfully", "copyError": "Copy Error", + "tags": "Tags", "api": { "publishSuccess": "Published Successfully", @@ -571,6 +572,10 @@ export const en = { "headerText": "Header Text Color", "labelColor": "Label Color", "label": "Label Color", + "colorType": "Color Type", + "presetColor": "Preset Color", + "preset": "Preset", + "custom": "Custom", "lineHeight":"Line Height", "subTitleColor": "SubTitle Color", "titleText": "Title Color", @@ -600,6 +605,14 @@ export const en = { "chartTextColor": "Text Color", "detailSize": "Detail Size", "hideColumn": "Hide Column", + "height": "Height", + "gap": "Gap", + "flexWrap": "Flex Wrap", + "justifyContent": "Justify Content", + "alignItems": "Align Items", + "borderless": "Borderless", + "individualStyling": "Individual Styling", + "individualStylingTooltip": "When enabled, each tag can have its own colors, borders, and spacing. When disabled, all tags use the general style settings.", "radiusTip": "Specifies the radius of the element's corners. Example: 5px, 50%, or 1em.", "gapTip": "Specifies the gap between rows and columns in a grid or flex container. Example: 10px, 1rem, or 5%.", diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index a931455d4b..beea9cae7a 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -104,6 +104,7 @@ import { TurnstileCaptchaCompIconSmall, PivotTableCompIconSmall, GraphChartCompIconSmall, + TagsCompIconSmall, } from "lowcoder-design"; // Memoize icon components to prevent unnecessary re-renders @@ -237,6 +238,7 @@ export const CompStateIcon: { step: , table: , text: , + multiTags: , timeline: , toggleButton: , tour: ,