From 3d77e54bf39406a8e80509360ff04151eb63861f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 22 Jan 2026 22:15:17 +0500 Subject: [PATCH 1/4] [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/4] 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/4] 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/4] 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();