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: ,