From 5c9256cef87dcde750768850e47c9f3e8abf2a2d Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 6 Jan 2026 20:52:24 +0500 Subject: [PATCH 01/40] [Fix]: #2101 transfer component selectedKeys + filterOption --- .../lowcoder/src/comps/comps/transferComp.tsx | 36 +++++++++++++------ .../packages/lowcoder/src/i18n/locales/en.ts | 5 +-- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/transferComp.tsx b/client/packages/lowcoder/src/comps/comps/transferComp.tsx index 0da018ee4f..466622cd93 100644 --- a/client/packages/lowcoder/src/comps/comps/transferComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/transferComp.tsx @@ -14,7 +14,7 @@ import type { TransferKey } from "antd/es/transfer/interface"; import { useResizeDetector } from "react-resize-detector"; import { changeEvent, eventHandlerControl, searchEvent, selectedChangeEvent } from "../controls/eventHandlerControl"; import styled, { css } from "styled-components"; -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState, useCallback } from "react"; import { valueComp, withDefault } from "../generators"; import type { TransferDirection } from 'antd/es/transfer'; import React from "react"; @@ -64,11 +64,12 @@ const childrenMap = { oneWay: BoolControl, pagination: BoolControl, showSearch: BoolControl.DEFAULT_TRUE, + caseSensitive: BoolControl, pageSize: withDefault(NumberControl, 10), items: arrayObjectExposingStateControl('items', defaultItems as any), targetKeys: arrayStringExposingStateControl('targetKeys', []), - selectedKeys: valueComp([[], []]), - targerObject: valueComp([]), + selectedKeys: arrayStringExposingStateControl('selectedKeys', []), + targetObject: arrayObjectExposingStateControl('targetObject', []), searchInfo: valueComp(['', '']), }; @@ -80,7 +81,6 @@ const TransferView = React.memo((props: RecordConstructorToView(null); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); - const [selectedKeys, setSelectedKeys] = useState([]); useEffect(() => { if (height && width) { @@ -90,13 +90,16 @@ const TransferView = React.memo((props: RecordConstructorToView { props.targetKeys.onChange(newTargetKeys as string[]); - props.dispatch(changeChildAction("targerObject", Array.isArray(props.items.value) ? props.items.value.filter(item => newTargetKeys.includes(item.key as string)) : [], false)); + const targetObjects = Array.isArray(props.items.value) + ? props.items.value.filter(item => newTargetKeys.includes(item.key as string)) + : []; + props.targetObject.onChange(targetObjects); props.onEvent('change') }; const onSelectChange = (sourceSelectedKeys: TransferKey[], targetSelectedKeys: TransferKey[]) => { - setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys] as string[]); - props.dispatch(changeChildAction("selectedKeys", [sourceSelectedKeys as string[], targetSelectedKeys as string[]], false)); + const allSelectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys] as string[]; + props.selectedKeys.onChange(allSelectedKeys); props.onEvent('selectedChange') }; @@ -105,6 +108,13 @@ const TransferView = React.memo((props: RecordConstructorToView { + if (props.caseSensitive) { + return item.title?.includes(inputValue) ?? false; + } + return item.title?.toLowerCase().includes(inputValue.toLowerCase()) ?? false; + }, [props.caseSensitive]); + const onResize = () => { const container = conRef.current; setWidth(container?.clientWidth ?? 0); @@ -130,10 +140,11 @@ const TransferView = React.memo((props: RecordConstructorToView item.title} + filterOption={filterOption} oneWay={props.oneWay} onSearch={handleSearch} pagination={props.pagination ? { @@ -153,8 +164,8 @@ const TransferCompPropertyView = React.memo((props: { {props.children.items.propertyView({ label: trans("transfer.items"), })} - {props.children.targetKeys.propertyView({ - label: trans("transfer.targetKeys"), + {props.children.selectedKeys.propertyView({ + label: trans("transfer.selectedKeys"), })} {props.children.sourceTitle.propertyView({ label: trans("transfer.sourceTitle"), @@ -165,6 +176,9 @@ const TransferCompPropertyView = React.memo((props: { {props.children.showSearch.propertyView({ label: trans("transfer.allowSearch"), })} + {props.children.showSearch.getView() && props.children.caseSensitive.propertyView({ + label: trans("transfer.caseSensitive"), + })} {props.children.oneWay.propertyView({ label: trans("transfer.oneWay"), })} @@ -202,7 +216,7 @@ TransferBasicComp = class extends TransferBasicComp { export const transferComp = withExposingConfigs(TransferBasicComp, [ new NameConfig("items", trans("transfer.items")), new NameConfig("targetKeys", trans("transfer.targetKeys")), - new NameConfig("targerObject", trans("transfer.targerObject")), + new NameConfig("targetObject", trans("transfer.targetObject")), new NameConfig("selectedKeys", trans("transfer.selectedKeys")), new NameConfig("searchInfo", trans("transfer.searchInfo")), NameConfigHidden, diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 8cbc264048..c49c2cdc3d 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1743,14 +1743,15 @@ export const en = { "targetTitle": "Target Data", "content": "Content {i}", "items": "Items", - "targetKeys": "Selected Keys", + "targetKeys": "Target Keys", "oneWay": "One Way", "pagination": "Pagination", "pageSize": "Page Size", "allowSearch": "Allow Search", "selectedKeys": "Selected Keys", "searchInfo": "Search Info", - "targerObject": "Targer Object" + "targetObject": "Target Object", + "caseSensitive": "Case Sensitive" }, "avatarGroup": { From df8fd22fa7819d0a0812c134e4685ebd8e9581a9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 8 Jan 2026 19:59:35 +0500 Subject: [PATCH 02/40] add select component selected styles --- .../comps/comps/selectInputComp/cascaderComp.tsx | 3 +++ .../selectInputComp/selectCompConstants.tsx | 6 ++++++ .../src/comps/controls/styleControlConstants.tsx | 16 +++++++++++++++- client/packages/lowcoder/src/i18n/locales/en.ts | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx index b96f8eb3e8..662cd9d98f 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx @@ -32,6 +32,9 @@ const DropdownRenderStyle = styled.div<{ $childrenInputFieldStyle: ChildrenMulti text-decoration: ${props => props.$childrenInputFieldStyle?.textDecoration}; color: ${props => props.$childrenInputFieldStyle?.text}; } + .ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled) { + background-color: ${props => props.$childrenInputFieldStyle?.activeBackground ? `${props.$childrenInputFieldStyle.activeBackground}22` : 'rgb(242, 247, 252)'}; + } ` let CascaderBasicComp = (function () { diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx index a75b9f05ce..08d7690fd9 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx @@ -217,6 +217,12 @@ export const DropdownStyled = styled.div<{ $style: ChildrenMultiSelectStyleType min-width: 14px; margin-right: 0; } + .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + background-color: ${props => props.$style?.selectBackground ? `${props.$style.selectBackground}` : 'transparent'}; + } + .ant-select-item-option-active:not(.ant-select-item-option-disabled) { + background-color: ${props => props.$style?.activeBackground ? `${props.$style.activeBackground}` : 'transparent'}; + } `; const Wrapper = styled.span` diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 01587643db..c7410749e8 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1555,7 +1555,21 @@ export const MultiSelectStyle = [ export const ChildrenMultiSelectStyle = [ ...STYLING_FIELDS_SEQUENCE, - getBackground() + getBackground(), + { + name: "activeBackground", + label: trans("style.activeBackground"), + depTheme: "primary", + depType: DEP_TYPE.SELF, + transformer: handleLightenColor, + }, + { + name: "selectBackground", + label: trans("style.selectBackground"), + depTheme: "primary", + depType: DEP_TYPE.SELF, + transformer: toSelf, + } ] as const; export const TabContainerStyle = [ diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 8cbc264048..b3234e2ef7 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -592,6 +592,7 @@ export const en = { "siderBackgroundImagePosition": "Sider Background Image Position", "siderBackgroundImageOrigin": "Sider Background Image Origin", "activeBackground": "Active Background Color", + "selectBackground": "Selected Background Color", "labelBackground": "Label Background Color", "gradientBackground": "Gradient Background Color", "direction": "Direction", From b9122be74335a3ff0a1a3cc095e702e1ca6a3fee Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 9 Jan 2026 19:07:30 +0500 Subject: [PATCH 03/40] [Feat]: #2099 add clearValueAt + one unified state --- .../comps/comps/fileComp/draggerUpload.tsx | 28 ++++----- .../src/comps/comps/fileComp/fileComp.tsx | 63 ++++++++++++++----- .../packages/lowcoder/src/i18n/locales/en.ts | 1 + 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx index 2f230ad38e..8c6ae5256b 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx @@ -1,7 +1,7 @@ import { default as AntdUpload } from "antd/es/upload"; import { default as Button } from "antd/es/button"; import { UploadFile, UploadChangeParam, UploadFileStatus, RcFile } from "antd/es/upload/interface"; -import { useState, useEffect } from "react"; +import { useState, useMemo } from "react"; import styled, { css } from "styled-components"; import { trans } from "i18n"; import _ from "lodash"; @@ -162,25 +162,27 @@ interface DraggerUploadProps { export const DraggerUpload = (props: DraggerUploadProps) => { const { dispatch, files, style, autoHeight, animationStyle } = props; - const [fileList, setFileList] = useState( - files.map((f) => ({ ...f, status: "done" })) as UploadFile[] - ); + // Track only files currently being uploaded (not yet in props.files) + const [uploadingFiles, setUploadingFiles] = useState([]); const [showModal, setShowModal] = useState(false); const isMobile = checkIsMobile(window.innerWidth); - useEffect(() => { - if (files.length === 0 && fileList.length !== 0) { - setFileList([]); - } - }, [files]); + // Derive fileList from props.files (source of truth) + currently uploading files + const fileList = useMemo(() => [ + ...(files.map((f) => ({ ...f, status: "done" as const })) as UploadFile[]), + ...uploadingFiles, + ], [files, uploadingFiles]); const handleOnChange = (param: UploadChangeParam) => { - const uploadingFiles = param.fileList.filter((f) => f.status === "uploading"); - if (uploadingFiles.length !== 0) { - setFileList(param.fileList); + const currentlyUploading = param.fileList.filter((f) => f.status === "uploading"); + if (currentlyUploading.length !== 0) { + setUploadingFiles(currentlyUploading); return; } + // Clear uploading state when all uploads complete + setUploadingFiles([]); + let maxFiles = props.maxFiles; if (props.uploadType === "single") { maxFiles = 1; @@ -240,8 +242,6 @@ export const DraggerUpload = (props: DraggerUploadProps) => { props.onEvent("parse"); }); } - - setFileList(uploadedFiles.slice(-maxFiles)); }; return ( diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx index 360a815569..37a3e86335 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx @@ -24,7 +24,7 @@ import { RecordConstructorToView, } from "lowcoder-core"; import { UploadRequestOption } from "rc-upload/lib/interface"; -import { Suspense, useCallback, useEffect, useRef, useState } from "react"; +import { Suspense, useCallback, useMemo, useRef, useState } from "react"; import styled, { css } from "styled-components"; import { JSONObject, JSONValue } from "../../../util/jsonTypes"; import { BoolControl, BoolPureControl } from "../../controls/boolControl"; @@ -265,29 +265,32 @@ const Upload = ( }, ) => { const { dispatch, files, style } = props; - const [fileList, setFileList] = useState( - files.map((f) => ({ ...f, status: "done" })) as UploadFile[] - ); + // Track only files currently being uploaded (not yet in props.files) + const [uploadingFiles, setUploadingFiles] = useState([]); const [showModal, setShowModal] = useState(false); const isMobile = checkIsMobile(window.innerWidth); - useEffect(() => { - if (files.length === 0 && fileList.length !== 0) { - setFileList([]); - } - }, [files]); + // Derive fileList from props.files (source of truth) + currently uploading files + const fileList = useMemo(() => [ + ...(files.map((f) => ({ ...f, status: "done" as const })) as UploadFile[]), + ...uploadingFiles, + ], [files, uploadingFiles]); + // chrome86 bug: button children should not contain only empty span const hasChildren = hasIcon(props.prefixIcon) || !!props.text || hasIcon(props.suffixIcon); const handleOnChange = (param: UploadChangeParam) => { - const uploadingFiles = param.fileList.filter((f) => f.status === "uploading"); + const currentlyUploading = param.fileList.filter((f) => f.status === "uploading"); // the onChange callback will be executed when the state of the antd upload file changes. // so make a trick logic: the file list with loading will not be processed - if (uploadingFiles.length !== 0) { - setFileList(param.fileList); + if (currentlyUploading.length !== 0) { + setUploadingFiles(currentlyUploading); return; } + // Clear uploading state when all uploads complete + setUploadingFiles([]); + let maxFiles = props.maxFiles; if (props.uploadType === "single") { maxFiles = 1; @@ -348,8 +351,6 @@ const Upload = ( props.onEvent("parse"); }); } - - setFileList(uploadedFiles.slice(-maxFiles)); }; return ( @@ -552,6 +553,40 @@ const FileWithMethods = withMethodExposing(FileImplComp, [ }) ), }, + { + method: { + name: "clearValueAt", + description: trans("file.clearValueAtDesc"), + params: [{ name: "index", type: "number" }], + }, + execute: (comp, params) => { + const index = params[0] as number; + const value = comp.children.value.getView(); + const files = comp.children.files.getView(); + const parsedValue = comp.children.parsedValue.getView(); + + if (index < 0 || index >= files.length) { + return; + } + + comp.dispatch( + multiChangeAction({ + value: changeValueAction( + [...value.slice(0, index), ...value.slice(index + 1)], + false + ), + files: changeValueAction( + [...files.slice(0, index), ...files.slice(index + 1)], + false + ), + parsedValue: changeValueAction( + [...parsedValue.slice(0, index), ...parsedValue.slice(index + 1)], + false + ), + }) + ); + }, + }, ]); export const FileComp = withExposingConfigs(FileWithMethods, [ diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 8cbc264048..643c74aa9d 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1934,6 +1934,7 @@ export const en = { "filesValueDesc": "The Contents of the Currently Uploaded File Are Base64 Encoded", "filesDesc": "List of the Current Uploaded Files. For Details, Refer to", "clearValueDesc": "Clear All Files", + "clearValueAtDesc": "Clear File at Index", "parseFiles": "Parse Files", "parsedValueTooltip1": "If parseFiles Is True, Upload Files Will Parse to Object, Array, or String. Parsed Data Can Be Accessed via the parsedValue Array.", "parsedValueTooltip2": "Supports Excel, JSON, CSV, and Text Files. Other Formats Will Return Null.", From a2b4c12f5481cc5f2425749e1f9b87ee25f262d7 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 14 Jan 2026 00:19:11 +0500 Subject: [PATCH 04/40] [Feat]: #2100 file validation add + refactor file upload --- .../comps/comps/fileComp/draggerUpload.tsx | 24 ++----- .../src/comps/comps/fileComp/fileComp.tsx | 69 +++++++++++++++---- .../packages/lowcoder/src/i18n/locales/en.ts | 5 ++ 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx index 8c6ae5256b..22f08989d7 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx @@ -11,8 +11,7 @@ import { multiChangeAction, } from "lowcoder-core"; import { hasIcon } from "comps/utils"; -import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; -import { resolveValue, resolveParsedValue, commonProps } from "./fileComp"; +import { resolveValue, resolveParsedValue, commonProps, validateFile } from "./fileComp"; import { FileStyleType, AnimationStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { ImageCaptureModal } from "./ImageCaptureModal"; import { v4 as uuidv4 } from "uuid"; @@ -152,6 +151,7 @@ interface DraggerUploadProps { minSize: number; maxSize: number; maxFiles: number; + fileNamePattern: string; uploadType: "single" | "multiple" | "directory"; text: string; dragHintText?: string; @@ -254,21 +254,11 @@ export const DraggerUpload = (props: DraggerUploadProps) => { $auto={autoHeight} capture={props.forceCapture} openFileDialogOnClick={!(props.forceCapture && !isMobile)} - beforeUpload={(file) => { - if (!file.size || file.size <= 0) { - messageInstance.error(`${file.name} ` + trans("file.fileEmptyErrorMsg")); - return AntdUpload.LIST_IGNORE; - } - - if ( - (!!props.minSize && file.size < props.minSize) || - (!!props.maxSize && file.size > props.maxSize) - ) { - messageInstance.error(`${file.name} ` + trans("file.fileSizeExceedErrorMsg")); - return AntdUpload.LIST_IGNORE; - } - return true; - }} + beforeUpload={(file) => validateFile(file, { + minSize: props.minSize, + maxSize: props.maxSize, + fileNamePattern: props.fileNamePattern, + })} onChange={handleOnChange} >

diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx index 37a3e86335..d18f7c8228 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx @@ -97,6 +97,7 @@ const validationChildren = { minSize: FileSizeControl, maxSize: FileSizeControl, maxFiles: NumberControl, + fileNamePattern: StringControl, }; const commonChildren = { @@ -127,6 +128,11 @@ const commonValidationFields = (children: RecordConstructorToComp options.onSuccess && options.onSuccess({}), // Override the default upload logic and do not upload to the specified server }); +export interface FileValidationOptions { + minSize?: number; + maxSize?: number; + fileNamePattern?: string; +} + + +export const validateFile = ( + file: { name: string; size?: number }, + options: FileValidationOptions +): boolean | typeof AntdUpload.LIST_IGNORE => { + // Empty file validation + if (!file.size || file.size <= 0) { + messageInstance.error(`${file.name} ` + trans("file.fileEmptyErrorMsg")); + return AntdUpload.LIST_IGNORE; + } + + // File size validation + if ( + (!!options.minSize && file.size < options.minSize) || + (!!options.maxSize && file.size > options.maxSize) + ) { + messageInstance.error(`${file.name} ` + trans("file.fileSizeExceedErrorMsg")); + return AntdUpload.LIST_IGNORE; + } + + // File name pattern validation + if (options.fileNamePattern) { + try { + const pattern = new RegExp(options.fileNamePattern); + if (!pattern.test(file.name)) { + messageInstance.error(`${file.name} ` + trans("file.fileNamePatternErrorMsg")); + return AntdUpload.LIST_IGNORE; + } + } catch (e) { + messageInstance.error(trans("file.invalidFileNamePatternMsg", { error: String(e) })); + return AntdUpload.LIST_IGNORE; + } + } + + return true; +}; + const getStyle = (style: FileStyleType) => { return css` .ant-btn { @@ -361,21 +410,11 @@ const Upload = ( {...commonProps(props)} $style={style} fileList={fileList} - beforeUpload={(file) => { - if (!file.size || file.size <= 0) { - messageInstance.error(`${file.name} ` + trans("file.fileEmptyErrorMsg")); - return AntdUpload.LIST_IGNORE; - } - - if ( - (!!props.minSize && file.size < props.minSize) || - (!!props.maxSize && file.size > props.maxSize) - ) { - messageInstance.error(`${file.name} ` + trans("file.fileSizeExceedErrorMsg")); - return AntdUpload.LIST_IGNORE; - } - return true; - }} + beforeUpload={(file) => validateFile(file, { + minSize: props.minSize, + maxSize: props.maxSize, + fileNamePattern: props.fileNamePattern, + })} onChange={handleOnChange} > diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 643c74aa9d..7bccf6aad5 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1949,6 +1949,11 @@ export const en = { "dragAreaText": "Click or drag file to this area to upload", "dragAreaHint": "Support for a single or bulk upload. Strictly prohibited from uploading company data or other banned files.", "dragHintText": "Hint Text", + "fileNamePattern": "File Name Pattern", + "fileNamePatternTooltip": "A regular expression pattern to validate file names (e.g., '^[a-zA-Z0-9_-]+\\.[a-z]+$' for alphanumeric names). Leave empty to allow all file names.", + "fileNamePatternPlaceholder": "^[a-zA-Z0-9_-]+\\.[a-z]+$", + "fileNamePatternErrorMsg": "Upload Failed. The File Name Does Not Match the Required Pattern.", + "invalidFileNamePatternMsg": "Invalid File Name Pattern: {error}", }, "date": { "format": "Format", From 3d77e54bf39406a8e80509360ff04151eb63861f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 22 Jan 2026 22:15:17 +0500 Subject: [PATCH 05/40] [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 06/40] 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 07/40] 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 08/40] 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 09/40] 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 10/40] 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 11/40] 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", From ca98a9fc52c71b6fef84f04a82d033d228f76eba Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 29 Jan 2026 20:23:48 +0500 Subject: [PATCH 12/40] [Fix]: tabbed container body padding --- .../lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index d1efa4a399..427ab09134 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -243,7 +243,7 @@ const TabPaneContent: React.FC = ({ positionParams={positionParamsView} dispatch={dispatch} autoHeight={autoHeight} - containerPadding={[paddingWidth, 20]} + containerPadding={[paddingWidth, 0]} /> From 8018b12e44babe80b13cb982b8c4a83cff0ff840 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 29 Jan 2026 23:48:27 +0500 Subject: [PATCH 13/40] [Fix]: #2118 table download event override --- .../lowcoder/src/comps/comps/tableComp/tableCompView.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index a5adc969c4..389db69525 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -294,8 +294,13 @@ export const TableCompView = React.memo((props: { ) } onDownload={() => { - handleChangeEvent("download"); - onDownload(`${compName}-data`) + if (compChildren.onEvent.isBind("download")) { + // Custom download handler exists + handleChangeEvent("download"); + } else { + // Download default CSV + onDownload(`${compName}-data`); + } }} hasChange={hasChange} onSaveChanges={() => handleChangeEvent("saveChanges")} From 0ec710af479c06c2a7f6fb6a6f3bd8d6c534633f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 2 Feb 2026 19:52:16 +0500 Subject: [PATCH 14/40] [Fix]: #2119 column layout hide property --- .../lowcoder/src/comps/comps/columnLayout/columnLayout.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index c457ba4c06..2fc3fbdd62 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -209,9 +209,8 @@ const ColumnLayout = (props: ColumnLayoutProps) => { {columns.map(column => { const id = String(column.id); const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); - if(!containers[id]) return null + if(!containers[id] || column.hidden) return null const containerProps = containers[id].children; - const noOfColumns = columns.length; return ( From 48383377b380651f482509387392b35c966d38dc Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 3 Feb 2026 21:08:02 +0500 Subject: [PATCH 15/40] [Fix]: #1758 tabbed container padding and remove unnecessary code --- .../comps/comps/tabs/tabbedContainerComp.tsx | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index d1efa4a399..12331702f3 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -31,7 +31,6 @@ import { trans } from "i18n"; import { BoolCodeControl, NumberControl } from "comps/controls/codeControl"; import { DisabledContext } from "comps/generators/uiCompBuilder"; import { EditorContext } from "comps/editorState"; -import { checkIsMobile } from "util/commonUtils"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { BoolControl } from "comps/controls/boolControl"; import { PositionControl,dropdownControl } from "comps/controls/dropdownControl"; @@ -150,14 +149,14 @@ const StyledTabs = styled(Tabs)<{ $style: TabContainerStyleType; $headerStyle: ContainerHeaderStyleType; $bodyStyle: TabBodyStyleType; - $isMobile?: boolean; $showHeader?: boolean; - $animationStyle:AnimationStyleType; + $animationStyle: AnimationStyleType; $isDestroyPane?: boolean; + $placement?: string; }>` &.ant-tabs { height: 100%; - ${props=>props.$animationStyle} + ${props => props.$animationStyle} } .ant-tabs-content-animated { @@ -170,20 +169,16 @@ const StyledTabs = styled(Tabs)<{ .ant-tabs-nav { display: ${(props) => (props.$showHeader ? "block" : "none")}; - padding: 0 ${(props) => (props.$isMobile ? 16 : 24)}px; margin: 0px; } .ant-tabs-tab + .ant-tabs-tab { - margin: 0 0 0 20px; + ${(props) => (props.$placement === "left" || props.$placement === "right") + ? `margin: 20px 0 0 0;` + : `margin: 0 0 0 20px;`} } - .ant-tabs-nav-operations { - margin-right: -24px; - } - - ${(props) => - props.$style && getStyle(props.$style, props.$headerStyle, props.$bodyStyle)} + ${(props) => props.$style && getStyle(props.$style, props.$headerStyle, props.$bodyStyle)} /* Conditional styling for all modes except Destroy Inactive Pane */ ${(props) => !props.$isDestroyPane && ` @@ -207,7 +202,6 @@ const ContainerInTab = (props: ContainerBaseProps) => { type TabPaneContentProps = { autoHeight: boolean; showVerticalScrollbar: boolean; - paddingWidth: number; horizontalGridCells: number; bodyBackground: string; layoutView: any; @@ -219,7 +213,6 @@ type TabPaneContentProps = { const TabPaneContent: React.FC = ({ autoHeight, showVerticalScrollbar, - paddingWidth, horizontalGridCells, bodyBackground, layoutView, @@ -243,7 +236,7 @@ const TabPaneContent: React.FC = ({ positionParams={positionParamsView} dispatch={dispatch} autoHeight={autoHeight} - containerPadding={[paddingWidth, 20]} + containerPadding={[0, 20]} /> @@ -272,11 +265,7 @@ const TabbedContainer = (props: TabbedContainerProps) => { } }, [activeKey, props.selectedTabKey.value]); - const editorState = useContext(EditorContext); - const maxWidth = editorState.getAppSettings().maxWidth; - const isMobile = checkIsMobile(maxWidth); const showHeader = props.showHeader.valueOf(); - const paddingWidth = isMobile ? 8 : 0; const tabItems = visibleTabs.map((tab) => { const id = String(tab.id); @@ -302,7 +291,6 @@ const TabbedContainer = (props: TabbedContainerProps) => { { $bodyStyle={bodyStyle} $showHeader={showHeader} $isDestroyPane={tabBehavior === "destroy"} + $placement={props.placement} onChange={(key) => { if (key !== props.selectedTabKey.value) { props.selectedTabKey.onChange(key); @@ -334,7 +323,6 @@ const TabbedContainer = (props: TabbedContainerProps) => { } }} animated - $isMobile={isMobile} items={tabItems} tabBarGutter={props.tabsGutter} centered={props.tabsCentered} From 0983be162cf97a856b5b23b752f55aa536b60655 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 4 Feb 2026 21:40:38 +0500 Subject: [PATCH 16/40] add progress bar toast customization --- .../comps/controls/styleControlConstants.tsx | 10 +++++++ .../lowcoder/src/comps/hooks/toastComp.tsx | 29 +++++++++++++++++++ .../packages/lowcoder/src/i18n/locales/en.ts | 4 +++ 3 files changed, 43 insertions(+) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 70ae4d527e..4d0bf7565e 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1635,6 +1635,16 @@ export const NotificationStyle = [ label: trans("toastComp.errorIconColor"), color: "#ff4d4f", }, + { + name: "progressColor", + label: trans("toastComp.progressColor"), + color: "#1890ff", + }, + { + name: "progressBackground", + label: trans("toastComp.progressBackground"), + color: "#e8e8e8", + }, 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 1540e071fd..6dc8056399 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx @@ -28,6 +28,9 @@ const ToastGlobalStyle = createGlobalStyle<{ $successIconColor?: string; $warningIconColor?: string; $errorIconColor?: string; + $progressColor?: string; + $progressBackground?: string; + $progressHeight?: string; $border?: string; $borderWidth?: string; $borderStyle?: string; @@ -74,6 +77,20 @@ const ToastGlobalStyle = createGlobalStyle<{ .ant-notification-notice-icon-error.anticon { color: ${props => props.$errorIconColor || '#ff4d4f'}; } + + .ant-notification-notice-progress { + ${props => props.$progressHeight ? `height: ${props.$progressHeight};` : ''} + ${props => props.$progressBackground ? `background: ${props.$progressBackground};` : ''} + &::-webkit-progress-bar { + background: ${props => props.$progressBackground || '#e8e8e8'}; + } + &::-webkit-progress-value { + background: ${props => props.$progressColor || '#1890ff'}; + } + &::-moz-progress-bar { + background: ${props => props.$progressColor || '#1890ff'}; + } + } } `; @@ -124,6 +141,7 @@ const childrenMap = { // Layout width: withDefault(StringControl, ""), + progressHeight: withDefault(StringControl, ""), // Event handlers onEvent: eventHandlerControl(ToastEventOptions), @@ -138,6 +156,8 @@ const childrenMap = { successIconColor: "", warningIconColor: "", errorIconColor: "", + progressColor: "", + progressBackground: "", border: "", radius: "", borderWidth: "", @@ -296,6 +316,11 @@ const ToastPropertyView = React.memo((props: { comp: any }) => { tooltip: trans("toastComp.widthTooltip"), placeholder: "384px or 100vw", })} + {comp.children.progressHeight.propertyView({ + label: trans("toastComp.progressHeight"), + tooltip: trans("toastComp.progressHeightTooltip"), + placeholder: "4px", + })}
@@ -318,6 +343,7 @@ 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 progressHeight = comp.children.progressHeight.getView() as string; const instanceId = useId().replace(/:/g, '-'); // Store instance ID and resolved styles @@ -342,6 +368,9 @@ const ToastRuntimeView = React.memo((props: { comp: any }) => { $successIconColor={style.successIconColor} $warningIconColor={style.warningIconColor} $errorIconColor={style.errorIconColor} + $progressColor={style.progressColor} + $progressBackground={style.progressBackground} + $progressHeight={progressHeight || undefined} $border={style.border} $borderWidth={style.borderWidth} $borderStyle={style.borderStyle} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 09cc320b7a..fcbfa3ce03 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3301,11 +3301,15 @@ export const en = { "behavior": "Behavior", "width": "Width", "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", + "progressHeight": "Progress Bar Height", + "progressHeightTooltip": "Height of the progress bar. Example: 4px, 8px", "closeIconColor": "Close Icon Color", "infoIconColor": "Info Icon Color", "successIconColor": "Success Icon Color", "warningIconColor": "Warning Icon Color", "errorIconColor": "Error Icon Color", + "progressColor": "Progress Bar Color", + "progressBackground": "Progress Bar Background", // Type options "typeInfo": "Info", From bbe7f746c0df08335918afe68cfc245d19c88eea Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 5 Feb 2026 19:40:10 +0500 Subject: [PATCH 17/40] [Feat]: add support of multiple input formats for date component --- .../src/comps/comps/dateComp/dateCompUtil.ts | 3 +++ .../src/comps/comps/dateComp/dateRangeUIView.tsx | 15 +++++---------- .../src/comps/comps/dateComp/dateUIView.tsx | 6 +++--- .../lowcoder/src/comps/utils/propertyUtils.tsx | 2 +- client/packages/lowcoder/src/i18n/locales/en.ts | 1 + 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateCompUtil.ts b/client/packages/lowcoder/src/comps/comps/dateComp/dateCompUtil.ts index 16bc634edf..ed0b794ecf 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateCompUtil.ts +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateCompUtil.ts @@ -171,6 +171,9 @@ export const getMobileStyle = (style: DateTimeStyleType) => export const dateRefMethods = refMethods([focusMethod, blurMethod]); +export const parseInputFormats = (inputFormat?: string): string | string[] => + inputFormat?.includes(',') ? inputFormat.split(',').map(f => f.trim()) : inputFormat || ''; + export const StyledPickerPanel = styled.div<{ $style: ChildrenMultiSelectStyleType }>` diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx index 65677b63bb..7e0c7bd5d1 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import type { DateCompViewProps } from "./dateComp"; -import { disabledDate, getStyle, StyledPickerPanel } from "comps/comps/dateComp/dateCompUtil"; +import { disabledDate, getStyle, StyledPickerPanel, parseInputFormats } from "comps/comps/dateComp/dateCompUtil"; import { useUIView } from "../../utils/useUIView"; import { checkIsMobile } from "util/commonUtils"; import React, { useContext } from "react"; @@ -68,20 +68,15 @@ export interface DateRangeUIViewProps extends DateCompViewProps { export const DateRangeUIView = (props: DateRangeUIViewProps) => { const editorState = useContext(EditorContext); + const placeholders: [string, string] = Array.isArray(props.placeholder) + ? props.placeholder + : [props.placeholder || 'Start Date', props.placeholder || 'End Date']; - // Extract or compute the placeholder values - let placeholders: [string, string]; - if (Array.isArray(props.placeholder)) { - placeholders = props.placeholder; - } else { - // Use the same placeholder for both start and end if it's a single string - placeholders = [props.placeholder || 'Start Date', props.placeholder || 'End Date']; - } return useUIView( , export const DateUIView = (props: DataUIViewProps) => { const editorState = useContext(EditorContext); - const placeholder = Array.isArray(props.placeholder) ? props.placeholder[0] : props.placeholder; + return useUIView( , - {trans("date.reference")}   + {trans("date.inputFormatTip")}   Date: Fri, 6 Feb 2026 20:55:32 +0500 Subject: [PATCH 18/40] refactor toast component + remove unnecessary code --- .../lowcoder/src/comps/hooks/toastComp.tsx | 504 ------------------ .../hooks/toastComp/ToastPropertyView.tsx | 72 +++ .../hooks/toastComp/ToastRuntimeView.tsx | 44 ++ .../src/comps/hooks/toastComp/index.tsx | 2 + .../src/comps/hooks/toastComp/toastComp.tsx | 122 +++++ .../comps/hooks/toastComp/toastConstants.ts | 78 +++ .../src/comps/hooks/toastComp/toastStyles.tsx | 78 +++ .../src/comps/hooks/toastComp/toastUtils.ts | 104 ++++ 8 files changed, 500 insertions(+), 504 deletions(-) delete mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp.tsx create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/ToastPropertyView.tsx create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/ToastRuntimeView.tsx create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/index.tsx create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/toastComp.tsx create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/toastConstants.ts create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/toastStyles.tsx create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp/toastUtils.ts diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp.tsx deleted file mode 100644 index 6dc8056399..0000000000 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.tsx +++ /dev/null @@ -1,504 +0,0 @@ -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 { 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"; -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, { useEffect, useId } from "react"; -import { stateComp } from "comps/generators/simpleGenerators"; -import { isEqual } from "lodash"; -import { createGlobalStyle } from "styled-components"; - - -const ToastGlobalStyle = createGlobalStyle<{ - $instanceId: string; - $background?: string; - $textColor?: string; - $closeIconColor?: string; - $infoIconColor?: string; - $successIconColor?: string; - $warningIconColor?: string; - $errorIconColor?: string; - $progressColor?: string; - $progressBackground?: string; - $progressHeight?: string; - $border?: string; - $borderWidth?: string; - $borderStyle?: string; - $radius?: string; - $margin?: string; - $padding?: string; - $width?: string; -}>` - .ant-notification .ant-notification-notice-wrapper:has(.lowcoder-toast-${props => props.$instanceId}) { - background: ${props => props.$background || 'inherit'}; - 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.$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'}; - } - - .ant-notification-notice-progress { - ${props => props.$progressHeight ? `height: ${props.$progressHeight};` : ''} - ${props => props.$progressBackground ? `background: ${props.$progressBackground};` : ''} - &::-webkit-progress-bar { - background: ${props => props.$progressBackground || '#e8e8e8'}; - } - &::-webkit-progress-value { - background: ${props => props.$progressColor || '#1890ff'}; - } - &::-moz-progress-bar { - background: ${props => props.$progressColor || '#1890ff'}; - } - } - } -`; - -// 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; - -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; - -const ToastEventOptions = [ - { label: trans("toastComp.click"), value: "click", description: trans("toastComp.clickDesc") }, - { label: trans("toastComp.close"), value: "close", description: trans("toastComp.closeDesc") }, -] as const; - -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), - - // Layout - width: withDefault(StringControl, ""), - progressHeight: withDefault(StringControl, ""), - - // Event handlers - onEvent: eventHandlerControl(ToastEventOptions), - - // Style - style: styleControl(NotificationStyle), - resolvedStyle: stateComp({ - background: "", - color: "", - closeIconColor: "", - infoIconColor: "", - successIconColor: "", - warningIconColor: "", - errorIconColor: "", - progressColor: "", - progressBackground: "", - border: "", - radius: "", - borderWidth: "", - borderStyle: "", - margin: "", - padding: "", - }), - - // 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"; - -// 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; - styleConfig?: NotificationStyleType; - style?: React.CSSProperties; - instanceId: 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, - className: `lowcoder-toast-${config.instanceId}`, - style: config.style, - 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, - style, - } = 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, - 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(); - 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.width.propertyView({ - label: trans("toastComp.width"), - tooltip: trans("toastComp.widthTooltip"), - placeholder: "384px or 100vw", - })} - {comp.children.progressHeight.propertyView({ - label: trans("toastComp.progressHeight"), - tooltip: trans("toastComp.progressHeightTooltip"), - placeholder: "4px", - })} -
- -
- {comp.children.onEvent.getPropertyView()} -
- -
- {comp.children.style.getPropertyView()} -
- - ); -}); - -ToastPropertyView.displayName = "ToastPropertyView"; - -/** - * 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 progressHeight = comp.children.progressHeight.getView() as string; - 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; - if (!isEqual(style, current)) { - comp.children.resolvedStyle.dispatchChangeValueAction(style); - } - }, [comp, style]); - - return ( - - ); -}); - -ToastRuntimeView.displayName = "ToastRuntimeView"; - -// Build the component -let ToastCompBase = simpleMultiComp(childrenMap); - -ToastCompBase = withViewFn(ToastCompBase, (comp) => ); - -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")), - new NameConfig("width", trans("toastComp.widthDesc")), -]); - -// 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(), - styleConfig: comp.children.resolvedStyle.getView() as NotificationStyleType, - instanceId: comp.children.instanceId.getView() as string, - }; - - 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/hooks/toastComp/ToastPropertyView.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp/ToastPropertyView.tsx new file mode 100644 index 0000000000..d064c03be7 --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/ToastPropertyView.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { Section, sectionNames } from "lowcoder-design"; +import { trans } from "i18n"; + +/** + * Property view for toast component configuration + */ +export 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.width.propertyView({ + label: trans("toastComp.width"), + tooltip: trans("toastComp.widthTooltip"), + placeholder: "384px or 100vw", + })} + {comp.children.progressHeight.propertyView({ + label: trans("toastComp.progressHeight"), + tooltip: trans("toastComp.progressHeightTooltip"), + placeholder: "4px", + })} +
+ +
+ {comp.children.onEvent.getPropertyView()} +
+ +
+ {comp.children.style.getPropertyView()} +
+ + ); +}); + +ToastPropertyView.displayName = "ToastPropertyView"; diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp/ToastRuntimeView.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp/ToastRuntimeView.tsx new file mode 100644 index 0000000000..891a138d7b --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/ToastRuntimeView.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useId } from "react"; +import { NotificationStyleType } from "comps/controls/styleControlConstants"; +import { ToastGlobalStyle } from "./toastStyles"; + +/** + * Toast runtime view - injects global CSS styles for the notification. + */ +export 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 progressHeight = comp.children.progressHeight.getView() as string; + const instanceId = useId().replace(/:/g, '-'); + + // Store instance ID so the show() method can use it for the notification className + useEffect(() => { + comp.children.instanceId.dispatchChangeValueAction(instanceId); + }, [comp, instanceId]); + + return ( + + ); +}); + +ToastRuntimeView.displayName = "ToastRuntimeView"; diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp/index.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp/index.tsx new file mode 100644 index 0000000000..1c10c86813 --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/index.tsx @@ -0,0 +1,2 @@ +export { ToastComp } from "./toastComp"; +export type { ToastType } from "./toastConstants"; diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp/toastComp.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp/toastComp.tsx new file mode 100644 index 0000000000..124d83a8be --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/toastComp.tsx @@ -0,0 +1,122 @@ +import { simpleMultiComp, withPropertyViewFn, withViewFn } from "comps/generators"; +import { withMethodExposing } from "comps/generators/withMethodExposing"; +import { withExposingConfigs } from "comps/generators/withExposing"; +import { notificationInstance } from "lowcoder-design"; +import { trans } from "i18n"; +import type { NotificationPlacement } from "antd/es/notification/interface"; +import React from "react"; + +import { childrenMap, showParams, closeParams, ToastType } from "./toastConstants"; +import { showNotificationWithEvents, showNotificationProgrammatic, ToastConfig } from "./toastUtils"; +import { ToastPropertyView } from "./ToastPropertyView"; +import { ToastRuntimeView } from "./ToastRuntimeView"; + +// Build the component +let ToastCompBase = simpleMultiComp(childrenMap); + +ToastCompBase = withViewFn(ToastCompBase, (comp) => ); + +ToastCompBase = withPropertyViewFn(ToastCompBase, (comp) => ( + +)); + + +let ToastCompWithExposing = withExposingConfigs(ToastCompBase, []); + +// Add method exposing +export let ToastComp = withMethodExposing(ToastCompWithExposing, [ + { + method: { + name: "show", + description: trans("toastComp.showMethod"), + params: [], + }, + execute: (comp) => { + const config: ToastConfig = { + 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(), + instanceId: comp.children.instanceId.getView() as string, + }; + + 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/hooks/toastComp/toastConstants.ts b/client/packages/lowcoder/src/comps/hooks/toastComp/toastConstants.ts new file mode 100644 index 0000000000..b69ee7a904 --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/toastConstants.ts @@ -0,0 +1,78 @@ +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 { styleControl } from "comps/controls/styleControl"; +import { NotificationStyle } from "comps/controls/styleControlConstants"; +import { withDefault } from "comps/generators"; +import { stateComp } from "comps/generators/simpleGenerators"; +import { ParamsConfig } from "comps/controls/actionSelector/executeCompTypes"; +import { trans } from "i18n"; + +// Toast type +export type ToastType = "info" | "success" | "warning" | "error"; + +// Toast type options +export 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 +export 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 +export 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 params +export const showParams: ParamsConfig = [ + { name: "text", type: "string" }, + { name: "options", type: "JSON" }, +]; + +export const closeParams: ParamsConfig = [ + { name: "key", type: "string" }, +]; + +// Children map for toast component configuration +export 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), + + // Layout + width: withDefault(StringControl, ""), + progressHeight: withDefault(StringControl, ""), + + // Event handlers + onEvent: eventHandlerControl(ToastEventOptions), + + // Style (applied via global CSS in ToastRuntimeView) + style: styleControl(NotificationStyle), + + // Internal state for tracking visibility + visible: stateComp(false), + + // Unique instance ID for scoped styling (set by ToastRuntimeView) + instanceId: stateComp(""), +}; diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp/toastStyles.tsx b/client/packages/lowcoder/src/comps/hooks/toastComp/toastStyles.tsx new file mode 100644 index 0000000000..95d1a9c9ca --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/toastStyles.tsx @@ -0,0 +1,78 @@ +import { createGlobalStyle } from "styled-components"; + +export interface ToastStyleProps { + $instanceId: string; + $background?: string; + $textColor?: string; + $closeIconColor?: string; + $infoIconColor?: string; + $successIconColor?: string; + $warningIconColor?: string; + $errorIconColor?: string; + $progressColor?: string; + $progressBackground?: string; + $progressHeight?: string; + $border?: string; + $borderWidth?: string; + $borderStyle?: string; + $radius?: string; + $margin?: string; + $padding?: string; + $width?: string; +} + +export const ToastGlobalStyle = createGlobalStyle` + .ant-notification .ant-notification-notice-wrapper:has(.lowcoder-toast-${props => props.$instanceId}) { + background: ${props => props.$background || 'inherit'}; + 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.$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'}; + } + + .ant-notification-notice-progress { + ${props => props.$progressHeight ? `height: ${props.$progressHeight};` : ''} + ${props => props.$progressBackground ? `background: ${props.$progressBackground};` : ''} + &::-webkit-progress-bar { + background: ${props => props.$progressBackground || '#e8e8e8'}; + } + &::-webkit-progress-value { + background: ${props => props.$progressColor || '#1890ff'}; + } + &::-moz-progress-bar { + background: ${props => props.$progressColor || '#1890ff'}; + } + } + } +`; diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp/toastUtils.ts b/client/packages/lowcoder/src/comps/hooks/toastComp/toastUtils.ts new file mode 100644 index 0000000000..27b51ec407 --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp/toastUtils.ts @@ -0,0 +1,104 @@ +import { notificationInstance } from "lowcoder-design"; +import type { ArgsProps, NotificationPlacement } from "antd/es/notification/interface"; +import { EvalParamType } from "comps/controls/actionSelector/executeCompTypes"; +import { JSONObject } from "util/jsonTypes"; +import React from "react"; +import { ToastType } from "./toastConstants"; + +export interface ToastConfig { + title: string; + description: string; + type: ToastType; + duration: number; + placement: NotificationPlacement; + dismissible: boolean; + showProgress: boolean; + pauseOnHover: boolean; + key?: string; + style?: React.CSSProperties; + instanceId: string; +} + +/** + * Show notification with event callbacks. + */ +export const showNotificationWithEvents = ( + config: ToastConfig, + onEvent: (eventName: "click" | "close") => Promise, + setVisible: (visible: boolean) => void +): string => { + 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, + className: `lowcoder-toast-${config.instanceId}`, + style: config.style, + 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; +}; + +/** + * Show notification programmatically (for method API like toast1.info(), toast1.success(), etc.) + */ +export const showNotificationProgrammatic = ( + params: EvalParamType[], + level: ToastType, + comp: any +): string => { + const text = params?.[0] as string; + const options = (params?.[1] as JSONObject) || {}; + + const { + description, + duration, + key, + placement, + dismissible, + showProgress, + pauseOnHover, + style, + } = options; + + // Use component config as defaults, override with params + const config: ToastConfig = { + 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, + style: style as React.CSSProperties | undefined, + instanceId: comp.children.instanceId.getView() as string, + }; + + const onEvent = comp.children.onEvent.getView(); + const setVisible = (visible: boolean) => { + comp.children.visible.dispatchChangeValueAction(visible); + }; + + return showNotificationWithEvents(config, onEvent, setVisible); +}; From 9345b1082927e995723cfbb8d41e87f6be407dca Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Feb 2026 00:26:35 +0500 Subject: [PATCH 19/40] [Fix]: pie chart tooltip --- .../src/comps/pieChartComp/pieChartUtils.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartUtils.ts b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartUtils.ts index 5453933397..c72fe4a32d 100644 --- a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartUtils.ts +++ b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartUtils.ts @@ -172,15 +172,7 @@ export function getEchartsConfig( } }, tooltip: props.tooltip && { - trigger: "axis", - axisPointer: { - type: "line", - lineStyle: { - color: "rgba(0,0,0,0.2)", - width: 2, - type: "solid" - } - } + trigger: "item", }, grid: { ...gridPos, From 56dd3fde9f989e06e877bf7c1eae59e081efc5d0 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Feb 2026 19:17:57 +0500 Subject: [PATCH 20/40] add support for high resolution images in fileComp --- .../comps/fileComp/ImageCaptureModal.tsx | 24 ++++++++++++++----- .../comps/comps/fileComp/draggerUpload.tsx | 4 +++- .../src/comps/comps/fileComp/fileComp.tsx | 22 +++++++++++++++++ .../packages/lowcoder/src/i18n/locales/en.ts | 6 +++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx index ea35e2c529..350a1d07b9 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useCallback, useEffect, useRef, useState } from "react"; +import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { default as Button } from "antd/es/button"; import Dropdown from "antd/es/dropdown"; import type { ItemType } from "antd/es/menu/interface"; @@ -8,6 +8,7 @@ import Flex from "antd/es/flex"; import styled from "styled-components"; import { trans } from "i18n"; import { CustomModal } from "lowcoder-design"; +import { CaptureResolution, RESOLUTION_CONSTRAINTS } from "./fileComp"; const CustomModalStyled = styled(CustomModal)` top: 10vh; @@ -53,23 +54,32 @@ const ReactWebcam = React.lazy(() => import("react-webcam")); export const ImageCaptureModal = (props: { showModal: boolean; + captureResolution?: CaptureResolution; onModalClose: () => void; onImageCapture: (image: string) => void; }) => { const [errMessage, setErrMessage] = useState(""); - const [videoConstraints, setVideoConstraints] = useState({ - facingMode: "environment", - }); + const [selectedDeviceId, setSelectedDeviceId] = useState(null); const [modeList, setModeList] = useState([]); const [dropdownShow, setDropdownShow] = useState(false); const [imgSrc, setImgSrc] = useState(); const webcamRef = useRef(null); + const resolution = props.captureResolution ?? "auto"; + const resolutionSize = RESOLUTION_CONSTRAINTS[resolution] ?? {}; + + const videoConstraints = useMemo(() => { + const base: MediaTrackConstraints = selectedDeviceId + ? { deviceId: { exact: selectedDeviceId } } + : { facingMode: "environment" }; + return { ...base, ...resolutionSize }; + }, [selectedDeviceId, resolutionSize]); + useEffect(() => { if (props.showModal) { setImgSrc(""); setErrMessage(""); - setVideoConstraints({ facingMode: "environment" }); + setSelectedDeviceId(null); setDropdownShow(false); } }, [props.showModal]); @@ -125,6 +135,8 @@ export const ImageCaptureModal = (props: { ref={webcamRef} onUserMediaError={handleMediaErr} screenshotFormat="image/jpeg" + screenshotQuality={1} + forceScreenshotSourceSize videoConstraints={videoConstraints} /> @@ -172,7 +184,7 @@ export const ImageCaptureModal = (props: { { - setVideoConstraints({ deviceId: { exact: value.key } }); + setSelectedDeviceId(value.key); setDropdownShow(false); }} /> diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx index 22f08989d7..19aef9aecc 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx @@ -11,7 +11,7 @@ import { multiChangeAction, } from "lowcoder-core"; import { hasIcon } from "comps/utils"; -import { resolveValue, resolveParsedValue, commonProps, validateFile } from "./fileComp"; +import { resolveValue, resolveParsedValue, commonProps, validateFile, CaptureResolution } from "./fileComp"; import { FileStyleType, AnimationStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { ImageCaptureModal } from "./ImageCaptureModal"; import { v4 as uuidv4 } from "uuid"; @@ -148,6 +148,7 @@ interface DraggerUploadProps { prefixIcon: any; suffixIcon: any; forceCapture: boolean; + captureResolution: CaptureResolution; minSize: number; maxSize: number; maxFiles: number; @@ -291,6 +292,7 @@ export const DraggerUpload = (props: DraggerUploadProps) => { setShowModal(false)} onImageCapture={async (image) => { setShowModal(false); diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx index d18f7c8228..b804c53410 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx @@ -100,6 +100,22 @@ const validationChildren = { fileNamePattern: StringControl, }; +export type CaptureResolution = "auto" | "1080p" | "720p" | "480p"; + +export const CaptureResolutionOptions = [ + { label: trans("file.captureResolutionAuto"), value: "auto" }, + { label: trans("file.captureResolution1080p"), value: "1080p" }, + { label: trans("file.captureResolution720p"), value: "720p" }, + { label: trans("file.captureResolution480p"), value: "480p" }, +] as const; + +export const RESOLUTION_CONSTRAINTS: Record = { + auto: {}, + "1080p": { width: 1920, height: 1080 }, + "720p": { width: 1280, height: 720 }, + "480p": { width: 640, height: 480 }, +}; + const commonChildren = { value: stateComp>([]), files: stateComp([]), @@ -114,6 +130,7 @@ const commonChildren = { prefixIcon: withDefault(IconControl, "/icon:solid/arrow-up-from-bracket"), suffixIcon: IconControl, forceCapture: BoolControl, + captureResolution: dropdownControl(CaptureResolutionOptions, "auto"), ...validationChildren, }; @@ -441,6 +458,7 @@ const Upload = ( setShowModal(false)} onImageCapture={async (image) => { setShowModal(false); @@ -548,6 +566,10 @@ let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => { label: trans("file.forceCapture"), tooltip: trans("file.forceCaptureTooltip") })} + {children.forceCapture.getView() && children.captureResolution.propertyView({ + label: trans("file.captureResolution"), + tooltip: trans("file.captureResolutionTooltip"), + })} {children.showUploadList.propertyView({ label: trans("file.showUploadList") })} {children.parseFiles.propertyView({ label: trans("file.parseFiles"), diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 7bccf6aad5..329724bced 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1954,6 +1954,12 @@ export const en = { "fileNamePatternPlaceholder": "^[a-zA-Z0-9_-]+\\.[a-z]+$", "fileNamePatternErrorMsg": "Upload Failed. The File Name Does Not Match the Required Pattern.", "invalidFileNamePatternMsg": "Invalid File Name Pattern: {error}", + "captureResolution": "Capture Resolution", + "captureResolutionTooltip": "Set the camera resolution for image capture. Higher resolutions produce better quality but may not be supported by all cameras.", + "captureResolutionAuto": "Auto (Camera Default)", + "captureResolution1080p": "1080p", + "captureResolution720p": "720p", + "captureResolution480p": "480p", }, "date": { "format": "Format", From c10f018fe84089d12031b5143ac561caf081ca47 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 10 Mar 2026 19:07:18 +0500 Subject: [PATCH 21/40] [Feat]: Add multiline column type --- .../src/components/table/EditableCell.tsx | 34 ++++-- .../comps/tableComp/column/columnTypeComp.tsx | 6 + .../columnMultilineTextComp.tsx | 105 ++++++++++++++++++ .../packages/lowcoder/src/i18n/locales/en.ts | 5 + 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMultilineTextComp.tsx diff --git a/client/packages/lowcoder/src/components/table/EditableCell.tsx b/client/packages/lowcoder/src/components/table/EditableCell.tsx index b886167448..4bc15fedc5 100644 --- a/client/packages/lowcoder/src/components/table/EditableCell.tsx +++ b/client/packages/lowcoder/src/components/table/EditableCell.tsx @@ -54,6 +54,8 @@ export type EditViewFn = (props: { value: T; onChange: (value: T) => void; onChangeEnd: () => void; + onCommit?: (value: T) => void; + onCancel?: () => void; onImmediateSave?: (value: T) => void; otherProps?: Record; }) => ReactNode; @@ -152,11 +154,11 @@ function EditableCellComp(props: EditableCellProps) { [] ); - const onChangeEnd = useCallback(() => { + const commitValue = useCallback((finalValue: T | null) => { if (!mountedRef.current) return; - + setIsEditing(false); - const newValue = _.isNil(tmpValue) || _.isEqual(tmpValue, baseValue) ? null : tmpValue; + const newValue = _.isNil(finalValue) || _.isEqual(finalValue, baseValue) ? null : finalValue; dispatch( changeChildAction( "changeValue", @@ -164,10 +166,26 @@ function EditableCellComp(props: EditableCellProps) { false ) ); - if(!_.isEqual(tmpValue, value)) { + if(!_.isEqual(finalValue, value)) { onTableEvent?.('columnEdited'); } - }, [dispatch, tmpValue, baseValue, value, onTableEvent, setIsEditing]); + }, [dispatch, baseValue, value, onTableEvent, setIsEditing]); + + const onChangeEnd = useCallback(() => { + commitValue(tmpValue); + }, [commitValue, tmpValue]); + + const onCommit = useCallback((nextValue: T) => { + if (!mountedRef.current) return; + setTmpValue(nextValue); + commitValue(nextValue); + }, [commitValue]); + + const onCancel = useCallback(() => { + if (!mountedRef.current) return; + setIsEditing(false); + setTmpValue(value); + }, [setIsEditing, value]); const onImmediateSave = useCallback((newValue: T) => { if (!mountedRef.current) return; @@ -187,8 +205,8 @@ function EditableCellComp(props: EditableCellProps) { }, [dispatch, baseValue, value, onTableEvent]); const editView = useMemo( - () => editViewFn?.({ value, onChange, onChangeEnd, onImmediateSave, otherProps }) ?? <>, - [editViewFn, value, onChange, onChangeEnd, onImmediateSave, otherProps] + () => editViewFn?.({ value, onChange, onChangeEnd, onCommit, onCancel, onImmediateSave, otherProps }) ?? <>, + [editViewFn, value, onChange, onChangeEnd, onCommit, onCancel, onImmediateSave, otherProps] ); const enterEditFn = useCallback(() => { @@ -243,4 +261,4 @@ function EditableCellComp(props: EditableCellProps) { ); } -export const EditableCell = React.memo(EditableCellComp) as typeof EditableCellComp; \ No newline at end of file +export const EditableCell = React.memo(EditableCellComp) as typeof EditableCellComp; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx index e2e82f58db..07c31de474 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx @@ -23,6 +23,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp"; import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp"; import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp"; import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp"; +import { ColumnMultilineTextComp } from "./columnTypeComps/columnMultilineTextComp"; const actionOptions = [ { @@ -106,6 +107,10 @@ const actionOptions = [ label: "Password", value: "password", }, + { + label: trans("table.multilineText"), + value: "multilineText", + }, ] as const; export const ColumnTypeCompMap = { @@ -129,6 +134,7 @@ export const ColumnTypeCompMap = { date: DateComp, time: TimeComp, password: ColumnPasswordComp, + multilineText: ColumnMultilineTextComp, }; type ColumnTypeMapType = typeof ColumnTypeCompMap; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMultilineTextComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMultilineTextComp.tsx new file mode 100644 index 0000000000..5006e86f3f --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMultilineTextComp.tsx @@ -0,0 +1,105 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { default as AntdModal } from "antd/es/modal"; +import { default as Input } from "antd/es/input"; +import { StringOrNumberControl } from "comps/controls/codeControl"; +import { trans } from "i18n"; +import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; +import { ColumnValueTooltip } from "../simpleColumnTypeComps"; +import { RecordConstructorToComp } from "lowcoder-core"; +import styled from "styled-components"; + +const { TextArea } = Input; + +const TextView = styled.div` + white-space: pre-wrap; + word-break: break-word; + cursor: pointer; +`; + +const childrenMap = { + text: StringOrNumberControl, +}; + +const getBaseValue: ColumnTypeViewFn = (props) => + typeof props.text === "string" ? props.text : String(props.text); + +const MultilineContent = React.memo(({ value }: { value: string }) => {value}); + +const MultilineEditModal = React.memo((props: { + value: string; + onCommit: (value: string) => void; + onCancel: () => void; +}) => { + const { value, onCommit, onCancel } = props; + const [localValue, setLocalValue] = useState(value); + const textAreaRef = useRef(null); + + useEffect(() => { + setLocalValue(value); + }, [value]); + + useEffect(() => { + const timeout = setTimeout(() => textAreaRef.current?.focus({ cursor: "end" }), 0); + return () => clearTimeout(timeout); + }, []); + + const handleSave = useCallback(() => { + onCommit(localValue); + }, [localValue, onCommit]); + + const handleCancel = useCallback(() => { + onCancel(); + }, [onCancel]); + + return ( + +