From 3d77e54bf39406a8e80509360ff04151eb63861f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 22 Jan 2026 22:15:17 +0500 Subject: [PATCH 1/7] [Feat]: #2113 add toast / notify component --- .../src/comps/hooks/hookCompTypes.tsx | 5 +- .../lowcoder/src/comps/hooks/toastComp.ts | 95 ------ .../lowcoder/src/comps/hooks/toastComp.tsx | 321 ++++++++++++++++++ client/packages/lowcoder/src/comps/index.tsx | 11 + .../lowcoder/src/comps/uiCompRegistry.ts | 1 + .../packages/lowcoder/src/i18n/locales/en.ts | 60 +++- 6 files changed, 391 insertions(+), 102 deletions(-) delete mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp.ts create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp.tsx diff --git a/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx b/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx index a310ff6e36..66442f648c 100644 --- a/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx +++ b/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx @@ -60,7 +60,10 @@ const HookCompConfig: Record< }, utils: { category: "hide" }, message: { category: "hide" }, - toast: { category: "hide" }, + toast: { + category: "ui", + singleton: false, + }, }; // Get hook component category diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.ts b/client/packages/lowcoder/src/comps/hooks/toastComp.ts deleted file mode 100644 index fdcee872f3..0000000000 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { withMethodExposing } from "../generators/withMethodExposing"; -import { simpleMultiComp } from "../generators"; -import { withExposingConfigs } from "../generators/withExposing"; -import { EvalParamType, ParamsConfig } from "../controls/actionSelector/executeCompTypes"; -import { JSONObject } from "../../util/jsonTypes"; -import { trans } from "i18n"; -import { notificationInstance } from "lowcoder-design"; -import type { ArgsProps, NotificationPlacement } from "antd/es/notification/interface"; - -const params: ParamsConfig = [ - { name: "text", type: "string" }, - { name: "options", type: "JSON" }, -]; - -const showNotification = ( - params: EvalParamType[], - level: "open" | "info" | "success" | "warning" | "error" -) => { - const text = params?.[0] as string; - const options = (params?.[1] as JSONObject) || {}; - - const { message , duration, id, placement, dismissible } = options; - - const closeIcon: boolean | undefined = dismissible === true ? undefined : (dismissible === false ? false : undefined); - - const durationNumberOrNull: number | null = typeof duration === 'number' ? duration : null; - - const notificationArgs: ArgsProps = { - message: text, - description: message as React.ReactNode, - duration: durationNumberOrNull ?? 3, - key: id as React.Key, - placement: placement as NotificationPlacement ?? "bottomRight", - closeIcon: closeIcon as boolean, - }; - - // Use notificationArgs to trigger the notification - - text && notificationInstance[level](notificationArgs); -}; - -const destroy = ( - params: EvalParamType[] -) => { - // Extract the id from the params - const id = params[0] as React.Key; - - // Call notificationInstance.destroy with the provided id - notificationInstance.destroy(id); -}; - -//what we would like to expose: title, text, duration, id, btn-obj, onClose, placement - -const ToastCompBase = simpleMultiComp({}); - -export let ToastComp = withExposingConfigs(ToastCompBase, []); - -ToastComp = withMethodExposing(ToastComp, [ - { - method: { name: "destroy", description: trans("toastComp.destroy"), params: params }, - execute: (comp, params) => destroy(params), - }, - { - method: { name: "open", description: trans("toastComp.info"), params: params }, - execute: (comp, params) => { - showNotification(params, "open"); - }, - }, - { - method: { name: "info", description: trans("toastComp.info"), params: params }, - execute: (comp, params) => { - showNotification(params, "info"); - }, - }, - { - method: { name: "success", description: trans("toastComp.success"), params: params }, - execute: (comp, params) => { - showNotification(params, "success"); - }, - }, - { - method: { name: "warn", description: trans("toastComp.warn"), params: params }, - execute: (comp, params) => { - showNotification(params, "warning"); - }, - }, - { - method: { name: "error", description: trans("toastComp.error"), params: params }, - execute: (comp, params) => { - showNotification(params, "error"); - }, - }, -]); - - diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx new file mode 100644 index 0000000000..8c2f65d30f --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -0,0 +1,321 @@ +import { BoolControl } from "comps/controls/boolControl"; +import { NumberControl, StringControl } from "comps/controls/codeControl"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { eventHandlerControl } from "comps/controls/eventHandlerControl"; +import { withDefault, simpleMultiComp, withPropertyViewFn } from "comps/generators"; +import { withMethodExposing } from "comps/generators/withMethodExposing"; +import { NameConfig, withExposingConfigs } from "comps/generators/withExposing"; +import { Section, sectionNames } from "lowcoder-design"; +import { trans } from "i18n"; +import { notificationInstance } from "lowcoder-design"; +import type { ArgsProps, NotificationPlacement } from "antd/es/notification/interface"; +import { EvalParamType, ParamsConfig } from "comps/controls/actionSelector/executeCompTypes"; +import { JSONObject } from "util/jsonTypes"; +import React from "react"; +import { stateComp } from "comps/generators/simpleGenerators"; + +// Toast type options +const toastTypeOptions = [ + { label: trans("toastComp.typeInfo"), value: "info" }, + { label: trans("toastComp.typeSuccess"), value: "success" }, + { label: trans("toastComp.typeWarning"), value: "warning" }, + { label: trans("toastComp.typeError"), value: "error" }, +] as const; + +// Placement options for notification position +const placementOptions = [ + { label: trans("toastComp.placementTopLeft"), value: "topLeft" }, + { label: trans("toastComp.placementTopRight"), value: "topRight" }, + { label: trans("toastComp.placementBottomLeft"), value: "bottomLeft" }, + { label: trans("toastComp.placementBottomRight"), value: "bottomRight" }, +] as const; + +// Event options for toast +const ToastEventOptions = [ + { label: trans("toastComp.click"), value: "click", description: trans("toastComp.clickDesc") }, + { label: trans("toastComp.close"), value: "close", description: trans("toastComp.closeDesc") }, +] as const; + +// Method parameters for programmatic API +const showParams: ParamsConfig = [ + { name: "text", type: "string" }, + { name: "options", type: "JSON" }, +]; + +const closeParams: ParamsConfig = [ + { name: "key", type: "string" }, +]; + +// Children map for toast component configuration +const childrenMap = { + // Basic configuration + title: withDefault(StringControl, ""), + description: withDefault(StringControl, ""), + type: dropdownControl(toastTypeOptions, "info"), + + // Timing + duration: withDefault(NumberControl, 4.5), + + // Position & Appearance + placement: dropdownControl(placementOptions, "bottomRight"), + dismissible: withDefault(BoolControl, true), + showProgress: withDefault(BoolControl, false), + pauseOnHover: withDefault(BoolControl, true), + + // Event handlers + onEvent: eventHandlerControl(ToastEventOptions), + + // Internal state for tracking visibility + visible: stateComp(false), +}; + +type ToastType = "info" | "success" | "warning" | "error"; + +// Helper function to show notification with event callbacks +const showNotificationWithEvents = ( + config: { + title: string; + description: string; + type: ToastType; + duration: number; + placement: NotificationPlacement; + dismissible: boolean; + showProgress: boolean; + pauseOnHover: boolean; + key?: string; + }, + onEvent: (eventName: "click" | "close") => Promise, + setVisible: (visible: boolean) => void +) => { + const notificationKey = config.key || `toast-${Date.now()}`; + + const notificationArgs: ArgsProps = { + message: config.title, + description: config.description || undefined, + duration: config.duration === 0 ? null : config.duration, + key: notificationKey, + placement: config.placement, + closeIcon: config.dismissible ? undefined : false, + showProgress: config.showProgress, + pauseOnHover: config.pauseOnHover, + onClick: () => { + onEvent("click"); + }, + onClose: () => { + setVisible(false); + onEvent("close"); + }, + }; + + // Show notification based on type + if (config.title || config.description) { + setVisible(true); + notificationInstance[config.type](notificationArgs); + } + + return notificationKey; +}; + +// Helper for programmatic API (backwards compatible) +const showNotificationProgrammatic = ( + params: EvalParamType[], + level: ToastType, + comp: any +) => { + const text = params?.[0] as string; + const options = (params?.[1] as JSONObject) || {}; + + const { + description, + duration, + key, + placement, + dismissible, + showProgress, + pauseOnHover, + } = options; + + // Use component config as defaults, override with params + const config = { + title: text || comp.children.title.getView(), + description: (description as string) ?? comp.children.description.getView(), + type: level, + duration: typeof duration === "number" ? duration : comp.children.duration.getView(), + placement: (placement as NotificationPlacement) ?? comp.children.placement.getView(), + dismissible: typeof dismissible === "boolean" ? dismissible : comp.children.dismissible.getView(), + showProgress: typeof showProgress === "boolean" ? showProgress : comp.children.showProgress.getView(), + pauseOnHover: typeof pauseOnHover === "boolean" ? pauseOnHover : comp.children.pauseOnHover.getView(), + key: key as string | undefined, + }; + + const onEvent = comp.children.onEvent.getView(); + const setVisible = (visible: boolean) => { + comp.children.visible.dispatchChangeValueAction(visible); + }; + + return showNotificationWithEvents(config, onEvent, setVisible); +}; + +// Property view component +const ToastPropertyView = React.memo((props: { comp: any }) => { + const { comp } = props; + + return ( + <> +
+ {comp.children.title.propertyView({ + label: trans("toastComp.title"), + placeholder: trans("toastComp.titlePlaceholder"), + })} + {comp.children.description.propertyView({ + label: trans("toastComp.description"), + placeholder: trans("toastComp.descriptionPlaceholder"), + })} + {comp.children.type.propertyView({ + label: trans("toastComp.type"), + })} +
+ +
+ {comp.children.duration.propertyView({ + label: trans("toastComp.duration"), + tooltip: trans("toastComp.durationTooltip"), + placeholder: "4.5", + })} + {comp.children.placement.propertyView({ + label: trans("toastComp.placement"), + })} + {comp.children.dismissible.propertyView({ + label: trans("toastComp.dismissible"), + })} + {comp.children.showProgress.propertyView({ + label: trans("toastComp.showProgress"), + tooltip: trans("toastComp.showProgressTooltip"), + })} + {comp.children.pauseOnHover.propertyView({ + label: trans("toastComp.pauseOnHover"), + })} +
+ +
+ {comp.children.onEvent.getPropertyView()} +
+ + ); +}); + +ToastPropertyView.displayName = "ToastPropertyView"; + +// Build the component +let ToastCompBase = simpleMultiComp(childrenMap); + +ToastCompBase = withPropertyViewFn(ToastCompBase, (comp) => ( + +)); + +// Add exposing configs +let ToastCompWithExposing = withExposingConfigs(ToastCompBase, [ + new NameConfig("visible", trans("toastComp.visibleDesc")), + new NameConfig("title", trans("toastComp.titleDesc")), + new NameConfig("description", trans("toastComp.descriptionDesc")), + new NameConfig("type", trans("toastComp.typeDesc")), + new NameConfig("duration", trans("toastComp.durationDesc")), + new NameConfig("placement", trans("toastComp.placementDesc")), +]); + +// Add method exposing +export let ToastComp = withMethodExposing(ToastCompWithExposing, [ + { + method: { + name: "show", + description: trans("toastComp.showMethod"), + params: [], + }, + execute: (comp) => { + const config = { + title: comp.children.title.getView(), + description: comp.children.description.getView(), + type: comp.children.type.getView() as ToastType, + duration: comp.children.duration.getView(), + placement: comp.children.placement.getView() as NotificationPlacement, + dismissible: comp.children.dismissible.getView(), + showProgress: comp.children.showProgress.getView(), + pauseOnHover: comp.children.pauseOnHover.getView(), + }; + + const onEvent = comp.children.onEvent.getView(); + const setVisible = (visible: boolean) => { + comp.children.visible.dispatchChangeValueAction(visible); + }; + + showNotificationWithEvents(config, onEvent, setVisible); + }, + }, + { + method: { + name: "info", + description: trans("toastComp.info"), + params: showParams, + }, + execute: (comp, params) => showNotificationProgrammatic(params, "info", comp), + }, + { + method: { + name: "success", + description: trans("toastComp.success"), + params: showParams, + }, + execute: (comp, params) => showNotificationProgrammatic(params, "success", comp), + }, + { + method: { + name: "warn", + description: trans("toastComp.warn"), + params: showParams, + }, + execute: (comp, params) => showNotificationProgrammatic(params, "warning", comp), + }, + { + method: { + name: "error", + description: trans("toastComp.error"), + params: showParams, + }, + execute: (comp, params) => showNotificationProgrammatic(params, "error", comp), + }, + { + method: { + name: "close", + description: trans("toastComp.closeMethod"), + params: closeParams, + }, + execute: (comp, params) => { + const key = params?.[0] as string; + if (key) { + notificationInstance.destroy(key); + } + comp.children.visible.dispatchChangeValueAction(false); + comp.children.onEvent.getView()("close"); + }, + }, + // Legacy method for backwards compatibility + { + method: { + name: "destroy", + description: trans("toastComp.destroy"), + params: closeParams, + }, + execute: (comp, params) => { + const key = params?.[0] as string; + notificationInstance.destroy(key); + }, + }, + { + method: { + name: "open", + description: trans("toastComp.openMethod"), + params: showParams, + }, + execute: (comp, params) => showNotificationProgrammatic(params, "info", comp), + }, +]); diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 0bbf0b7312..72ed8c9905 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -193,6 +193,7 @@ import { TreeComp } from "./comps/treeComp/treeComp"; import { TreeSelectComp } from "./comps/treeComp/treeSelectComp"; import { DrawerComp } from "./hooks/drawerComp"; import { ModalComp } from "./hooks/modalComp"; +import { ToastComp } from "./hooks/toastComp"; import { defaultCollapsibleContainerData } from "./comps/containerComp/collapsibleContainerComp"; import { ContainerComp as FloatTextContainerComp } from "./comps/containerComp/textContainerComp"; import { MultiTagsComp } from "./comps/tagsComp/tagsCompView"; @@ -761,6 +762,16 @@ export var uiCompMap: Registry = { comp: DrawerComp, withoutLoading: true, }, + toast: { + name: trans("uiComp.toastCompName"), + enName: "Toast", + description: trans("uiComp.toastCompDesc"), + categories: ["layout"], + icon: ModalCompIcon, + keywords: trans("uiComp.toastCompKeywords"), + comp: ToastComp, + withoutLoading: true, + }, divider: { name: trans("uiComp.dividerCompName"), enName: "Divider", diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index f8e09763ce..ef37a41799 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -110,6 +110,7 @@ export type UICompType = | "multiTags" // Added by Kamal Qureshi | "tabbedContainer" | "modal" + | "toast" | "listView" | "grid" | "navigation" diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 8cbc264048..8cc8ee297d 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1258,6 +1258,10 @@ export const en = { "drawerCompDesc": "A sliding panel component that can be used for additional navigation or content display, typically emerging from the edge of the screen.", "drawerCompKeywords": "drawer, sliding, panel, navigation", + "toastCompName": "Toast", + "toastCompDesc": "A notification component for displaying brief messages, alerts, or feedback to users. Supports click and close event handlers.", + "toastCompKeywords": "toast, notification, alert, message, snackbar", + "chartCompName": "Chart (deprecated)", "chartCompDesc": "A versatile component for visualizing data through various types of charts and graphs.", "chartCompKeywords": "chart, graph, data, visualization", @@ -3271,12 +3275,56 @@ export const en = { "error": "Send an Error Notification" }, "toastComp": { - "destroy": "close a Notification", - "info": "Send a Notification", - "loading": "Send a Loading Notification", - "success": "Send a Success Notification", - "warn": "Send a Warning Notification", - "error": "Send an Error Notification" + // Method descriptions + "destroy": "Close a notification by key", + "info": "Show an info notification", + "success": "Show a success notification", + "warn": "Show a warning notification", + "error": "Show an error notification", + "showMethod": "Show notification with configured settings", + "closeMethod": "Close the notification", + "openMethod": "Show an info notification (alias for info)", + + // Property labels + "title": "Title", + "titlePlaceholder": "Notification title", + "description": "Description", + "descriptionPlaceholder": "Notification description", + "type": "Type", + "duration": "Duration (seconds)", + "durationTooltip": "Time in seconds before auto-close. Set to 0 to disable auto-close.", + "placement": "Placement", + "dismissible": "Show Close Button", + "showProgress": "Show Progress Bar", + "showProgressTooltip": "Display a progress bar indicating time until auto-close", + "pauseOnHover": "Pause on Hover", + "behavior": "Behavior", + + // Type options + "typeInfo": "Info", + "typeSuccess": "Success", + "typeWarning": "Warning", + "typeError": "Error", + + // Placement options + "placementTopLeft": "Top Left", + "placementTopRight": "Top Right", + "placementBottomLeft": "Bottom Left", + "placementBottomRight": "Bottom Right", + + // Event labels + "click": "Click", + "clickDesc": "Triggered when the notification is clicked", + "close": "Close", + "closeDesc": "Triggered when the notification is closed or dismissed", + + // Exposed state descriptions + "visibleDesc": "Whether the notification is currently visible", + "titleDesc": "The configured title of the notification", + "descriptionDesc": "The configured description of the notification", + "typeDesc": "The configured type (info, success, warning, error)", + "durationDesc": "The configured duration in seconds", + "placementDesc": "The configured placement position" }, "themeComp": { "switchTo": "Switch Theme" From 877972bd20bddc497cf046b93cbc60eefcf21680 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 22 Jan 2026 23:58:59 +0500 Subject: [PATCH 2/7] fix type error icon --- client/packages/lowcoder/src/pages/editor/editorConstants.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index 54cd5faaf4..a3630350b8 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -222,6 +222,7 @@ export const CompStateIcon: { mention: , mermaid: , modal: , + toast: , module: , moduleContainer: , navigation: , From 79a727dd14747c322e7741e637d301772086455d Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 23 Jan 2026 19:35:43 +0500 Subject: [PATCH 3/7] add styling programmtic support --- client/packages/lowcoder/src/comps/hooks/toastComp.tsx | 4 ++++ client/packages/lowcoder/src/comps/index.tsx | 2 +- client/packages/lowcoder/src/pages/editor/editorConstants.tsx | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx index 8c2f65d30f..60aed179c5 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -83,6 +83,7 @@ const showNotificationWithEvents = ( showProgress: boolean; pauseOnHover: boolean; key?: string; + style?: React.CSSProperties; }, onEvent: (eventName: "click" | "close") => Promise, setVisible: (visible: boolean) => void @@ -98,6 +99,7 @@ const showNotificationWithEvents = ( closeIcon: config.dismissible ? undefined : false, showProgress: config.showProgress, pauseOnHover: config.pauseOnHover, + style: config.style, onClick: () => { onEvent("click"); }, @@ -133,6 +135,7 @@ const showNotificationProgrammatic = ( dismissible, showProgress, pauseOnHover, + style, } = options; // Use component config as defaults, override with params @@ -146,6 +149,7 @@ const showNotificationProgrammatic = ( showProgress: typeof showProgress === "boolean" ? showProgress : comp.children.showProgress.getView(), pauseOnHover: typeof pauseOnHover === "boolean" ? pauseOnHover : comp.children.pauseOnHover.getView(), key: key as string | undefined, + style: style as React.CSSProperties | undefined, }; const onEvent = comp.children.onEvent.getView(); diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 72ed8c9905..2f07d21f6f 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -767,7 +767,7 @@ export var uiCompMap: Registry = { enName: "Toast", description: trans("uiComp.toastCompDesc"), categories: ["layout"], - icon: ModalCompIcon, + icon: CommentCompIcon, keywords: trans("uiComp.toastCompKeywords"), comp: ToastComp, withoutLoading: true, diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index a3630350b8..ebdc145039 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -222,7 +222,7 @@ export const CompStateIcon: { mention: , mermaid: , modal: , - toast: , + toast: , module: , moduleContainer: , navigation: , From 5de05eee324db4f3a6e5d6340107f08b4443b287 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 23 Jan 2026 22:00:58 +0500 Subject: [PATCH 4/7] add style control propertyview --- .../comps/controls/styleControlConstants.tsx | 14 ++++ .../lowcoder/src/comps/hooks/toastComp.tsx | 66 +++++++++++++++++-- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 01587643db..ed2f1e852a 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1598,6 +1598,19 @@ export const ModalStyle = [ BACKGROUND_IMAGE_ORIGIN, ] as const; + +export const NotificationStyle = [ + getBackground("primarySurface"), + { + name: "color", + label: trans("color"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + getStaticBorder("transparent"), +] as const; + export const CascaderStyle = [ ...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc"), TEXT, @@ -2488,6 +2501,7 @@ export type ChildrenMultiSelectStyleType = StyleConfigType< export type TabContainerStyleType = StyleConfigType; export type TabBodyStyleType = StyleConfigType; export type ModalStyleType = StyleConfigType; +export type NotificationStyleType = StyleConfigType; export type CascaderStyleType = StyleConfigType; export type CheckboxStyleType = StyleConfigType; export type RadioStyleType = StyleConfigType; diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx index 60aed179c5..4e639b08f7 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -2,7 +2,9 @@ import { BoolControl } from "comps/controls/boolControl"; import { NumberControl, StringControl } from "comps/controls/codeControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { eventHandlerControl } from "comps/controls/eventHandlerControl"; -import { withDefault, simpleMultiComp, withPropertyViewFn } from "comps/generators"; +import { styleControl } from "comps/controls/styleControl"; +import { NotificationStyle, NotificationStyleType } from "comps/controls/styleControlConstants"; +import { withDefault, simpleMultiComp, withPropertyViewFn, withViewFn } from "comps/generators"; import { withMethodExposing } from "comps/generators/withMethodExposing"; import { NameConfig, withExposingConfigs } from "comps/generators/withExposing"; import { Section, sectionNames } from "lowcoder-design"; @@ -11,8 +13,9 @@ import { notificationInstance } from "lowcoder-design"; import type { ArgsProps, NotificationPlacement } from "antd/es/notification/interface"; import { EvalParamType, ParamsConfig } from "comps/controls/actionSelector/executeCompTypes"; import { JSONObject } from "util/jsonTypes"; -import React from "react"; +import React, { useEffect } from "react"; import { stateComp } from "comps/generators/simpleGenerators"; +import { isEqual } from "lodash"; // Toast type options const toastTypeOptions = [ @@ -64,6 +67,14 @@ const childrenMap = { // Event handlers onEvent: eventHandlerControl(ToastEventOptions), + + // Style + style: styleControl(NotificationStyle), + resolvedStyle: stateComp({ + background: "", + color: "", + border: "", + }), // Internal state for tracking visibility visible: stateComp(false), @@ -83,23 +94,38 @@ const showNotificationWithEvents = ( showProgress: boolean; pauseOnHover: boolean; key?: string; + styleConfig?: NotificationStyleType; style?: React.CSSProperties; }, onEvent: (eventName: "click" | "close") => Promise, setVisible: (visible: boolean) => void ) => { const notificationKey = config.key || `toast-${Date.now()}`; + + const borderColor = config.styleConfig?.border; + const computedStyle: React.CSSProperties = { + background: config.styleConfig?.background, + color: config.styleConfig?.color, + border: + borderColor && borderColor !== "transparent" ? `1px solid ${borderColor}` : undefined, + }; + const mergedStyle: React.CSSProperties = { ...computedStyle, ...(config.style || {}) }; + const textColor = typeof mergedStyle.color === "string" ? mergedStyle.color : undefined; const notificationArgs: ArgsProps = { - message: config.title, - description: config.description || undefined, + message: textColor ? {config.title} : config.title, + description: config.description + ? textColor + ? {config.description} + : config.description + : undefined, duration: config.duration === 0 ? null : config.duration, key: notificationKey, placement: config.placement, closeIcon: config.dismissible ? undefined : false, showProgress: config.showProgress, pauseOnHover: config.pauseOnHover, - style: config.style, + style: mergedStyle, onClick: () => { onEvent("click"); }, @@ -149,6 +175,7 @@ const showNotificationProgrammatic = ( showProgress: typeof showProgress === "boolean" ? showProgress : comp.children.showProgress.getView(), pauseOnHover: typeof pauseOnHover === "boolean" ? pauseOnHover : comp.children.pauseOnHover.getView(), key: key as string | undefined, + styleConfig: comp.children.resolvedStyle.getView() as NotificationStyleType, style: style as React.CSSProperties | undefined, }; @@ -204,15 +231,43 @@ const ToastPropertyView = React.memo((props: { comp: any }) => {
{comp.children.onEvent.getPropertyView()}
+ +
+ {comp.children.style.getPropertyView()} +
); }); ToastPropertyView.displayName = "ToastPropertyView"; +/** + * Toast has no visible view, but we still need a runtime view to: + * - avoid executing styleControl.getView() inside EditorView's useMemo (hooks warning) + * - resolve theme-dependent styleControl values inside React render, then persist them + * into a plain state field (`resolvedStyle`) that can be safely used by methods. + */ +const ToastRuntimeView = React.memo((props: { comp: any }) => { + const { comp } = props; + const style = comp.children.style.getView() as NotificationStyleType; + + useEffect(() => { + const current = comp.children.resolvedStyle.getView() as NotificationStyleType; + if (!isEqual(style, current)) { + comp.children.resolvedStyle.dispatchChangeValueAction(style); + } + }, [comp, style]); + + return null; +}); + +ToastRuntimeView.displayName = "ToastRuntimeView"; + // Build the component let ToastCompBase = simpleMultiComp(childrenMap); +ToastCompBase = withViewFn(ToastCompBase, (comp) => ); + ToastCompBase = withPropertyViewFn(ToastCompBase, (comp) => ( )); @@ -245,6 +300,7 @@ export let ToastComp = withMethodExposing(ToastCompWithExposing, [ dismissible: comp.children.dismissible.getView(), showProgress: comp.children.showProgress.getView(), pauseOnHover: comp.children.pauseOnHover.getView(), + styleConfig: comp.children.resolvedStyle.getView() as NotificationStyleType, }; const onEvent = comp.children.onEvent.getView(); From 8fda44419c862a9819cde1131847e1f2fd5ae481 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 26 Jan 2026 20:53:33 +0500 Subject: [PATCH 5/7] add isolate styles for each toast via uniqueID --- .../lowcoder/src/comps/hooks/toastComp.tsx | 74 +++++++++++++------ 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx index 4e639b08f7..d7bf10a7fe 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -13,9 +13,31 @@ import { notificationInstance } from "lowcoder-design"; import type { ArgsProps, NotificationPlacement } from "antd/es/notification/interface"; import { EvalParamType, ParamsConfig } from "comps/controls/actionSelector/executeCompTypes"; import { JSONObject } from "util/jsonTypes"; -import React, { useEffect } from "react"; +import React, { useEffect, useId } from "react"; import { stateComp } from "comps/generators/simpleGenerators"; import { isEqual } from "lodash"; +import { createGlobalStyle } from "styled-components"; + +// Dynamic global styles for toast notifications - scoped by unique instance ID +// Using high specificity selectors to override Ant Design's default styles +const ToastGlobalStyle = createGlobalStyle<{ + $instanceId: string; + $background?: string; + $textColor?: string; + $border?: string; +}>` + .ant-notification .ant-notification-notice-wrapper .ant-notification-notice.lowcoder-toast-${props => props.$instanceId} { + background: ${props => props.$background || 'inherit'}; + border: ${props => props.$border && props.$border !== 'transparent' + ? `1px solid ${props.$border}` + : 'none'}; + + .ant-notification-notice-message, + .ant-notification-notice-description { + color: ${props => props.$textColor || 'inherit'}; + } + } +`; // Toast type options const toastTypeOptions = [ @@ -78,6 +100,9 @@ const childrenMap = { // Internal state for tracking visibility visible: stateComp(false), + + // Unique instance ID for scoped styling (set by ToastRuntimeView) + instanceId: stateComp(""), }; type ToastType = "info" | "success" | "warning" | "error"; @@ -96,36 +121,24 @@ const showNotificationWithEvents = ( key?: string; styleConfig?: NotificationStyleType; style?: React.CSSProperties; + instanceId: string; }, onEvent: (eventName: "click" | "close") => Promise, setVisible: (visible: boolean) => void ) => { const notificationKey = config.key || `toast-${Date.now()}`; - const borderColor = config.styleConfig?.border; - const computedStyle: React.CSSProperties = { - background: config.styleConfig?.background, - color: config.styleConfig?.color, - border: - borderColor && borderColor !== "transparent" ? `1px solid ${borderColor}` : undefined, - }; - const mergedStyle: React.CSSProperties = { ...computedStyle, ...(config.style || {}) }; - const textColor = typeof mergedStyle.color === "string" ? mergedStyle.color : undefined; - const notificationArgs: ArgsProps = { - message: textColor ? {config.title} : config.title, - description: config.description - ? textColor - ? {config.description} - : config.description - : undefined, + message: config.title, + description: config.description || undefined, duration: config.duration === 0 ? null : config.duration, key: notificationKey, placement: config.placement, closeIcon: config.dismissible ? undefined : false, showProgress: config.showProgress, pauseOnHover: config.pauseOnHover, - style: mergedStyle, + className: `lowcoder-toast-${config.instanceId}`, + style: config.style, onClick: () => { onEvent("click"); }, @@ -177,6 +190,7 @@ const showNotificationProgrammatic = ( key: key as string | undefined, styleConfig: comp.children.resolvedStyle.getView() as NotificationStyleType, style: style as React.CSSProperties | undefined, + instanceId: comp.children.instanceId.getView() as string, }; const onEvent = comp.children.onEvent.getView(); @@ -242,14 +256,20 @@ const ToastPropertyView = React.memo((props: { comp: any }) => { ToastPropertyView.displayName = "ToastPropertyView"; /** - * Toast has no visible view, but we still need a runtime view to: - * - avoid executing styleControl.getView() inside EditorView's useMemo (hooks warning) - * - resolve theme-dependent styleControl values inside React render, then persist them - * into a plain state field (`resolvedStyle`) that can be safely used by methods. + * Toast runtime view: + * - Resolves theme-dependent style values and stores them in `resolvedStyle` + * - Generates unique instance ID for scoped styling + * - Injects global styles scoped to this toast instance */ const ToastRuntimeView = React.memo((props: { comp: any }) => { const { comp } = props; const style = comp.children.style.getView() as NotificationStyleType; + const instanceId = useId().replace(/:/g, '-'); + + // Store instance ID and resolved styles + useEffect(() => { + comp.children.instanceId.dispatchChangeValueAction(instanceId); + }, [comp, instanceId]); useEffect(() => { const current = comp.children.resolvedStyle.getView() as NotificationStyleType; @@ -258,7 +278,14 @@ const ToastRuntimeView = React.memo((props: { comp: any }) => { } }, [comp, style]); - return null; + return ( + + ); }); ToastRuntimeView.displayName = "ToastRuntimeView"; @@ -301,6 +328,7 @@ export let ToastComp = withMethodExposing(ToastCompWithExposing, [ showProgress: comp.children.showProgress.getView(), pauseOnHover: comp.children.pauseOnHover.getView(), styleConfig: comp.children.resolvedStyle.getView() as NotificationStyleType, + instanceId: comp.children.instanceId.getView() as string, }; const onEvent = comp.children.onEvent.getView(); From e3eac9715c4d6d767fe82405f7542cbc628ea66c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 Jan 2026 00:33:40 +0500 Subject: [PATCH 6/7] fix + add more styling customizations for toast component --- .../comps/controls/styleControlConstants.tsx | 5 ++ .../lowcoder/src/comps/hooks/toastComp.tsx | 57 ++++++++++++++----- .../packages/lowcoder/src/i18n/locales/en.ts | 5 +- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index ed2f1e852a..2325d375d0 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1609,6 +1609,11 @@ export const NotificationStyle = [ transformer: contrastText, }, getStaticBorder("transparent"), + RADIUS, + BORDER_WIDTH, + BORDER_STYLE, + MARGIN, + PADDING, ] as const; export const CascaderStyle = [ diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx index d7bf10a7fe..fb9aa48232 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -18,19 +18,32 @@ import { stateComp } from "comps/generators/simpleGenerators"; import { isEqual } from "lodash"; import { createGlobalStyle } from "styled-components"; -// Dynamic global styles for toast notifications - scoped by unique instance ID -// Using high specificity selectors to override Ant Design's default styles + const ToastGlobalStyle = createGlobalStyle<{ $instanceId: string; $background?: string; $textColor?: string; $border?: string; + $borderWidth?: string; + $borderStyle?: string; + $radius?: string; + $margin?: string; + $padding?: string; + $width?: string; }>` - .ant-notification .ant-notification-notice-wrapper .ant-notification-notice.lowcoder-toast-${props => props.$instanceId} { + .ant-notification .ant-notification-notice-wrapper:has(.lowcoder-toast-${props => props.$instanceId}) { background: ${props => props.$background || 'inherit'}; - border: ${props => props.$border && props.$border !== 'transparent' - ? `1px solid ${props.$border}` - : 'none'}; + border-color: ${props => props.$border || 'transparent'}; + border-width: ${props => props.$borderWidth || '0'}; + border-style: ${props => props.$borderStyle || 'solid'}; + border-radius: ${props => props.$radius || '8px'}; + ${props => props.$margin ? `margin: ${props.$margin};` : ''} + ${props => props.$width ? `width: ${props.$width};` : ''} + ${props => props.$padding ? `padding: ${props.$padding};` : ''} + + .ant-notification-notice { + background: transparent; + } .ant-notification-notice-message, .ant-notification-notice-description { @@ -47,7 +60,6 @@ const toastTypeOptions = [ { label: trans("toastComp.typeError"), value: "error" }, ] as const; -// Placement options for notification position const placementOptions = [ { label: trans("toastComp.placementTopLeft"), value: "topLeft" }, { label: trans("toastComp.placementTopRight"), value: "topRight" }, @@ -55,13 +67,11 @@ const placementOptions = [ { label: trans("toastComp.placementBottomRight"), value: "bottomRight" }, ] as const; -// Event options for toast const ToastEventOptions = [ { label: trans("toastComp.click"), value: "click", description: trans("toastComp.clickDesc") }, { label: trans("toastComp.close"), value: "close", description: trans("toastComp.closeDesc") }, ] as const; -// Method parameters for programmatic API const showParams: ParamsConfig = [ { name: "text", type: "string" }, { name: "options", type: "JSON" }, @@ -87,6 +97,9 @@ const childrenMap = { showProgress: withDefault(BoolControl, false), pauseOnHover: withDefault(BoolControl, true), + // Layout + width: withDefault(StringControl, ""), + // Event handlers onEvent: eventHandlerControl(ToastEventOptions), @@ -96,6 +109,11 @@ const childrenMap = { background: "", color: "", border: "", + radius: "", + borderWidth: "", + borderStyle: "", + margin: "", + padding: "", }), // Internal state for tracking visibility @@ -242,6 +260,14 @@ const ToastPropertyView = React.memo((props: { comp: any }) => { })} +
+ {comp.children.width.propertyView({ + label: trans("toastComp.width"), + tooltip: trans("toastComp.widthTooltip"), + placeholder: "384", + })} +
+
{comp.children.onEvent.getPropertyView()}
@@ -256,14 +282,12 @@ const ToastPropertyView = React.memo((props: { comp: any }) => { ToastPropertyView.displayName = "ToastPropertyView"; /** - * Toast runtime view: - * - Resolves theme-dependent style values and stores them in `resolvedStyle` - * - Generates unique instance ID for scoped styling - * - Injects global styles scoped to this toast instance + * Toast runtime view */ const ToastRuntimeView = React.memo((props: { comp: any }) => { const { comp } = props; const style = comp.children.style.getView() as NotificationStyleType; + const width = comp.children.width.getView() as string; const instanceId = useId().replace(/:/g, '-'); // Store instance ID and resolved styles @@ -284,6 +308,12 @@ const ToastRuntimeView = React.memo((props: { comp: any }) => { $background={style.background} $textColor={style.color} $border={style.border} + $borderWidth={style.borderWidth} + $borderStyle={style.borderStyle} + $radius={style.radius} + $margin={style.margin} + $padding={style.padding || '20px'} + $width={width ? `${width}px` : undefined} /> ); }); @@ -307,6 +337,7 @@ let ToastCompWithExposing = withExposingConfigs(ToastCompBase, [ new NameConfig("type", trans("toastComp.typeDesc")), new NameConfig("duration", trans("toastComp.durationDesc")), new NameConfig("placement", trans("toastComp.placementDesc")), + new NameConfig("width", trans("toastComp.widthDesc")), ]); // Add method exposing diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 8cc8ee297d..480728804f 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3299,6 +3299,8 @@ export const en = { "showProgressTooltip": "Display a progress bar indicating time until auto-close", "pauseOnHover": "Pause on Hover", "behavior": "Behavior", + "width": "Width", + "widthTooltip": "Width of the notification in pixels. Example: 384", // Type options "typeInfo": "Info", @@ -3324,7 +3326,8 @@ export const en = { "descriptionDesc": "The configured description of the notification", "typeDesc": "The configured type (info, success, warning, error)", "durationDesc": "The configured duration in seconds", - "placementDesc": "The configured placement position" + "placementDesc": "The configured placement position", + "widthDesc": "The configured width of the notification" }, "themeComp": { "switchTo": "Switch Theme" From c13e155a61adc610c9e0a6b0538b605c354be994 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 Jan 2026 23:45:13 +0500 Subject: [PATCH 7/7] fix toast width layout + add color customization for icons --- .../comps/controls/styleControlConstants.tsx | 27 ++++++++++++ .../lowcoder/src/comps/hooks/toastComp.tsx | 41 +++++++++++++++++-- .../packages/lowcoder/src/i18n/locales/en.ts | 7 +++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 2325d375d0..70ae4d527e 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1608,6 +1608,33 @@ export const NotificationStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + { + name: "closeIconColor", + label: trans("toastComp.closeIconColor"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + { + name: "infoIconColor", + label: trans("toastComp.infoIconColor"), + color: "#1890ff", + }, + { + name: "successIconColor", + label: trans("toastComp.successIconColor"), + color: "#52c41a", + }, + { + name: "warningIconColor", + label: trans("toastComp.warningIconColor"), + color: "#faad14", + }, + { + name: "errorIconColor", + label: trans("toastComp.errorIconColor"), + color: "#ff4d4f", + }, getStaticBorder("transparent"), RADIUS, BORDER_WIDTH, diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx index fb9aa48232..1540e071fd 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -23,6 +23,11 @@ const ToastGlobalStyle = createGlobalStyle<{ $instanceId: string; $background?: string; $textColor?: string; + $closeIconColor?: string; + $infoIconColor?: string; + $successIconColor?: string; + $warningIconColor?: string; + $errorIconColor?: string; $border?: string; $borderWidth?: string; $borderStyle?: string; @@ -38,17 +43,37 @@ const ToastGlobalStyle = createGlobalStyle<{ border-style: ${props => props.$borderStyle || 'solid'}; border-radius: ${props => props.$radius || '8px'}; ${props => props.$margin ? `margin: ${props.$margin};` : ''} - ${props => props.$width ? `width: ${props.$width};` : ''} ${props => props.$padding ? `padding: ${props.$padding};` : ''} .ant-notification-notice { background: transparent; + ${props => props.$width ? `width: ${props.$width};` : ''} } .ant-notification-notice-message, .ant-notification-notice-description { color: ${props => props.$textColor || 'inherit'}; } + + .ant-notification-notice-close { + color: ${props => props.$closeIconColor || 'inherit'}; + } + + .ant-notification-notice-icon-info.anticon { + color: ${props => props.$infoIconColor || '#1890ff'}; + } + + .ant-notification-notice-icon-success.anticon { + color: ${props => props.$successIconColor || '#52c41a'}; + } + + .ant-notification-notice-icon-warning.anticon { + color: ${props => props.$warningIconColor || '#faad14'}; + } + + .ant-notification-notice-icon-error.anticon { + color: ${props => props.$errorIconColor || '#ff4d4f'}; + } } `; @@ -108,6 +133,11 @@ const childrenMap = { resolvedStyle: stateComp({ background: "", color: "", + closeIconColor: "", + infoIconColor: "", + successIconColor: "", + warningIconColor: "", + errorIconColor: "", border: "", radius: "", borderWidth: "", @@ -264,7 +294,7 @@ const ToastPropertyView = React.memo((props: { comp: any }) => { {comp.children.width.propertyView({ label: trans("toastComp.width"), tooltip: trans("toastComp.widthTooltip"), - placeholder: "384", + placeholder: "384px or 100vw", })} @@ -307,13 +337,18 @@ const ToastRuntimeView = React.memo((props: { comp: any }) => { $instanceId={instanceId} $background={style.background} $textColor={style.color} + $closeIconColor={style.closeIconColor} + $infoIconColor={style.infoIconColor} + $successIconColor={style.successIconColor} + $warningIconColor={style.warningIconColor} + $errorIconColor={style.errorIconColor} $border={style.border} $borderWidth={style.borderWidth} $borderStyle={style.borderStyle} $radius={style.radius} $margin={style.margin} $padding={style.padding || '20px'} - $width={width ? `${width}px` : undefined} + $width={width || undefined} /> ); }); diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 480728804f..09cc320b7a 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3300,7 +3300,12 @@ export const en = { "pauseOnHover": "Pause on Hover", "behavior": "Behavior", "width": "Width", - "widthTooltip": "Width of the notification in pixels. Example: 384", + "widthTooltip": "Width of the notification in pixels, percentages, or other CSS units. if you want to adjust it according to the screen size, you can use viewport units. Example: 100vw", + "closeIconColor": "Close Icon Color", + "infoIconColor": "Info Icon Color", + "successIconColor": "Success Icon Color", + "warningIconColor": "Warning Icon Color", + "errorIconColor": "Error Icon Color", // Type options "typeInfo": "Info",