diff --git a/client/VERSION b/client/VERSION index 9aa34646dc..fbafd6b600 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.7.0 \ No newline at end of file +2.7.2 \ No newline at end of file diff --git a/client/package.json b/client/package.json index 1d539f23bc..12f93a4aa7 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-frontend", - "version": "2.7.0", + "version": "2.7.2", "type": "module", "private": true, "workspaces": [ diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 2819fd79ce..613c8ee3db 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "2.7.1", + "version": "2.7.2", "type": "module", "license": "MIT", "dependencies": { diff --git a/client/packages/lowcoder-design/src/components/Modal/handler.tsx b/client/packages/lowcoder-design/src/components/Modal/handler.tsx index c51a6858f9..2293236d65 100644 --- a/client/packages/lowcoder-design/src/components/Modal/handler.tsx +++ b/client/packages/lowcoder-design/src/components/Modal/handler.tsx @@ -1,5 +1,5 @@ import styled, { css } from "styled-components"; -import { memo, useMemo } from "react"; +import { RefObject } from "react"; type ResizeHandleAxis = "s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne"; type ReactRef = { @@ -84,10 +84,9 @@ const ResizeHandle = styled.div<{ $axis: string }>` ${(props) => (["sw", "nw", "se", "ne"].indexOf(props.$axis) >= 0 ? CornerHandle : "")}; `; -// Memoize Handle component -const Handle = memo((axis: ResizeHandleAxis, ref: ReactRef) => { - return ; -}); +const Handle = (resizeHandle: ResizeHandleAxis, ref: RefObject) => { + return ; +}; Handle.displayName = 'Handle'; diff --git a/client/packages/lowcoder-design/src/icons/index.tsx b/client/packages/lowcoder-design/src/icons/index.tsx index 94453db48b..b033d52e92 100644 --- a/client/packages/lowcoder-design/src/icons/index.tsx +++ b/client/packages/lowcoder-design/src/icons/index.tsx @@ -255,7 +255,7 @@ export { ReactComponent as RecyclerIcon } from "./remix/delete-bin-line.svg"; export { ReactComponent as MarketplaceIcon } from "./v1/icon-application-marketplace.svg"; export { ReactComponent as FavoritesIcon } from "./v1/icon-application-favorites.svg"; export { ReactComponent as HomeSettingIcon } from "./remix/settings-4-line.svg"; -export { ReactComponent as EnterpriseIcon } from "./remix/earth-line.svg"; +export { ReactComponent as EnterpriseIcon } from "./remix/shield-star-line.svg"; export { ReactComponent as VerticalIcon } from "./remix/vertical.svg"; export { ReactComponent as HorizontalIcon } from "./remix/horizontal.svg"; diff --git a/client/packages/lowcoder-sdk-webpack-bundle/package.json b/client/packages/lowcoder-sdk-webpack-bundle/package.json index b9266b7bd5..267fb8fb36 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/package.json +++ b/client/packages/lowcoder-sdk-webpack-bundle/package.json @@ -1,7 +1,7 @@ { "name": "lowcoder-sdk-webpack-bundle", "description": "", - "version": "2.7.0", + "version": "2.7.2", "main": "index.jsx", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/client/packages/lowcoder-sdk/package.json b/client/packages/lowcoder-sdk/package.json index e901e98aac..7cc5d5ea49 100644 --- a/client/packages/lowcoder-sdk/package.json +++ b/client/packages/lowcoder-sdk/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-sdk", - "version": "2.7.0", + "version": "2.7.2", "type": "module", "files": [ "src", diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index f25be12c32..e838c870d8 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder", - "version": "2.7.0", + "version": "2.7.2", "private": true, "type": "module", "main": "src/index.sdk.ts", diff --git a/client/packages/lowcoder/src/api/newsApi.ts b/client/packages/lowcoder/src/api/newsApi.ts index 9da9202609..2d8c822e2c 100644 --- a/client/packages/lowcoder/src/api/newsApi.ts +++ b/client/packages/lowcoder/src/api/newsApi.ts @@ -132,7 +132,7 @@ export const getHubspotContent = async () => { }; try { const result = await NewsApi.secureRequest(apiBody); - return result?.data[0]?.hubspot?.length > 0 ? result.data[0].hubspot as any[] : []; + return result?.data[0]?.results?.length > 0 ? result.data[0].results as any[] : []; } catch (error) { console.error("Error getting news:", error); throw error; diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index a8211777db..2b33bf3766 100644 --- a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useEffect, useState, useCallback } from "react"; import { Input, Section, sectionNames } from "lowcoder-design"; import { BoolControl } from "comps/controls/boolControl"; import { styleControl } from "comps/controls/styleControl"; @@ -43,6 +43,7 @@ import { default as AutoComplete } from "antd/es/auto-complete"; import { RefControl } from "comps/controls/refControl"; import { booleanExposingStateControl, + jsonValueExposingStateControl, } from "comps/controls/codeStateControl"; import { getDayJSLocale } from "i18n/dayjsLocale"; @@ -78,6 +79,7 @@ const childrenMap = { prefixIcon: IconControl, suffixIcon: IconControl, items: jsonControl(convertAutoCompleteData, autoCompleteDate), + filterOptionsByInput: BoolControl.DEFAULT_TRUE, ignoreCase: BoolControl.DEFAULT_TRUE, searchFirstPY: BoolControl.DEFAULT_TRUE, searchCompletePY: BoolControl, @@ -87,6 +89,7 @@ const childrenMap = { autocompleteIconColor: dropdownControl(autocompleteIconColor, "blue"), componentSize: dropdownControl(componentSize, "small"), valueInItems: booleanExposingStateControl("valueInItems"), + selectedOption: jsonValueExposingStateControl("selectedOption", {}), style: styleControl(InputFieldStyle , 'style'), labelStyle: styleControl(LabelStyle , 'labelStyle'), inputFieldStyle: styleControl(InputLikeStyle , 'inputFieldStyle'), @@ -118,10 +121,11 @@ let AutoCompleteCompBase = (function () { autoCompleteType, autocompleteIconColor, componentSize, + filterOptionsByInput, } = props; - const getTextInputValidate = () => { + const getTextInputValidate = useCallback(() => { return { value: { value: props.value.value }, required: props.required, @@ -131,7 +135,15 @@ let AutoCompleteCompBase = (function () { regex: props.regex, customRule: props.customRule, }; - }; + }, [ + props.value.value, + props.required, + props.minLength, + props.maxLength, + props.validationType, + props.regex, + props.customRule, + ]); const [activationFlag, setActivationFlag] = useState(false); const [searchtext, setsearchtext] = useState(props.value.value); @@ -154,6 +166,117 @@ let AutoCompleteCompBase = (function () { props.customRule, ]); + const handleFilterOptions = useCallback((inputValue: string, option: any) => { + if (ignoreCase) { + if ( + option?.label && + option?.label + .toUpperCase() + .indexOf(inputValue.toUpperCase()) !== -1 + ) + return true; + } else { + if (option?.label && option?.label.indexOf(inputValue) !== -1) + return true; + } + if ( + chineseEnv && + searchFirstPY && + option?.label && + option.label + .spell("first") + .toString() + .toLowerCase() + .indexOf(inputValue.toLowerCase()) >= 0 + ) + return true; + if ( + chineseEnv && + searchCompletePY && + option?.label && + option.label + .spell() + .toString() + .toLowerCase() + .indexOf(inputValue.toLowerCase()) >= 0 + ) + return true; + if (!searchLabelOnly) { + if (ignoreCase) { + if ( + option?.value && + option?.value + .toUpperCase() + .indexOf(inputValue.toUpperCase()) !== -1 + ) + return true; + } else { + if ( + option?.value && + option?.value.indexOf(inputValue) !== -1 + ) + return true; + } + if ( + chineseEnv && + searchFirstPY && + option?.value && + option.value + .spell("first") + .toString() + .toLowerCase() + .indexOf(inputValue.toLowerCase()) >= 0 + ) + return true; + if ( + chineseEnv && + searchCompletePY && + option?.value && + option.value + .spell() + .toString() + .toLowerCase() + .indexOf(inputValue.toLowerCase()) >= 0 + ) + return true; + } + return false; + }, [filterOptionsByInput, ignoreCase, chineseEnv, searchFirstPY, searchCompletePY, searchLabelOnly]); + + const handleChange = useCallback((value: string) => { + props.valueInItems.onChange(false); + setvalidateState(textInputValidate(getTextInputValidate())); + setsearchtext(value); + props.value.onChange(value); + props.onEvent("change"); + if(!Boolean(value)) { + props.selectedOption.onChange({}); + } + }, [props.valueInItems, getTextInputValidate, props.value, props.onEvent, props.selectedOption]); + + const handleSelect = useCallback((data: string, option: any) => { + setsearchtext(option[valueOrLabel]); + props.valueInItems.onChange(true); + props.value.onChange(option[valueOrLabel]); + props.selectedOption.onChange(option); + props.onEvent("submit"); + }, [valueOrLabel, props.valueInItems, props.value, props.onEvent, props.selectedOption]); + + const handleFocus = useCallback(() => { + setActivationFlag(true); + props.onEvent("focus"); + }, [props.onEvent]); + + const handleBlur = useCallback(() => { + props.onEvent("blur"); + }, [props.onEvent]); + + const popupRender = useCallback((originNode: ReactNode) => ( + + {originNode} + + ), [props.childrenInputFieldStyle]); + return props.label({ required: props.required, children: ( @@ -163,117 +286,24 @@ let AutoCompleteCompBase = (function () { value={searchtext} options={items} style={{ width: "100%" }} - onChange={(value: string, option) => { - props.valueInItems.onChange(false); - setvalidateState(textInputValidate(getTextInputValidate())); - setsearchtext(value); - props.value.onChange(value); - props.onEvent("change") - }} - onFocus={() => { - setActivationFlag(true) - props.onEvent("focus") - }} - onBlur={() => props.onEvent("blur")} - onSelect={(data: string, option) => { - setsearchtext(option[valueOrLabel]); - props.valueInItems.onChange(true); - props.value.onChange(option[valueOrLabel]); - props.onEvent("submit"); - }} - filterOption={(inputValue: string, option) => { - if (ignoreCase) { - if ( - option?.label && - option?.label - .toUpperCase() - .indexOf(inputValue.toUpperCase()) !== -1 - ) - return true; - } else { - if (option?.label && option?.label.indexOf(inputValue) !== -1) - return true; - } - if ( - chineseEnv && - searchFirstPY && - option?.label && - option.label - .spell("first") - .toString() - .toLowerCase() - .indexOf(inputValue.toLowerCase()) >= 0 - ) - return true; - if ( - chineseEnv && - searchCompletePY && - option?.label && - option.label - .spell() - .toString() - .toLowerCase() - .indexOf(inputValue.toLowerCase()) >= 0 - ) - return true; - if (!searchLabelOnly) { - if (ignoreCase) { - if ( - option?.value && - option?.value - .toUpperCase() - .indexOf(inputValue.toUpperCase()) !== -1 - ) - return true; - } else { - if ( - option?.value && - option?.value.indexOf(inputValue) !== -1 - ) - return true; - } - if ( - chineseEnv && - searchFirstPY && - option?.value && - option.value - .spell("first") - .toString() - .toLowerCase() - .indexOf(inputValue.toLowerCase()) >= 0 - ) - return true; - if ( - chineseEnv && - searchCompletePY && - option?.value && - option.value - .spell() - .toString() - .toLowerCase() - .indexOf(inputValue.toLowerCase()) >= 0 - ) - return true; - } - return false; - }} - popupRender={(originNode: ReactNode) => ( - - {originNode} - - )} + onChange={handleChange} + onFocus={handleFocus} + onBlur={handleBlur} + onSelect={handleSelect} + filterOption={!filterOptionsByInput ? false : handleFilterOptions} + popupRender={popupRender} > - + ), @@ -289,54 +319,63 @@ let AutoCompleteCompBase = (function () { .setPropertyViewFn((children) => { return ( <> -
- {children.autoCompleteType.getView() === 'normal' && - children.prefixIcon.propertyView({ - label: trans('button.prefixIcon'), - })} - {children.autoCompleteType.getView() === 'normal' && - children.suffixIcon.propertyView({ - label: trans('button.suffixIcon'), - })} - {allowClearPropertyView(children)} -
+
{children.items.propertyView({ label: trans('autoComplete.value'), tooltip: itemsDataTooltip, placeholder: '[]', })} - {getDayJSLocale() === 'zh-cn' && + {children.filterOptionsByInput.propertyView({ + label: trans('autoComplete.filterOptionsByInput'), + })} + {children.filterOptionsByInput.getView() && getDayJSLocale() === 'zh-cn' && ( children.searchFirstPY.propertyView({ label: trans('autoComplete.searchFirstPY'), - })} - {getDayJSLocale() === 'zh-cn' && + }) + )} + {children.filterOptionsByInput.getView() && getDayJSLocale() === 'zh-cn' && ( children.searchCompletePY.propertyView({ label: trans('autoComplete.searchCompletePY'), - })} - {children.searchLabelOnly.propertyView({ + }) + )} + {children.filterOptionsByInput.getView() && children.searchLabelOnly.propertyView({ label: trans('autoComplete.searchLabelOnly'), })} - {children.ignoreCase.propertyView({ - label: trans('autoComplete.ignoreCase'), - })} + {children.filterOptionsByInput.getView() && ( + children.ignoreCase.propertyView({ + label: trans('autoComplete.ignoreCase'), + }) + )} {children.valueOrLabel.propertyView({ label: trans('autoComplete.checkedValueFrom'), radioButton: true, })}
- {children.label.getPropertyView()} - {}
{hiddenPropertyView(children)}
+ +
+ {children.autoCompleteType.getView() === 'normal' && + children.prefixIcon.propertyView({ + label: trans('button.prefixIcon'), + })} + {children.autoCompleteType.getView() === 'normal' && + children.suffixIcon.propertyView({ + label: trans('button.suffixIcon'), + })} + {allowClearPropertyView(children)} +
+ + {}
{children.style.getPropertyView()} @@ -356,9 +395,6 @@ let AutoCompleteCompBase = (function () { > {children.animationStyle.getPropertyView()}
-
- {children.tabIndex.propertyView({ label: trans("prop.tabIndex") })} -
); }) @@ -382,6 +418,7 @@ AutoCompleteCompBase = class extends AutoCompleteCompBase { export const AutoCompleteComp = withExposingConfigs(AutoCompleteCompBase, [ new NameConfig("value", trans("export.inputValueDesc")), new NameConfig("valueInItems", trans("autoComplete.valueInItems")), + new NameConfig("selectedOption", trans("autoComplete.selectedOption")), NameConfigPlaceHolder, NameConfigRequired, ...TextInputConfigs, diff --git a/client/packages/lowcoder/src/comps/comps/avatar.tsx b/client/packages/lowcoder/src/comps/comps/avatar.tsx index bbd39f73e8..94e24d59a4 100644 --- a/client/packages/lowcoder/src/comps/comps/avatar.tsx +++ b/client/packages/lowcoder/src/comps/comps/avatar.tsx @@ -25,6 +25,7 @@ import { IconControl } from "comps/controls/iconControl"; import { clickEvent, eventHandlerControl, + doubleClickEvent, } from "../controls/eventHandlerControl"; import { Avatar, AvatarProps, Badge, Dropdown, Menu } from "antd"; import { LeftRightControl, dropdownControl } from "../controls/dropdownControl"; @@ -34,6 +35,8 @@ import { BadgeBasicSection, badgeChildren } from "./badgeComp/badgeConstants"; import { DropdownOptionControl } from "../controls/optionsControl"; import { ReactElement, useContext, useEffect } from "react"; import { CompNameContext, EditorContext } from "../editorState"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; + const AvatarWrapper = styled(Avatar) ` background: ${(props) => props.$style.background}; @@ -106,7 +109,7 @@ padding: ${props=>props.$style.padding}; background: ${props=>props.$style.background}; text-decoration: ${props => props.$style.textDecoration}; ` -const EventOptions = [clickEvent] as const; +const EventOptions = [clickEvent, doubleClickEvent] as const; const sharpOptions = [ { label: trans("avatarComp.square"), value: "square" }, { label: trans("avatarComp.circle"), value: "circle" }, @@ -140,6 +143,8 @@ const childrenMap = { const AvatarView = (props: RecordConstructorToView) => { const { shape, title, src, iconSize } = props; const comp = useContext(EditorContext).getUICompByName(useContext(CompNameContext)); + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + // const eventsCount = comp ? Object.keys(comp?.children.comp.children.onEvent.children).length : 0; const hasIcon = props.options.findIndex((option) => (option.prefixIcon as ReactElement)?.props.value) > -1; const items = props.options @@ -181,8 +186,7 @@ const AvatarView = (props: RecordConstructorToView) => { shape={shape} $style={props.avatarStyle} src={src.value} - // $cursorPointer={eventsCount > 0} - onClick={() => props.onEvent("click")} + onClick={handleClickEvent} > {title.value} diff --git a/client/packages/lowcoder/src/comps/comps/avatarGroup.tsx b/client/packages/lowcoder/src/comps/comps/avatarGroup.tsx index 4cc2567c64..f370a4ef99 100644 --- a/client/packages/lowcoder/src/comps/comps/avatarGroup.tsx +++ b/client/packages/lowcoder/src/comps/comps/avatarGroup.tsx @@ -8,7 +8,7 @@ import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; import { NumberControl, StringControl } from "comps/controls/codeControl"; import { Avatar, Tooltip } from "antd"; -import { clickEvent, eventHandlerControl, refreshEvent } from "../controls/eventHandlerControl"; +import { clickEvent, doubleClickEvent, eventHandlerControl, refreshEvent } from "../controls/eventHandlerControl"; import styled from "styled-components"; import { useContext, ReactElement, useEffect } from "react"; import { MultiCompBuilder, stateComp, withDefault } from "../generators"; @@ -19,6 +19,7 @@ import { optionsControl } from "../controls/optionsControl"; import { BoolControl } from "../controls/boolControl"; import { dropdownControl } from "../controls/dropdownControl"; import { JSONObject } from "util/jsonTypes"; +import { useCompClickEventHandler } from "../utils/useCompClickEventHandler"; const MacaroneList = [ '#fde68a', @@ -77,7 +78,7 @@ const DropdownOption = new MultiCompBuilder( )) .build(); -const EventOptions = [clickEvent, refreshEvent] as const; +const EventOptions = [clickEvent, refreshEvent, doubleClickEvent] as const; export const alignOptions = [ { label: , value: "flex-start" }, @@ -105,6 +106,8 @@ const childrenMap = { }; const AvatarGroupView = (props: RecordConstructorToView & { dispatch: (action: CompAction) => void; }) => { + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + return ( & { }} size={props.avatarSize} onClick={() => { - props.onEvent("click") + handleClickEvent(); props.dispatch(changeChildAction("currentAvatar", item as JSONObject, false)); }} > diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/buttonComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/buttonComp.tsx index 6f657c1e84..70a8de5d83 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/buttonComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/buttonComp.tsx @@ -29,6 +29,7 @@ import { AnimationStyle } from "@lowcoder-ee/comps/controls/styleControlConstant import { styleControl } from "@lowcoder-ee/comps/controls/styleControl"; import { RecordConstructorToComp } from "lowcoder-core"; import { ToViewReturn } from "@lowcoder-ee/comps/generators/multi"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; const FormLabel = styled(CommonBlueLabel)` font-size: 13px; @@ -181,6 +182,7 @@ const ButtonPropertyView = React.memo((props: { const ButtonView = React.memo((props: ToViewReturn) => { const editorState = useContext(EditorContext); const mountedRef = useRef(true); + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}); useEffect(() => { return () => { @@ -193,7 +195,7 @@ const ButtonView = React.memo((props: ToViewReturn) => { try { if (isDefault(props.type)) { - props.onEvent("click"); + handleClickEvent(); } else { submitForm(editorState, props.form); } diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx index 223650ef48..358a1e6ff2 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { RecordConstructorToView } from "lowcoder-core"; import { BoolControl } from "comps/controls/boolControl"; import { stringExposingStateControl } from "comps/controls/codeStateControl"; @@ -16,7 +17,7 @@ import { IconControl } from "comps/controls/iconControl"; import styled from "styled-components"; import { ButtonEventHandlerControl } from "comps/controls/eventHandlerControl"; import { manualOptionsControl } from "comps/controls/optionsControl"; -import { useContext, useEffect } from "react"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; const StyledFloatButton = styled(FloatButton)<{ $animationStyle: AnimationStyleType; @@ -98,21 +99,51 @@ const childrenMap = { dot: BoolControl, }; +const FloatButtonItem = React.memo(({ + button, + animationStyle, + badgeStyle, + buttonTheme, + shape, + dot +}: { + button: any; + animationStyle: AnimationStyleType; + badgeStyle: BadgeStyleType; + buttonTheme: 'primary' | 'default'; + shape: 'circle' | 'square'; + dot: boolean; +}) => { + const handleClickEvent = useCompClickEventHandler({ onEvent: button.onEvent }); + + return ( + + ); +}); + const FloatButtonView = (props: RecordConstructorToView) => { const renderButton = (button: any, onlyOne?: boolean) => { return !button?.hidden ? ( - button.onEvent("click")} - tooltip={button?.label} - description={button?.description} - badge={{ count: button?.badge, color: props.badgeStyle.badgeColor, dot: props?.dot }} - type={onlyOne ? props.buttonTheme : 'default'} + button={button} + animationStyle={props.animationStyle} + badgeStyle={props.badgeStyle} + buttonTheme={onlyOne ? props.buttonTheme : 'default'} shape={props.shape} - />) - : '' + dot={props.dot} + /> + ) : ''; } return ( diff --git a/client/packages/lowcoder/src/comps/comps/commentComp/commentComp.tsx b/client/packages/lowcoder/src/comps/comps/commentComp/commentComp.tsx index 4fb21b69f5..f3b14959c9 100644 --- a/client/packages/lowcoder/src/comps/comps/commentComp/commentComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/commentComp/commentComp.tsx @@ -25,10 +25,11 @@ import { eventHandlerControl, deleteEvent, mentionEvent, -} from "comps/controls/eventHandlerControl"; - + doubleClickEvent, +} from "comps/controls/eventHandlerControl"; import { EditorContext } from "comps/editorState"; + // Introducing styles import { AnimationStyle, @@ -66,6 +67,7 @@ import dayjs from "dayjs"; // import "dayjs/locale/zh-cn"; import { getInitialsAndColorCode } from "util/stringUtils"; import { default as CloseOutlined } from "@ant-design/icons/CloseOutlined"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; dayjs.extend(relativeTime); // dayjs.locale("zh-cn"); @@ -80,6 +82,7 @@ dayjs.extend(relativeTime); const EventOptions = [ clickEvent, + doubleClickEvent, submitEvent, deleteEvent, mentionEvent, @@ -133,6 +136,8 @@ const CommentCompBase = ( const [commentListData, setCommentListData] = useState([]); const [prefix, setPrefix] = useState("@"); const [context, setContext] = useState(""); + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + // Integrate the comment list with the names in the original mention list const mergeAllMentionList = (mentionList: any) => { setMentionList( @@ -174,7 +179,7 @@ const CommentCompBase = ( const generateCommentAvatar = (item: commentDataTYPE) => { return ( props.onEvent("click")} + onClick={handleClickEvent} // If there is an avatar, no background colour is set, and if displayName is not null, displayName is called using getInitialsAndColorCode style={{ backgroundColor: item?.user?.avatar @@ -290,7 +295,9 @@ const CommentCompBase = ( props.onEvent("click")}> +
{item?.user?.name} { props.container.showHeader = false; - // 注入容器参数 props.container.style = Object.assign(props.container.style, { CONTAINER_BODY_PADDING: props.style.containerBodyPadding, border: '#00000000', @@ -205,6 +205,12 @@ export const ContainerBaseComp = (function () { const conRef = useRef(null); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + const actionHandlers = props.actionOptions.map(item => ({ + ...item, + clickHandler: useCompClickEventHandler({onEvent: item.onEvent}) + })); + useEffect(() => { if (height && width) { onResize(); @@ -233,7 +239,7 @@ export const ContainerBaseComp = (function () { $cardType={props.cardType} onMouseEnter={() => props.onEvent('focus')} onMouseLeave={() => props.onEvent('blur')} - onClick={() => props.onEvent('click')} + onClick={handleClickEvent} > } actions={props.cardType == 'common' && props.showActionIcon ? - props.actionOptions.filter(item => !item.hidden).map(item => { + actionHandlers.filter(item => !item.hidden).map(item => { return ( item.onEvent('click')} + onClick={(e) => { + e.stopPropagation() + item.clickHandler() + }} disabled={item.disabled} $style={props.style} > diff --git a/client/packages/lowcoder/src/comps/comps/iconComp.tsx b/client/packages/lowcoder/src/comps/comps/iconComp.tsx index 4ae9dcdd98..8cc3716e16 100644 --- a/client/packages/lowcoder/src/comps/comps/iconComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/iconComp.tsx @@ -27,11 +27,13 @@ import { AutoHeightControl } from "../controls/autoHeightControl"; import { clickEvent, eventHandlerControl, + doubleClickEvent, } from "../controls/eventHandlerControl"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; import { dropdownControl } from "../controls/dropdownControl"; +import { useCompClickEventHandler } from "../utils/useCompClickEventHandler"; const Container = styled.div<{ $sourceMode: string; @@ -72,7 +74,7 @@ const Container = styled.div<{ `} `; -const EventOptions = [clickEvent] as const; +const EventOptions = [clickEvent, doubleClickEvent] as const; const ModeOptions = [ { label: "Standard", value: "standard" }, @@ -94,6 +96,7 @@ const IconView = (props: RecordConstructorToView) => { const conRef = useRef(null); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) useEffect(() => { if (height && width) { @@ -134,7 +137,7 @@ const IconView = (props: RecordConstructorToView) => { $sourceMode={props.sourceMode} $animationStyle={props.animationStyle} style={style} - onClick={() => props.onEvent("click")} + onClick={handleClickEvent} > { props.sourceMode === 'standard' ? (props.icon || '') diff --git a/client/packages/lowcoder/src/comps/comps/imageComp.tsx b/client/packages/lowcoder/src/comps/comps/imageComp.tsx index ec4190bc6e..8bc246a2b1 100644 --- a/client/packages/lowcoder/src/comps/comps/imageComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/imageComp.tsx @@ -3,6 +3,7 @@ import { Section, sectionNames } from "lowcoder-design"; import { clickEvent, eventHandlerControl, + doubleClickEvent, } from "../controls/eventHandlerControl"; import { StringStateControl } from "../controls/codeStateControl"; import { UICompBuilder, withDefault } from "../generators"; @@ -37,6 +38,7 @@ import { StringControl } from "../controls/codeControl"; import { PositionControl } from "comps/controls/dropdownControl"; import { dropdownControl } from "../controls/dropdownControl"; import { AssetType, IconscoutControl } from "../controls/iconscoutControl"; +import { useCompClickEventHandler } from "../utils/useCompClickEventHandler"; const Container = styled.div<{ $style: ImageStyleType | undefined, @@ -112,7 +114,7 @@ const getStyle = (style: ImageStyleType) => { `; }; -const EventOptions = [clickEvent] as const; +const EventOptions = [clickEvent, doubleClickEvent] as const; const ModeOptions = [ { label: "URL", value: "standard" }, { label: "Asset Library", value: "asset-library" }, @@ -123,6 +125,8 @@ const ContainerImg = (props: RecordConstructorToView) => { const conRef = useRef(null); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + const imgOnload = (img: HTMLImageElement) => { img.onload = function () { @@ -211,7 +215,7 @@ const ContainerImg = (props: RecordConstructorToView) => { draggable={false} preview={props.supportPreview ? {src: props.previewSrc || props.src.value } : false} fallback={DEFAULT_IMG_URL} - onClick={() => props.onEvent("click")} + onClick={handleClickEvent} />
diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/JsonFormsRenderer.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/JsonFormsRenderer.tsx index cbdab8a0ff..bc1eaaf656 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/JsonFormsRenderer.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/JsonFormsRenderer.tsx @@ -15,13 +15,11 @@ import { Steps, } from "antd"; import styled from "styled-components"; -import type { JsonSchema } from "@jsonforms/core"; import type { JSONSchema7 } from "json-schema"; import { debounce } from "lodash"; import dayjs from "dayjs"; import { trans } from "i18n"; import type { - JsonFormsUiSchema, FieldUiSchema, Layout, Categorization, @@ -30,10 +28,14 @@ import type { Category, Control } from "./types"; -import type { SwitchChangeEventHandler } from "antd/es/switch"; +import { useContainerWidth } from "./jsonSchemaFormComp"; + const { TextArea } = Input; -const Container = styled.div` +const Container = styled.div +` + gap: 16px; + width: 100%; .ant-form-item { margin-bottom: 16px; } @@ -62,11 +64,6 @@ const Container = styled.div` } `; -interface HorizontalLayout { - type: "HorizontalLayout"; - elements: Control[]; -} - const JsonFormsRenderer: React.FC = ({ schema, data, @@ -78,6 +75,7 @@ const JsonFormsRenderer: React.FC = ({ validationState: externalValidationState, onValidationChange, }) => { + const containerWidth = useContainerWidth(); // Local state to handle immediate updates const [localData, setLocalData] = useState(data); // Track focused field @@ -116,7 +114,7 @@ const JsonFormsRenderer: React.FC = ({ if (!uiSchema) return undefined; // For JSONForms UI schema, we need to find the Control element that matches the path - if (uiSchema.type === "HorizontalLayout" && Array.isArray(uiSchema.elements)) { + if (Array.isArray(uiSchema.elements)) { const control = uiSchema.elements.find((element: any) => { if (element.type === "Control") { // Convert the scope path to match our field path @@ -666,24 +664,41 @@ const JsonFormsRenderer: React.FC = ({ // Fallback to default rendering if not a categorization return ( - -
- {Object.entries(schema.properties || {}).map( - ([key, fieldSchema]: [string, any]) => - renderField(key, fieldSchema, localData?.[key]) - )} - - - -
-
+ +
+ + {Object.entries(schema.properties || {}).map(([key, fieldSchema]) => { + const fieldUiSchema = uiSchema?.[key] || {}; + const colSpan = calculateColSpan(fieldUiSchema, containerWidth); + + return ( + + {renderField(key, fieldSchema, localData?.[key])} + + ); + })} + + + + +
+
); }; -export default React.memo(JsonFormsRenderer); +const calculateColSpan = (uiSchema: any, containerWidth: number) => { + const colSpan = uiSchema?.["ui:colSpan"] || { xs: 24, sm: 24, md: 12, lg: 12, xl: 8 }; + if (containerWidth > 1200 && colSpan.xl) return { span: colSpan.xl }; + if (containerWidth > 992 && colSpan.lg) return { span: colSpan.lg }; + if (containerWidth > 768 && colSpan.md) return { span: colSpan.md }; + if (containerWidth > 576 && colSpan.sm) return { span: colSpan.sm }; + return { span: 24 }; +}; + +export default React.memo(JsonFormsRenderer); \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx index f0b83c4f53..0705a745b6 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx @@ -483,13 +483,6 @@ let FormBasicComp = (function () { tooltip: "Define custom error messages for form fields. Use __errors array for field-specific errors.", }) ) - // : ( - // children.validationState.propertyView({ - // key: "validationState", - // label: trans("jsonSchemaForm.validationState"), - // tooltip: "Current validation state of the form fields. Shows errors and touched state for each field.", - // }) - // ) } )} diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index 313358815a..0445c94039 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -41,6 +41,7 @@ import { useResizeDetector } from "react-resize-detector"; import { useContext } from "react"; import { Tooltip } from "antd"; import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; const Container = styled.div<{ $style: any }>` height: 100%; @@ -212,6 +213,9 @@ let ButtonTmpComp = (function () { const imgRef = useRef(null); const conRef = useRef(null); + + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + useEffect(() => { if (height && width) { onResize(); @@ -285,7 +289,7 @@ let ButtonTmpComp = (function () { } onClick={() => isDefault(props.type) - ? props.onEvent("click") + ? handleClickEvent() : submitForm(editorState, props.form) } > diff --git a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx index 0ce837560c..c9c8229ed4 100644 --- a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx @@ -234,7 +234,7 @@ const ResponsiveLayout = (props: ResponsiveLayoutProps) => { {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; // Use the actual minWidth from column configuration instead of calculated width diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx index f221b547db..619b42674f 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx @@ -9,6 +9,8 @@ import { withDefault } from "comps/generators"; import styled from "styled-components"; import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; +import { clickEvent, eventHandlerControl, doubleClickEvent } from "comps/controls/eventHandlerControl"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; const InputNumberWrapper = styled.div` .ant-input-number { @@ -25,6 +27,15 @@ const InputNumberWrapper = styled.div` } `; +const NumberViewWrapper = styled.div` + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; +`; + +const NumberEventOptions = [clickEvent, doubleClickEvent] as const; + const childrenMap = { text: NumberControl, step: withDefault(NumberControl, 1), @@ -34,6 +45,7 @@ const childrenMap = { prefixIcon: IconControl, suffixIcon: IconControl, suffix: StringControl, + onEvent: eventHandlerControl(NumberEventOptions), }; const getBaseValue: ColumnTypeViewFn = (props) => props.text; @@ -46,6 +58,7 @@ type NumberViewProps = { suffixIcon: ReactNode; float: boolean; precision: number; + onEvent?: (eventName: string) => void; }; type NumberEditProps = { @@ -58,6 +71,8 @@ type NumberEditProps = { }; const ColumnNumberView = React.memo((props: NumberViewProps) => { + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent ?? (() => {})}) + const formattedValue = useMemo(() => { let result = !props.float ? Math.floor(props.value) : props.value; if (props.float) { @@ -66,8 +81,12 @@ const ColumnNumberView = React.memo((props: NumberViewProps) => { return result; }, [props.value, props.float, props.precision]); + const handleClick = useCallback(() => { + handleClickEvent() + }, [props.onEvent]); + return ( - <> + {hasIcon(props.prefixIcon) && ( {props.prefixIcon} )} @@ -75,7 +94,7 @@ const ColumnNumberView = React.memo((props: NumberViewProps) => { {hasIcon(props.suffixIcon) && ( {props.suffixIcon} )} - + ); }); @@ -197,6 +216,7 @@ export const ColumnNumberComp = (function () { children.step.dispatchChangeValueAction(String(newValue)); } })} + {children.onEvent.propertyView()} ); }) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnAvatarsComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnAvatarsComp.tsx index c34b6dfbb9..f02ee19943 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnAvatarsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnAvatarsComp.tsx @@ -9,7 +9,7 @@ import { avatarGroupStyle, AvatarGroupStyleType } from "comps/controls/styleCont import { AlignCenter, AlignLeft, AlignRight } from "lowcoder-design"; import { NumberControl } from "comps/controls/codeControl"; import { Avatar, Tooltip } from "antd"; -import { clickEvent, eventHandlerControl, refreshEvent } from "comps/controls/eventHandlerControl"; +import { clickEvent, eventHandlerControl, refreshEvent, doubleClickEvent } from "comps/controls/eventHandlerControl"; import React, { ReactElement, useCallback, useEffect, useRef } from "react"; import { IconControl } from "comps/controls/iconControl"; import { ColorControl } from "comps/controls/colorControl"; @@ -17,6 +17,7 @@ import { optionsControl } from "comps/controls/optionsControl"; import { BoolControl } from "comps/controls/boolControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { JSONObject } from "util/jsonTypes"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; const MacaroneList = [ '#fde68a', @@ -38,6 +39,8 @@ const Container = styled.div<{ $style: AvatarGroupStyleType | undefined, alignme cursor: pointer; `; +const AvatarEventOptions = [clickEvent, refreshEvent] as const; + const DropdownOption = new MultiCompBuilder( { src: StringControl, @@ -46,6 +49,7 @@ const DropdownOption = new MultiCompBuilder( color: ColorControl, backgroundColor: ColorControl, Tooltip: StringControl, + onEvent: eventHandlerControl(AvatarEventOptions), }, (props) => props ) @@ -63,12 +67,13 @@ const DropdownOption = new MultiCompBuilder( {children.color.propertyView({ label: trans("style.fill") })} {children.backgroundColor.propertyView({ label: trans("style.background") })} {children.Tooltip.propertyView({ label: trans("badge.tooltip") })} + {children.onEvent.propertyView()} ); }) .build(); -const EventOptions = [clickEvent, refreshEvent] as const; +const EventOptions = [clickEvent, refreshEvent, doubleClickEvent] as const; export const alignOptions = [ { label: , value: "flex-start" }, @@ -83,16 +88,20 @@ const MemoizedAvatar = React.memo(({ style, autoColor, avatarSize, - onEvent + onEvent, + onItemEvent }: { item: any; index: number; style: any; autoColor: boolean; avatarSize: number; - onEvent: (event: string) => void; + onEvent: (event: string) => void; + onItemEvent?: (event: string) => void; }) => { const mountedRef = useRef(true); + const handleClickEvent = useCompClickEventHandler({onEvent}) + // Cleanup on unmount useEffect(() => { @@ -103,8 +112,15 @@ const MemoizedAvatar = React.memo(({ const handleClick = useCallback(() => { if (!mountedRef.current) return; - onEvent("click"); - }, [onEvent]); + + // Trigger individual avatar event first + if (onItemEvent) { + onItemEvent("click"); + } + + // Then trigger main component event + handleClickEvent() + }, [onItemEvent, handleClickEvent]); return ( @@ -114,6 +130,7 @@ const MemoizedAvatar = React.memo(({ style={{ color: item.color ? item.color : (style.fill !== '#FFFFFF' ? style.fill : '#FFFFFF'), backgroundColor: item.backgroundColor ? item.backgroundColor : (autoColor ? MacaroneList[index % MacaroneList.length] : style.background), + cursor: 'pointer', }} size={avatarSize} onClick={handleClick} @@ -162,6 +179,7 @@ const MemoizedAvatarGroup = React.memo(({ autoColor={autoColor} avatarSize={avatarSize} onEvent={onEvent} + onItemEvent={item.onEvent} /> ))} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDateComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDateComp.tsx index 99bee383e6..55168b1515 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDateComp.tsx @@ -127,7 +127,7 @@ const Wrapper = styled.div` export function formatDate(date: string, format: string) { let mom = dayjs(date); - if (isNumber(Number(date)) && date !== "") { + if (isNumber(Number(date)) && !isNaN(Number(date)) && date !== "") { mom = dayjs(Number(date)); } if (!mom.isValid()) { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDropdownComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDropdownComp.tsx index d71ad03cb3..b78601a5fa 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDropdownComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDropdownComp.tsx @@ -15,6 +15,8 @@ import { ButtonStyle } from "comps/controls/styleControlConstants"; import { Button100 } from "comps/comps/buttonComp/buttonCompConstants"; import styled from "styled-components"; import { ButtonType } from "antd/es/button"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; const StyledButton = styled(Button100)` display: flex; @@ -28,19 +30,23 @@ const StyledIconWrapper = styled(IconWrapper)` margin: 0; `; +const DropdownEventOptions = [clickEvent] as const; + const childrenMap = { buttonType: dropdownControl(ButtonTypeOptions, "primary"), label: withDefault(StringControl, 'Menu'), prefixIcon: IconControl, suffixIcon: IconControl, options: DropdownOptionControl, + onEvent: eventHandlerControl(DropdownEventOptions), }; const getBaseValue: ColumnTypeViewFn = (props) => props.label; // Memoized dropdown menu component -const DropdownMenu = React.memo(({ items, options }: { items: any[]; options: any[] }) => { +const DropdownMenu = React.memo(({ items, options, onEvent }: { items: any[]; options: any[]; onEvent: (eventName: string) => void }) => { const mountedRef = useRef(true); + const handleClickEvent = useCompClickEventHandler({onEvent}) // Cleanup on unmount useEffect(() => { @@ -54,7 +60,9 @@ const DropdownMenu = React.memo(({ items, options }: { items: any[]; options: an const item = items.find((o) => o.key === key); const itemIndex = options.findIndex(option => option.label === item?.label); item && options[itemIndex]?.onEvent("click"); - }, [items, options]); + // Also trigger the dropdown's main event handler + handleClickEvent(); + }, [items, options, handleClickEvent]); const handleMouseDown = useCallback((e: React.MouseEvent) => { e.stopPropagation(); @@ -78,6 +86,7 @@ const DropdownView = React.memo((props: { prefixIcon: ReactNode; suffixIcon: ReactNode; options: any[]; + onEvent?: (eventName: string) => void; }) => { const mountedRef = useRef(true); @@ -120,8 +129,8 @@ const DropdownView = React.memo((props: { const buttonStyle = useStyle(ButtonStyle); const menu = useMemo(() => ( - - ), [items, props.options]); + {})} /> + ), [items, props.options, props.onEvent]); return ( ); }) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnImgComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnImgComp.tsx index b062f8fc46..d3d2041016 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnImgComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnImgComp.tsx @@ -10,20 +10,28 @@ import { withDefault } from "comps/generators"; import { TacoImage } from "lowcoder-design"; import styled from "styled-components"; import { DEFAULT_IMG_URL } from "@lowcoder-ee/util/stringUtils"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; export const ColumnValueTooltip = trans("table.columnValueTooltip"); const childrenMap = { src: withDefault(StringControl, "{{currentCell}}"), size: withDefault(NumberControl, "50"), + onEvent: eventHandlerControl([clickEvent]), }; const StyledTacoImage = styled(TacoImage)` - pointer-events: auto; + pointer-events: auto !important; + cursor: pointer !important; + + &:hover { + opacity: 0.8; + transition: opacity 0.2s ease; + } `; // Memoized image component -const ImageView = React.memo(({ src, size }: { src: string; size: number }) => { +const ImageView = React.memo(({ src, size, onEvent }: { src: string; size: number; onEvent?: (eventName: string) => void }) => { const mountedRef = useRef(true); // Cleanup on unmount @@ -33,10 +41,19 @@ const ImageView = React.memo(({ src, size }: { src: string; size: number }) => { }; }, []); + const handleClick = useCallback(() => { + console.log("Image clicked!", { src, onEvent: !!onEvent }); // Debug log + if (mountedRef.current && onEvent) { + onEvent("click"); + } + }, [onEvent, src]); + return ( ); }); @@ -96,7 +113,7 @@ export const ImageComp = (function () { childrenMap, (props, dispatch) => { const value = props.changeValue ?? getBaseValue(props, dispatch); - return ; + return ; }, (nodeValue) => nodeValue.src.value, getBaseValue @@ -118,6 +135,7 @@ export const ImageComp = (function () { {children.size.propertyView({ label: trans("table.imageSize"), })} + {children.onEvent.propertyView()} ); }) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx index c82b7326a3..e93b3082a6 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx @@ -4,19 +4,24 @@ import { ColumnTypeCompBuilder, ColumnTypeViewFn, } from "comps/comps/tableComp/column/columnTypeCompBuilder"; -import { ActionSelectorControlInContext } from "comps/controls/actionSelector/actionSelectorControl"; import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; import { trans } from "i18n"; import { disabledPropertyView } from "comps/utils/propertyUtils"; import styled, { css } from "styled-components"; import { styleControl } from "comps/controls/styleControl"; import { TableColumnLinkStyle } from "comps/controls/styleControlConstants"; +import { clickEvent, eventHandlerControl, doubleClickEvent } from "comps/controls/eventHandlerControl"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; +import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators"; +import { fixOldActionData } from "comps/comps/tableComp/column/simpleColumnTypeComps"; export const ColumnValueTooltip = trans("table.columnValueTooltip"); +const LinkEventOptions = [clickEvent, doubleClickEvent] as const; + const childrenMap = { text: StringControl, - onClick: ActionSelectorControlInContext, + onClick: eventHandlerControl(LinkEventOptions), disabled: BoolCodeControl, style: styleControl(TableColumnLinkStyle), }; @@ -34,10 +39,11 @@ const StyledLink = styled.a<{ $disabled: boolean }>` `; // Memoized link component -export const ColumnLink = React.memo(({ disabled, label, onClick }: { disabled: boolean; label: string; onClick?: () => void }) => { +export const ColumnLink = React.memo(({ disabled, label, onClick }: { disabled: boolean; label: string; onClick: (eventName: string) => void }) => { + const handleClickEvent = useCompClickEventHandler({onEvent: onClick}) const handleClick = useCallback(() => { - if (!disabled && onClick) { - onClick(); + if (!disabled) { + handleClickEvent(); } }, [disabled, onClick]); @@ -101,7 +107,7 @@ LinkEdit.displayName = 'LinkEdit'; const getBaseValue: ColumnTypeViewFn = (props) => props.text; -export const LinkComp = (function () { +const LinkCompTmp = (function () { return new ColumnTypeCompBuilder( childrenMap, (props, dispatch) => { @@ -125,10 +131,7 @@ export const LinkComp = (function () { tooltip: ColumnValueTooltip, })} {disabledPropertyView(children)} - {children.onClick.propertyView({ - label: trans("table.action"), - placement: "table", - })} + {children.onClick.propertyView()} )) .setStylePropertyViewFn((children) => ( @@ -138,3 +141,5 @@ export const LinkComp = (function () { )) .build(); })(); + +export const LinkComp = migrateOldData(LinkCompTmp, fixOldActionData); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx index 4ecd308ddd..5a7fae3d3e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { default as Menu } from "antd/es/menu"; import { ColumnTypeCompBuilder } from "comps/comps/tableComp/column/columnTypeCompBuilder"; -import { ActionSelectorControlInContext } from "comps/controls/actionSelector/actionSelectorControl"; import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; import { manualOptionsControl } from "comps/controls/optionsControl"; import { MultiCompBuilder } from "comps/generators"; @@ -10,6 +9,10 @@ import { trans } from "i18n"; import styled from "styled-components"; import { ColumnLink } from "comps/comps/tableComp/column/columnTypeComps/columnLinkComp"; import { LightActiveTextColor, PrimaryColor } from "constants/style"; +import { clickEvent, eventHandlerControl, doubleClickEvent } from "comps/controls/eventHandlerControl"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; +import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators"; +import { fixOldActionData } from "comps/comps/tableComp/column/simpleColumnTypeComps"; const MenuLinkWrapper = styled.div` > a { @@ -37,20 +40,16 @@ const MenuWrapper = styled.div` } `; +const LinkEventOptions = [clickEvent, doubleClickEvent] as const; + // Memoized menu item component const MenuItem = React.memo(({ option, index }: { option: any; index: number }) => { - const handleClick = useCallback(() => { - if (!option.disabled && option.onClick) { - option.onClick(); - } - }, [option.disabled, option.onClick]); - return ( ); @@ -58,10 +57,10 @@ const MenuItem = React.memo(({ option, index }: { option: any; index: number }) MenuItem.displayName = 'MenuItem'; -const OptionItem = new MultiCompBuilder( +const OptionItemTmp = new MultiCompBuilder( { label: StringControl, - onClick: ActionSelectorControlInContext, + onClick: eventHandlerControl(LinkEventOptions), hidden: BoolCodeControl, disabled: BoolCodeControl, }, @@ -73,17 +72,16 @@ const OptionItem = new MultiCompBuilder( return ( <> {children.label.propertyView({ label: trans("label") })} - {children.onClick.propertyView({ - label: trans("table.action"), - placement: "table", - })} {hiddenPropertyView(children)} {disabledPropertyView(children)} + {children.onClick.propertyView()} ); }) .build(); +const OptionItem = migrateOldData(OptionItemTmp, fixOldActionData); + // Memoized menu component const LinksMenu = React.memo(({ options }: { options: any[] }) => { const mountedRef = useRef(true); @@ -114,7 +112,7 @@ const LinksMenu = React.memo(({ options }: { options: any[] }) => { LinksMenu.displayName = 'LinksMenu'; -export const ColumnLinksComp = (function () { +const ColumnLinksCompTmp = (function () { const childrenMap = { options: manualOptionsControl(OptionItem, { initOptions: [{ label: trans("table.option1") }], @@ -137,3 +135,5 @@ export const ColumnLinksComp = (function () { )) .build(); })(); + +export const ColumnLinksComp = migrateOldData(ColumnLinksCompTmp, fixOldActionData); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMarkdownComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMarkdownComp.tsx index e8fcd9a4b2..17ad78efd3 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMarkdownComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnMarkdownComp.tsx @@ -9,10 +9,12 @@ import { StringControl } from "comps/controls/codeControl"; import { trans } from "i18n"; import { markdownCompCss, TacoMarkDown } from "lowcoder-design"; import styled from "styled-components"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; const Wrapper = styled.div` ${markdownCompCss}; max-height: 32px; + cursor: pointer; > .markdown-body { margin: 0; @@ -22,16 +24,25 @@ const Wrapper = styled.div` } `; +const MarkdownEventOptions = [clickEvent] as const; + const childrenMap = { text: StringControl, + onEvent: eventHandlerControl(MarkdownEventOptions), }; const getBaseValue: ColumnTypeViewFn = (props) => props.text; // Memoized markdown view component -const MarkdownView = React.memo(({ value }: { value: string }) => { +const MarkdownView = React.memo(({ value, onEvent }: { value: string; onEvent?: (eventName: string) => void }) => { + const handleClick = useCallback(() => { + if (onEvent) { + onEvent("click"); + } + }, [onEvent]); + return ( - + {value} ); @@ -92,7 +103,7 @@ export const ColumnMarkdownComp = (function () { childrenMap, (props, dispatch) => { const value = props.changeValue ?? getBaseValue(props, dispatch); - return ; + return ; }, (nodeValue) => nodeValue.text.value, getBaseValue @@ -110,6 +121,7 @@ export const ColumnMarkdownComp = (function () { label: trans("table.columnValue"), tooltip: ColumnValueTooltip, })} + {children.onEvent.propertyView()} )) .build(); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnSelectComp.tsx index de76a4dd87..b54be87997 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnSelectComp.tsx @@ -1,13 +1,16 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { SelectUIView } from "comps/comps/selectInputComp/selectCompConstants"; -import { SelectOptionControl } from "comps/controls/optionsControl"; -import { StringControl } from "comps/controls/codeControl"; - +import { StringControl, BoolCodeControl } from "comps/controls/codeControl"; +import { IconControl } from "comps/controls/iconControl"; +import { MultiCompBuilder } from "comps/generators"; +import { optionsControl } from "comps/controls/optionsControl"; +import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; import { ColumnValueTooltip } from "../simpleColumnTypeComps"; import { styled } from "styled-components"; +import { clickEvent, eventHandlerControl, doubleClickEvent } from "comps/controls/eventHandlerControl"; const Wrapper = styled.div` display: inline-flex; @@ -75,9 +78,44 @@ const Wrapper = styled.div` } `; +const SelectOptionEventOptions = [clickEvent, doubleClickEvent] as const; + +// Create a new option type with event handlers for each option +const SelectOptionWithEvents = new MultiCompBuilder( + { + value: StringControl, + label: StringControl, + prefixIcon: IconControl, + disabled: BoolCodeControl, + hidden: BoolCodeControl, + onEvent: eventHandlerControl(SelectOptionEventOptions), + }, + (props) => props +) + .setPropertyViewFn((children) => ( + <> + {children.label.propertyView({ label: trans("label") })} + {children.value.propertyView({ label: trans("value") })} + {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} + {children.onEvent.propertyView()} + + )) + .build(); + +const SelectOptionWithEventsControl = optionsControl(SelectOptionWithEvents, { + initOptions: [ + { label: trans("optionsControl.optionI", { i: 1 }), value: "1" }, + { label: trans("optionsControl.optionI", { i: 2 }), value: "2" }, + ], + uniqField: "value", +}); + const childrenMap = { text: StringControl, - options: SelectOptionControl, + options: SelectOptionWithEventsControl, + onEvent: eventHandlerControl(SelectOptionEventOptions), }; const getBaseValue: ColumnTypeViewFn = (props) => props.text; @@ -87,6 +125,7 @@ type SelectEditProps = { onChange: (value: string) => void; onChangeEnd: () => void; options: any[]; + onMainEvent?: (eventName: string) => void; }; const SelectEdit = React.memo((props: SelectEditProps) => { @@ -106,7 +145,18 @@ const SelectEdit = React.memo((props: SelectEditProps) => { if (!mountedRef.current) return; props.onChange(val); setCurrentValue(val); - }, [props.onChange]); + + // Trigger the specific option's event handler + const selectedOption = props.options.find(option => option.value === val); + if (selectedOption?.onEvent) { + selectedOption.onEvent("click"); + } + + // Also trigger the main component's event handler + if (props.onMainEvent) { + props.onMainEvent("click"); + } + }, [props.onChange, props.options, props.onMainEvent]); const handleEvent = useCallback(async (eventName: string) => { if (!mountedRef.current) return [] as unknown[]; @@ -159,6 +209,7 @@ export const ColumnSelectComp = (function () { options={props.otherProps?.options || []} onChange={props.onChange} onChangeEnd={props.onChangeEnd} + onMainEvent={props.otherProps?.onEvent} /> ) @@ -173,6 +224,7 @@ export const ColumnSelectComp = (function () { {children.options.propertyView({ title: trans("optionsControl.optionList"), })} + {children.onEvent.propertyView()} ); }) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnTagsComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnTagsComp.tsx index 0f4f1e15f6..3bdbbed9dc 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnTagsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnTagsComp.tsx @@ -16,6 +16,7 @@ import { hashToNum } from "util/stringUtils"; import { CustomSelect, PackUpIcon } from "lowcoder-design"; import { ScrollBar } from "lowcoder-design"; import { ColoredTagOptionControl } from "comps/controls/optionsControl"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; const colors = PresetStatusColorTypes; @@ -58,10 +59,58 @@ const TagsControl = codeControl | string>( function getTagColor(tagText : any, tagOptions: any[]) { const foundOption = tagOptions.find((option: { label: any; }) => option.label === tagText); - return foundOption ? foundOption.color : (function() { - const index = Math.abs(hashToNum(tagText)) % colors.length; - return colors[index]; - })(); + if (foundOption) { + if (foundOption.colorType === "preset") { + return foundOption.presetColor; + } else if (foundOption.colorType === "custom") { + return undefined; // For custom colors, we'll use style instead + } + // Backward compatibility - if no colorType specified, assume it's the old color field + return foundOption.color; + } + // Default fallback + const index = Math.abs(hashToNum(tagText)) % colors.length; + return colors[index]; +} + +function getTagStyle(tagText: any, tagOptions: any[]) { + const foundOption = tagOptions.find((option: { label: any; }) => option.label === tagText); + if (foundOption) { + const style: any = {}; + + // Handle color styling + if (foundOption.colorType === "custom") { + style.backgroundColor = foundOption.color; + style.color = foundOption.textColor; + style.border = `1px solid ${foundOption.color}`; + } + + // Add border styling if specified + if (foundOption.border) { + style.borderColor = foundOption.border; + if (!foundOption.colorType || foundOption.colorType !== "custom") { + style.border = `1px solid ${foundOption.border}`; + } + } + + // Add border radius if specified + if (foundOption.radius) { + style.borderRadius = foundOption.radius; + } + + // Add margin if specified + if (foundOption.margin) { + style.margin = foundOption.margin; + } + + // Add padding if specified + if (foundOption.padding) { + style.padding = foundOption.padding; + } + + return style; + } + return {}; } function getTagIcon(tagText: any, tagOptions: any[]) { @@ -72,6 +121,7 @@ function getTagIcon(tagText: any, tagOptions: any[]) { const childrenMap = { text: TagsControl, tagColors: ColoredTagOptionControl, + onEvent: eventHandlerControl([clickEvent]), }; const getBaseValue: ColumnTypeViewFn = ( @@ -181,6 +231,7 @@ export const DropdownStyled = styled.div` export const TagStyled = styled(Tag)` margin-right: 8px; + cursor: pointer; svg { margin-right: 4px; } @@ -250,6 +301,14 @@ const TagEdit = React.memo((props: TagEditPropsType) => { setOpen(false); }, [props.onChangeEnd]); + const handleTagClick = useCallback((tagText: string, e: React.MouseEvent) => { + e.stopPropagation(); + const foundOption = memoizedTagOptions.find(option => option.label === tagText); + if (foundOption && foundOption.onEvent) { + foundOption.onEvent("click"); + } + }, [memoizedTagOptions]); + return ( { {tags.map((value, index) => ( {value.split(",")[1] ? ( - value.split(",").map((item, i) => ( - - {item} - - )) + value.split(",").map((item, i) => { + const tagColor = getTagColor(item, memoizedTagOptions); + const tagIcon = getTagIcon(item, memoizedTagOptions); + const tagStyle = getTagStyle(item, memoizedTagOptions); + + return ( + handleTagClick(item, e)} + > + {item} + + ); + }) ) : ( - + handleTagClick(value, e)} + > {value} )} @@ -313,12 +397,34 @@ export const ColumnTagsComp = (function () { let value = props.changeValue ?? getBaseValue(props, dispatch); value = typeof value === "string" && value.split(",")[1] ? value.split(",") : value; const tags = _.isArray(value) ? value : (value.length ? [value] : []); + + const handleTagClick = (tagText: string) => { + const foundOption = tagOptions.find(option => option.label === tagText); + if (foundOption && foundOption.onEvent) { + foundOption.onEvent("click"); + } + // Also trigger the main component's event handler + if (props.onEvent) { + props.onEvent("click"); + } + }; + const view = tags.map((tag, index) => { // The actual eval value is of type number or boolean const tagText = String(tag); + const tagColor = getTagColor(tagText, tagOptions); + const tagIcon = getTagIcon(tagText, tagOptions); + const tagStyle = getTagStyle(tagText, tagOptions); + return (
- + handleTagClick(tagText)} + > {tagText}
@@ -349,8 +455,9 @@ export const ColumnTagsComp = (function () { tooltip: ColumnValueTooltip, })} {children.tagColors.propertyView({ - title: "test", + title: "Tag Options", })} + {children.onEvent.propertyView()} )) .build(); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/simpleTextComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/simpleTextComp.tsx index 36d1d7ce99..dcdffe3907 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/simpleTextComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/simpleTextComp.tsx @@ -7,11 +7,24 @@ import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; import React, { useCallback, useMemo } from "react"; import { RecordConstructorToComp } from "lowcoder-core"; +import { clickEvent, doubleClickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; +import styled from "styled-components"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; + +const TextEventOptions = [clickEvent, doubleClickEvent] as const; + +const TextWrapper = styled.div` + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; +`; const childrenMap = { text: StringOrNumberControl, prefixIcon: IconControl, suffixIcon: IconControl, + onEvent: eventHandlerControl(TextEventOptions), }; // Memoize the base value function to prevent unnecessary string creation @@ -27,6 +40,7 @@ interface SimpleTextContentProps { value: string | number; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; + onEvent?: (eventName: string) => void; } interface SimpleTextEditViewProps { @@ -35,13 +49,21 @@ interface SimpleTextEditViewProps { onChangeEnd: () => void; } -const SimpleTextContent = React.memo(({ value, prefixIcon, suffixIcon }: SimpleTextContentProps) => ( - <> - {hasIcon(prefixIcon) && } - {value} - {hasIcon(suffixIcon) && } - -)); +const SimpleTextContent = React.memo(({ value, prefixIcon, suffixIcon, onEvent }: SimpleTextContentProps) => { + const handleClickEvent = useCompClickEventHandler({onEvent: onEvent ?? (() => {})}) + + const handleClick = useCallback(() => { + handleClickEvent() + }, [handleClickEvent]); + + return ( + + {hasIcon(prefixIcon) && } + {value} + {hasIcon(suffixIcon) && } + + ); +}); const SimpleTextEditView = React.memo(({ value, onChange, onChangeEnd }: SimpleTextEditViewProps) => { const handleChange = useCallback((e: React.ChangeEvent) => { @@ -55,9 +77,10 @@ const SimpleTextEditView = React.memo(({ value, onChange, onChangeEnd }: SimpleT variant="borderless" onChange={handleChange} onBlur={onChangeEnd} - onPressEnter={onChangeEnd} - /> -)}); + onPressEnter={onChangeEnd} + /> + ); +}); const SimpleTextPropertyView = React.memo(({ children }: { children: RecordConstructorToComp }) => { return useMemo(() => ( @@ -72,8 +95,9 @@ const SimpleTextPropertyView = React.memo(({ children }: { children: RecordConst {children.suffixIcon.propertyView({ label: trans("button.suffixIcon"), })} + {children.onEvent.propertyView()} - ), [children.text, children.prefixIcon, children.suffixIcon]); + ), [children.text, children.prefixIcon, children.suffixIcon, children.onEvent]); }); export const SimpleTextComp = new ColumnTypeCompBuilder( @@ -85,6 +109,7 @@ export const SimpleTextComp = new ColumnTypeCompBuilder( value={value} prefixIcon={props.prefixIcon} suffixIcon={props.suffixIcon} + onEvent={props.onEvent} /> ); }, diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx index 3d5096cc89..8ec51c6a1a 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx @@ -13,7 +13,23 @@ import React, { useCallback, useEffect, useMemo } from "react"; import { CSSProperties } from "react"; import { RecordConstructorToComp } from "lowcoder-core"; import { ToViewReturn } from "@lowcoder-ee/comps/generators/multi"; +import { clickEvent, eventHandlerControl, doubleClickEvent } from "comps/controls/eventHandlerControl"; +import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; +export const fixOldActionData = (oldData: any) => { + if (!oldData) return oldData; + if (Boolean(oldData.onClick)) { + return { + ...oldData, + onClick: [{ + name: "click", + handler: oldData.onClick, + }], + }; + } + return oldData; +} export const ColumnValueTooltip = trans("table.columnValueTooltip"); export const ButtonTypeOptions = [ @@ -31,10 +47,12 @@ export const ButtonTypeOptions = [ }, ] as const; +const ButtonEventOptions = [clickEvent, doubleClickEvent] as const; + const childrenMap = { text: StringControl, buttonType: dropdownControl(ButtonTypeOptions, "primary"), - onClick: ActionSelectorControlInContext, + onClick: eventHandlerControl(ButtonEventOptions), loading: BoolCodeControl, disabled: BoolCodeControl, prefixIcon: IconControl, @@ -47,10 +65,11 @@ const ButtonStyled = React.memo(({ props }: { props: ToViewReturn { - props.onClick?.(); - }, [props.onClick]); + handleClickEvent() + }, [handleClickEvent]); const buttonStyle = useMemo(() => ({ margin: 0, @@ -76,7 +95,7 @@ const ButtonStyled = React.memo(({ props }: { props: ToViewReturn , @@ -100,11 +119,10 @@ export const ButtonComp = (function () { })} {loadingPropertyView(children)} {disabledPropertyView(children)} - {children.onClick.propertyView({ - label: trans("table.action"), - placement: "table", - })} + {children.onClick.propertyView()} )) .build(); })(); + +export const ButtonComp = migrateOldData(ButtonCompTmp, fixOldActionData); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx index b3dbe77c92..938983ac9e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx @@ -194,26 +194,27 @@ const ColumnPropertyView = React.memo(({ summaryRowIndex: number; }) => { const selectedColumn = comp.children.render.getSelectedComp(); - const columnType = useMemo(() => selectedColumn.getComp().children.compType.getView(), [selectedColumn] ); - + + const initialColumns = useMemo(() => + selectedColumn.getParams()?.initialColumns as OptionType[] || [], + [selectedColumn] + ); + const columnValue = useMemo(() => { const column = selectedColumn.getComp().toJsonValue(); if (column.comp?.hasOwnProperty('src')) { return (column.comp as any).src; } else if (column.comp?.hasOwnProperty('text')) { - return (column.comp as any).text; + const value = (column.comp as any).text; + const isDynamicValue = initialColumns.find((column) => column.value === value); + return !isDynamicValue ? '{{currentCell}}' : value; } return '{{currentCell}}'; - }, [selectedColumn]); - - const initialColumns = useMemo(() => - selectedColumn.getParams()?.initialColumns as OptionType[] || [], - [selectedColumn] - ); + }, [selectedColumn, initialColumns]); const summaryColumns = comp.children.summaryColumns.getView(); @@ -434,6 +435,8 @@ export class ColumnComp extends ColumnInitComp { ) ) ); + // clear render comp cache when change set is cleared + this.children.render.dispatch(RenderComp.clearAction()); } dispatchClearInsertSet() { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx index 60e292d0dc..037516d91b 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx @@ -7,6 +7,11 @@ import { TableOnEventView } from "./tableTypes"; import { OB_ROW_ORI_INDEX, RecordType } from "comps/comps/tableComp/tableUtils"; import { ControlNodeCompBuilder } from "comps/generators/controlCompBuilder"; +// double-click detection constants +const DOUBLE_CLICK_THRESHOLD = 300; // ms +let lastClickTime = 0; +let clickTimer: ReturnType; + const modeOptions = [ { label: trans("selectionControl.single"), @@ -38,8 +43,9 @@ export function getSelectedRowKeys( return [selection.children.selectedRowKey.getView()]; case "multiple": return selection.children.selectedRowKeys.getView(); + default: + return []; } - return []; } export const SelectionControl = (function () { @@ -50,34 +56,52 @@ export const SelectionControl = (function () { }; return new ControlNodeCompBuilder(childrenMap, (props, dispatch) => { const changeSelectedRowKey = (record: RecordType) => { - if (getKey(record) !== props.selectedRowKey) { - dispatch(changeChildAction("selectedRowKey", getKey(record), false)); + const key = getKey(record); + if (key !== props.selectedRowKey) { + dispatch(changeChildAction("selectedRowKey", key, false)); } }; + return (onEvent: TableOnEventView) => { + const handleClick = (record: RecordType) => { + return () => { + const now = Date.now(); + clearTimeout(clickTimer); + if (now - lastClickTime < DOUBLE_CLICK_THRESHOLD) { + + changeSelectedRowKey(record); + onEvent("doubleClick"); + if (getKey(record) !== props.selectedRowKey) { + onEvent("rowSelectChange"); + } + } else { + clickTimer = setTimeout(() => { + changeSelectedRowKey(record); + onEvent("rowClick"); + if (getKey(record) !== props.selectedRowKey) { + onEvent("rowSelectChange"); + } + }, DOUBLE_CLICK_THRESHOLD); + } + lastClickTime = now; + }; + }; + if (props.mode === "single" || props.mode === "close") { return { rowKey: getKey, rowClassName: (record: RecordType, index: number, indent: number) => { - // Turn off row selection mode, only do visual shutdown, selectedRow still takes effect if (props.mode === "close") { return ""; } return getKey(record) === props.selectedRowKey ? "ant-table-row-selected" : ""; }, - onRow: (record: RecordType, index: number | undefined) => { - return { - onClick: () => { - changeSelectedRowKey(record); - onEvent("rowClick"); - if (getKey(record) !== props.selectedRowKey) { - onEvent("rowSelectChange"); - } - }, - }; - }, + onRow: (record: RecordType, index: number | undefined) => ({ + onClick: handleClick(record), + }), }; } + const result: TableRowSelection = { type: "checkbox", selectedRowKeys: props.selectedRowKeys, @@ -86,7 +110,6 @@ export const SelectionControl = (function () { dispatch(changeChildAction("selectedRowKeys", selectedRowKeys as string[], false)); onEvent("rowSelectChange"); }, - // click checkbox also trigger row click event onSelect: (record: RecordType) => { changeSelectedRowKey(record); onEvent("rowClick"); @@ -95,14 +118,9 @@ export const SelectionControl = (function () { return { rowKey: getKey, rowSelection: result, - onRow: (record: RecordType) => { - return { - onClick: () => { - changeSelectedRowKey(record); - onEvent("rowClick"); - }, - }; - }, + onRow: (record: RecordType) => ({ + onClick: handleClick(record), + }), }; }; }) @@ -113,4 +131,4 @@ export const SelectionControl = (function () { }) ) .build(); -})(); +})(); \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index fee2da5233..721f645657 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -548,7 +548,8 @@ let TableTmpComp = withViewFn(TableImplComp, (comp) => { const withEditorModeStatus = (Component:any) => (props:any) => { const editorModeStatus = useContext(EditorContext).editorModeStatus; - return ; + const {ref, ...otherProps} = props; + return ; }; // Use this HOC when defining TableTmpComp diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 7d5d26e131..fa53d2f506 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -1039,6 +1039,8 @@ export const TableCompView = React.memo((props: { summaryRows={parseInt(summaryRows)} columns={columns} summaryRowStyle={summaryRowStyle} + dynamicColumn={dynamicColumn} + dynamicColumnConfig={dynamicColumnConfig} /> ); } diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableSummaryComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableSummaryComp.tsx index 58a7871aa3..56e4584c26 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableSummaryComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableSummaryComp.tsx @@ -186,6 +186,8 @@ export const TableSummary = memo(function TableSummary(props: { columns: ColumnComp[]; summaryRowStyle: TableSummaryRowStyleType; istoolbarPositionBelow: boolean; + dynamicColumn: boolean; + dynamicColumnConfig: string[]; }) { const { columns, @@ -195,10 +197,18 @@ export const TableSummary = memo(function TableSummary(props: { expandableRows, multiSelectEnabled, istoolbarPositionBelow, + dynamicColumn, + dynamicColumnConfig, } = props; const visibleColumns = useMemo(() => { let cols = columns.filter(col => !col.getView().hide); + if (dynamicColumn && dynamicColumnConfig?.length) { + cols = cols.filter(col => { + const colView = col.getView(); + return dynamicColumnConfig.includes(colView.isCustom ? colView.title : colView.dataIndex) + }) + } if (expandableRows) { cols.unshift(new ColumnComp({})); } @@ -206,7 +216,7 @@ export const TableSummary = memo(function TableSummary(props: { cols.unshift(new ColumnComp({})); } return cols; - }, [columns, expandableRows, multiSelectEnabled]); + }, [columns, expandableRows, multiSelectEnabled, dynamicColumn, dynamicColumnConfig]); const renderSummaryCell = useCallback((column: ColumnComp, rowIndex: number, index: number) => { const summaryColumn = column.children.summaryColumns.getView()[rowIndex].getView(); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx index 47db799b84..f40f18c73d 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx @@ -144,6 +144,11 @@ export const TableEventOptions = [ value: "refresh", description: trans("table.refresh"), }, + { + label: trans("event.doubleClick"), + value: "doubleClick", + description: trans("event.doubleClickDesc"), + } ] as const; export type TableEventOptionValues = typeof TableEventOptions[number]['value']; diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index 2d930fbf44..73fa06c0da 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -245,7 +245,7 @@ const TabbedContainer = (props: TabbedContainerProps) => { label, key: tab.key, forceRender: !destroyInactiveTab, - destroyInactiveTabPane: destroyInactiveTab, + destroyInactiveTab: destroyInactiveTab, children: ( diff --git a/client/packages/lowcoder/src/comps/comps/textComp.tsx b/client/packages/lowcoder/src/comps/comps/textComp.tsx index 93b3d79ae0..dcc5ccdb2b 100644 --- a/client/packages/lowcoder/src/comps/comps/textComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textComp.tsx @@ -20,13 +20,14 @@ import { PaddingControl } from "../controls/paddingControl"; import React, { useContext, useEffect, useRef, useMemo } from "react"; import { EditorContext } from "comps/editorState"; -import { clickEvent, eventHandlerControl } from "../controls/eventHandlerControl"; +import { clickEvent, doubleClickEvent, eventHandlerControl } from "../controls/eventHandlerControl"; import { NewChildren } from "../generators/uiCompBuilder"; import { RecordConstructorToComp } from "lowcoder-core"; import { ToViewReturn } from "../generators/multi"; import { BoolControl } from "../controls/boolControl"; +import { useCompClickEventHandler } from "../utils/useCompClickEventHandler"; -const EventOptions = [clickEvent] as const; +const EventOptions = [clickEvent, doubleClickEvent] as const; const getStyle = (style: TextStyleType) => { return css` @@ -224,9 +225,11 @@ const TextPropertyView = React.memo((props: { const TextView = React.memo((props: ToViewReturn) => { const value = props.text.value; + const handleClickEvent = useCompClickEventHandler({onEvent: props.onEvent}) + const handleClick = React.useCallback(() => { - props.onEvent("click"); - }, [props.onEvent]); + handleClickEvent() + }, [handleClickEvent]); const containerStyle = useMemo(() => ({ justifyContent: props.horizontalAlignment, diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index c2ab8801b2..fc25e03e75 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -11,7 +11,7 @@ import { stringExposingStateControl } from "comps/controls/codeStateControl"; import { LabelControl } from "comps/controls/labelControl"; import { InputLikeStyleType, LabelStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { Section, sectionNames, ValueFromOption } from "lowcoder-design"; -import _, { debounce } from "lodash"; +import { fromPairs } from "lodash"; import { css } from "styled-components"; import { EMAIL_PATTERN, URL_PATTERN } from "util/stringUtils"; import { MultiBaseComp, RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; @@ -84,7 +84,7 @@ type ValidationParams = { customRule: string; }; -const valueInfoMap = _.fromPairs( +const valueInfoMap = fromPairs( TextInputValidationOptions.map((option) => [option.value, option]) ); @@ -216,26 +216,19 @@ export const useTextInputProps = (props: RecordConstructorToView { + const onChangeRef = useRef( + (value: string) => { props.value.onChange(value); - }, 1000) + } ); - // Cleanup debounced function on unmount - useEffect(() => { - return () => { - debouncedOnChangeRef.current.cancel(); - }; - }, []); - const handleChange = (e: ChangeEvent) => { const value = e.target.value; setLocalInputValue(value); changeRef.current = true; touchRef.current = true; - debouncedOnChangeRef.current?.(value); + onChangeRef.current?.(value); }; // Cleanup refs on unmount @@ -244,6 +237,7 @@ export const useTextInputProps = (props: RecordConstructorToView { const { value, dispatch, style, mode, reverse, onEvent } = props; const [icons, setIcons] = useState([]); + const handleClickEvent = useCompClickEventHandler({onEvent}) + useEffect(() => { const loadIcons = async () => { const iconComponents = await Promise.all( @@ -140,7 +145,7 @@ const TimelineComp = ( e.preventDefault(); dispatch(changeChildAction("clickedObject", value, false)); dispatch(changeChildAction("clickedIndex", index, false)); - onEvent("click"); + handleClickEvent() }} // for responsiveness style={{ diff --git a/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx b/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx index d8c26d7ad8..b4b19d5228 100644 --- a/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx @@ -704,6 +704,7 @@ export const InputEventHandlerControl = eventHandlerControl([ export const ButtonEventHandlerControl = eventHandlerControl([ clickEvent, + doubleClickEvent, ] as const); export const ChangeEventHandlerControl = eventHandlerControl([ @@ -818,4 +819,5 @@ export const CardEventHandlerControl = eventHandlerControl([ clickExtraEvent, focusEvent, blurEvent, + doubleClickEvent ] as const); \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index e32c32c09f..1d36ec52c5 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -30,6 +30,7 @@ import { Option, WidthIcon, ImageCompIcon, + CloseEyeIcon, } from "lowcoder-design"; import styled from "styled-components"; import { lastValueIfEqual } from "util/objectUtils"; @@ -41,6 +42,26 @@ import { ColorControl } from "./colorControl"; import { StringStateControl } from "./codeStateControl"; import { reduceInContext } from "../utils/reduceContext"; +// Tag preset color options +const TAG_PRESET_COLORS = [ + { label: "Magenta", value: "magenta" }, + { label: "Red", value: "red" }, + { label: "Volcano", value: "volcano" }, + { label: "Orange", value: "orange" }, + { label: "Gold", value: "gold" }, + { label: "Lime", value: "lime" }, + { label: "Green", value: "green" }, + { label: "Cyan", value: "cyan" }, + { label: "Blue", value: "blue" }, + { label: "Geek Blue", value: "geekblue" }, + { label: "Purple", value: "purple" }, + { label: "Success", value: "success" }, + { label: "Processing", value: "processing" }, + { label: "Error", value: "error" }, + { label: "Warning", value: "warning" }, + { label: "Default", value: "default" }, +] as const; + const OptionTypes = [ { label: trans("prop.manual"), @@ -588,6 +609,7 @@ const ColumnOption = new MultiCompBuilder( radius: withDefault(RadiusControl, ""), margin: withDefault(StringControl, ""), padding: withDefault(StringControl, ""), + hidden: withDefault(BoolCodeControl, false), }, (props) => props ) @@ -624,6 +646,10 @@ const ColumnOption = new MultiCompBuilder( preInputNode: , placeholder: '3px', })} + {children.hidden.propertyView({ + label: trans('style.hideColumn'), + preInputNode: + })} )) .build(); @@ -729,24 +755,70 @@ let ColoredTagOption = new MultiCompBuilder( { label: StringControl, icon: IconControl, - color: withDefault(ColorControl, ""), + colorType: withDefault(dropdownControl([ + { label: "Preset", value: "preset" }, + { label: "Custom", value: "custom" }, + ] as const, "preset"), "preset"), + presetColor: withDefault(dropdownControl(TAG_PRESET_COLORS, "blue"), "blue"), + color: withDefault(ColorControl, "#1890ff"), + textColor: withDefault(ColorControl, "#ffffff"), + border: withDefault(ColorControl, ""), + radius: withDefault(RadiusControl, ""), + margin: withDefault(StringControl, ""), + padding: withDefault(StringControl, ""), + onEvent: ButtonEventHandlerControl, }, (props) => props ).build(); ColoredTagOption = class extends ColoredTagOption implements OptionCompProperty { propertyView(param: { autoMap?: boolean }) { + const colorType = this.children.colorType.getView(); return ( <> {this.children.label.propertyView({ label: trans("coloredTagOptionControl.tag") })} {this.children.icon.propertyView({ label: trans("coloredTagOptionControl.icon") })} - {this.children.color.propertyView({ label: trans("coloredTagOptionControl.color") })} + {this.children.colorType.propertyView({ + label: "Color Type", + radioButton: true + })} + {colorType === "preset" && this.children.presetColor.propertyView({ + label: "Preset Color" + })} + {colorType === "custom" && ( + <> + {this.children.color.propertyView({ label: trans("coloredTagOptionControl.color") })} + {this.children.textColor.propertyView({ label: "Text Color" })} + + )} + {this.children.border.propertyView({ + label: trans('style.border') + })} + {this.children.radius.propertyView({ + label: trans('style.borderRadius'), + preInputNode: , + placeholder: '3px', + })} + {this.children.margin.propertyView({ + label: trans('style.margin'), + preInputNode: , + placeholder: '3px', + })} + {this.children.padding.propertyView({ + label: trans('style.padding'), + preInputNode: , + placeholder: '3px', + })} + {this.children.onEvent.propertyView()} ); } }; export const ColoredTagOptionControl = optionsControl(ColoredTagOption, { - initOptions: [{ label: "Tag1", icon: "/icon:solid/tag", color: "#f50" }, { label: "Tag2", icon: "/icon:solid/tag", color: "#2db7f5" }], + initOptions: [ + { label: "Tag1", icon: "/icon:solid/tag", colorType: "preset", presetColor: "blue" }, + { label: "Tag2", icon: "/icon:solid/tag", colorType: "preset", presetColor: "green" } + ], uniqField: "label", }); \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/hooks/localStorageComp.ts b/client/packages/lowcoder/src/comps/hooks/localStorageComp.ts index e3ce4633f1..252ecaf1b3 100644 --- a/client/packages/lowcoder/src/comps/hooks/localStorageComp.ts +++ b/client/packages/lowcoder/src/comps/hooks/localStorageComp.ts @@ -3,7 +3,7 @@ import { isEmpty } from "lodash"; import { simpleMultiComp, stateComp, withViewFn } from "../generators"; import { NameConfig, withExposingConfigs } from "../generators/withExposing"; import { JSONObject } from "../../util/jsonTypes"; -import { useEffect } from "react"; +import { useEffect, useMemo, useCallback } from "react"; import isEqual from "fast-deep-equal"; import { trans } from "i18n"; import log from "loglevel"; @@ -13,7 +13,22 @@ const APP_STORE_NAMESPACE = "lowcoder_app_local_storage"; const LocalStorageCompBase = withViewFn( simpleMultiComp({ values: stateComp({}) }), (comp) => { - // add custom event listener to update values reactively + const originStore = localStorage.getItem(APP_STORE_NAMESPACE) || "{}"; + + let parseStore = {}; + try { + parseStore = JSON.parse(originStore); + } catch (e) { + log.error("application local storage invalid"); + } + + useEffect(() => { + const value = comp.children.values.value; + if (!isEqual(value, parseStore)) { + comp.children.values.dispatchChangeValueAction(parseStore); + } + }, [parseStore]); + useEffect(() => { const handler = () => { try { @@ -28,9 +43,6 @@ const LocalStorageCompBase = withViewFn( // Add listener on mount window.addEventListener("lowcoder-localstorage-updated", handler); - // Run once on mount to initialize - handler(); - return () => { window.removeEventListener("lowcoder-localstorage-updated", handler); }; diff --git a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx index 2977ad4b98..5c98ddb89d 100644 --- a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx @@ -14,7 +14,7 @@ import { Layers } from "constants/Layers"; import { HintPlaceHolder, Modal, Section, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { changeChildAction } from "lowcoder-core"; -import { CSSProperties, useCallback, useMemo, useRef } from "react"; +import { CSSProperties, useCallback, useEffect, useMemo, useRef } from "react"; import { ResizeHandle } from "react-resizable"; import styled, { css } from "styled-components"; import { useUserViewMode } from "util/hooks"; @@ -118,6 +118,12 @@ let TmpModalComp = (function () { const appID = useApplicationId(); const containerRef = useRef(null); + useEffect(() => { + return () => { + containerRef.current = null; + }; + }, []); + // Memoize body style const bodyStyle = useMemo(() => ({ padding: 0, @@ -171,11 +177,9 @@ let TmpModalComp = (function () { // Memoize container getter const getContainer = useCallback(() => { - if (!containerRef.current) { - containerRef.current = document.querySelector(`#${CanvasContainerID}`) || document.body; - } + containerRef.current = document.querySelector(`#${CanvasContainerID}`) || document.body; return containerRef.current; - }, []); + }, [CanvasContainerID]); // Memoize event handlers const handleCancel = useCallback((e: React.MouseEvent) => { @@ -228,6 +232,7 @@ let TmpModalComp = (function () { mask={props.showMask} className={clsx(`app-${appID}`, props.className)} data-testid={props.dataTestId as string} + destroyOnHidden > { if (datasourceType === "js" || datasourceType === "streamApi") { return JSTriggerTypeOptions; diff --git a/client/packages/lowcoder/src/comps/utils/useCompClickEventHandler.tsx b/client/packages/lowcoder/src/comps/utils/useCompClickEventHandler.tsx new file mode 100644 index 0000000000..e8f64cc5a4 --- /dev/null +++ b/client/packages/lowcoder/src/comps/utils/useCompClickEventHandler.tsx @@ -0,0 +1,47 @@ +import React, { useCallback, useRef } from "react"; + +export enum ClickEventType { + CLICK = "click", + DOUBLE_CLICK = "doubleClick" +} + +interface Props { + onEvent: (event: ClickEventType) => void; +} + +const DOUBLE_CLICK_THRESHOLD = 300; // ms + +export const useCompClickEventHandler = (props: Props) => { + const lastClickTimeRef = useRef(0); + const clickTimerRef = useRef>(); + + const handleClick = useCallback(() => { + const now = Date.now(); + + // Clear any existing timeout + if (clickTimerRef.current) { + clearTimeout(clickTimerRef.current); + } + + if ((now - lastClickTimeRef.current) < DOUBLE_CLICK_THRESHOLD) { + props.onEvent(ClickEventType.DOUBLE_CLICK); + } else { + clickTimerRef.current = setTimeout(() => { + props.onEvent(ClickEventType.CLICK); + }, DOUBLE_CLICK_THRESHOLD); + } + + lastClickTimeRef.current = now; + }, [props.onEvent]); + + // Cleanup on unmount + React.useEffect(() => { + return () => { + if (clickTimerRef.current) { + clearTimeout(clickTimerRef.current); + } + }; + }, []); + + return handleClick; +}; diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index 17d38343d0..4df6ce4720 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -2458,8 +2458,10 @@ export const de = { "usageLogDevices": "Gerät/OS Aufschlüsselung", "usageLogBrowsers": "Browser/Layout Engine Aufschlüsselung", "premiumFeaturesNotice": "Alle Premium Features sind in der Enterprise Edition von Lowcoder verfügbar.", +"readMoreNotice" : "Erfahren Sie mehr über die Enterprise Edition und wie Sie sie ganz einfach installieren können.", +"readMoreButton": "Details zur Enterprise Edition", "requestLicense": "Lizenzen für die Enterprise Edition anfordern", -"requestLicensesBtton": "Request Enterprise Access", +"requestLicensesBtton": "Enterprise Edition anfragen", "AuditLogsTitle": "Audit Logs", "AuditLogsIntroTitle": "Leistungsstarker Einblick in die Aktivitäten Ihres Arbeitsbereichs", "AuditLogsIntro1": "Mithilfe von Audit-Protokollen können Administratoren genau verfolgen, was auf der gesamten Lowcoder-Plattform passiert. Von Benutzeranmeldungen bis hin zu App-Änderungen wird jede relevante Aktion erfasst und gespeichert.", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index a88b9debdd..de24d5b64a 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -591,6 +591,7 @@ export const en = { "chartBorderColor": "Border Color", "chartTextColor": "Text Color", "detailSize": "Detail Size", + "hideColumn": "Hide Column", "radiusTip": "Specifies the radius of the element's corners. Example: 5px, 50%, or 1em.", "gapTip": "Specifies the gap between rows and columns in a grid or flex container. Example: 10px, 1rem, or 5%.", @@ -2448,8 +2449,10 @@ export const en = { "usageLogDevices" : "Device/OS Breakdown", "usageLogBrowsers" : "Browser/Layout Engine Breakdown", "premiumFeaturesNotice" : "All Premium Features are avilable in the Enterprise Edition of Lowcoder.", + "readMoreNotice" : "Learn more about the Enterprise Edition and how to install it easily.", + "readMoreButton" : "Enterprise Edition Details", "requestLicense" : "Request Enterprise Edition Licenses", - "requestLicensesBtton" : "Request Enterprise Access", + "requestLicensesBtton" : "Unlock Enterprise Features", "AuditLogsTitle": "Audit Logs", "AuditLogsIntroTitle": "Powerful visibility into your workspace activity", "AuditLogsIntro1": "Audit Logs enable administrators to track exactly what happens across the entire Lowcoder platform. From user sign-ins to app modifications, every relevant action is captured and stored.", @@ -4475,6 +4478,7 @@ export const en = { "autoComplete": { "value": "Auto Complete Value", "checkedValueFrom": "Checked Value From", + "filterOptionsByInput": "Filter Options By Input", "ignoreCase": "Search Ignore Case", "searchLabelOnly": "Search Label Only", "searchFirstPY": "Search First Pinyin", diff --git a/client/packages/lowcoder/src/i18n/locales/es.ts b/client/packages/lowcoder/src/i18n/locales/es.ts index 6062930932..44623f20ee 100644 --- a/client/packages/lowcoder/src/i18n/locales/es.ts +++ b/client/packages/lowcoder/src/i18n/locales/es.ts @@ -2458,6 +2458,8 @@ export const es = { "usageLogDevices": "Desglose de dispositivos/OS", "usageLogBrowsers": "Desglose del navegador/motor de diseño", "premiumFeaturesNotice": "Todas las características Premium están disponibles en la Edición Enterprise de Lowcoder.", +"readMoreNotice" : "Conozca más sobre la edición Enterprise y cómo instalarla fácilmente.", +"readMoreButton": "Detalles de la edición Enterprise", "requestLicense": "Solicitar licencias de Enterprise Edition", "requestLicensesBtton": "Solicitar acceso para empresas", "AuditLogsTitle": "Registros de auditoría", diff --git a/client/packages/lowcoder/src/i18n/locales/it.ts b/client/packages/lowcoder/src/i18n/locales/it.ts index bb2833901e..c6bdbd7236 100644 --- a/client/packages/lowcoder/src/i18n/locales/it.ts +++ b/client/packages/lowcoder/src/i18n/locales/it.ts @@ -2458,6 +2458,8 @@ export const it = { "usageLogDevices": "Ripartizione dispositivi/OS", "usageLogBrowsers": "Browser/Motore di layout", "premiumFeaturesNotice": "Tutte le funzioni Premium sono disponibili nell'edizione Enterprise di Lowcoder.", +"readMoreNotice" : "Scopri di più sulla versione Enterprise e su come installarla facilmente.", +"readMoreButton": "Dettagli dell'edizione Enterprise", "requestLicense": "Richiesta di licenze Enterprise Edition", "requestLicensesBtton": "Richiesta di accesso aziendale", "AuditLogsTitle": "Registri di controllo", diff --git a/client/packages/lowcoder/src/i18n/locales/pt.ts b/client/packages/lowcoder/src/i18n/locales/pt.ts index a4d094bb76..c512002273 100644 --- a/client/packages/lowcoder/src/i18n/locales/pt.ts +++ b/client/packages/lowcoder/src/i18n/locales/pt.ts @@ -2458,6 +2458,8 @@ export const pt = { "usageLogDevices": "Discriminação por dispositivo/SO", "usageLogBrowsers": "Navegador/motor de apresentação", "premiumFeaturesNotice": "Todas as funcionalidades Premium estão disponíveis na Enterprise Edition do Lowcoder.", +"readMoreNotice" : "Saiba mais sobre a edição Enterprise e como instalá-la com facilidade.", +"readMoreButton": "Detalhes da edição Enterprise", "requestLicense": "Solicitar licenças da Enterprise Edition", "requestLicensesBtton": "Pedir acesso à empresa", "AuditLogsTitle": "Registos de auditoria", diff --git a/client/packages/lowcoder/src/i18n/locales/ru.ts b/client/packages/lowcoder/src/i18n/locales/ru.ts index 806eea60ee..5ce0435c35 100644 --- a/client/packages/lowcoder/src/i18n/locales/ru.ts +++ b/client/packages/lowcoder/src/i18n/locales/ru.ts @@ -2458,6 +2458,8 @@ export const ru = { "usageLogDevices": "Разбивка по устройствам/ОС", "usageLogBrowsers": "Разбивка браузера/программного обеспечения", "premiumFeaturesNotice": "Все премиум-функции доступны в корпоративной версии Lowcoder.", +"readMoreNotice" : "Узнайте больше о версии Enterprise и о том, как легко её установить.", +"readMoreButton": "Подробности о версии Enterprise", "requestLicense": "Запрос лицензий Enterprise Edition", "requestLicensesBtton": "Запрос доступа к предприятию", "AuditLogsTitle": "Журналы аудита", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 797ed9354f..69555a8d1d 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -2459,6 +2459,8 @@ export const zh = { "usageLogDevices": "设备/操作系统明细", "usageLogBrowsers": "浏览器/布局引擎细分", "premiumFeaturesNotice": "Lowcoder 企业版可提供所有高级功能。", + "readMoreNotice" : "了解有关企业版的更多信息,以及如何轻松安装它。", + "readMoreButton": "企业版详情", "requestLicense": "申请企业版许可证", "requestLicensesBtton": "申请企业访问权限", "AuditLogsTitle": "审计日志", diff --git a/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx b/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx index 0e6a228d39..739e87723f 100644 --- a/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx +++ b/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx @@ -242,18 +242,8 @@ const ResizableChildren = React.memo((props: { children: JSX.Element | React.ReactNode; }) => { const { ref: innerRef } = useResizeDetector({ - skipOnMount: ( - props.compType === 'responsiveLayout' - || props.compType === 'columnLayout' - || props.compType === 'pageLayout' - || props.compType === 'splitLayout' - || props.compType === 'floatTextContainer' - || props.compType === 'tabbedContainer' - || props.compType === 'collapsibleContainer' - || props.compType === 'container' - ), refreshMode: "debounce", - refreshRate: 0, + refreshRate: 10, onResize: ({width, height}: ResizePayload) => props.onInnerResize(width ?? undefined, height ?? undefined), observerOptions: { box: "border-box" } }); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/NewsLayout.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/NewsLayout.tsx index c9db2e1fe1..0075e23b88 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/NewsLayout.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/NewsLayout.tsx @@ -98,8 +98,6 @@ export function NewsLayout() { .catch(err => console.error("Failed to load news:", err)); }, []); - console.log(youTubeData); - return ( @@ -110,6 +108,69 @@ export function NewsLayout() {

Lowcoder {trans("home.news")}

+ + 📝 Latest Blog Posts + + {hubspotData?.map((item: { htmlTitle: any; publishDate: any; postSummary: any; url: any; featuredImage: any; metaDescription: any; }, idx: any) => { + const { + htmlTitle, + publishDate, + postSummary, + url, + featuredImage, + metaDescription, + } = item; + + const summaryHtml = postSummary || metaDescription || ""; + const coverImage = featuredImage || "https://placehold.co/600x400?text=Lowcoder+Blog"; + + // Strip HTML to plain text + const stripHtml = (html: string): string => { + const div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; + }; + + const plainSummary = stripHtml(summaryHtml); + + return ( + + + + + } + > + + {htmlTitle} + + } + description={ + <> + + {new Date(publishDate).toLocaleDateString()} + + + {plainSummary} + + + } + /> + + + ); + })} + + + 📺 Latest YouTube Videos @@ -173,17 +234,7 @@ export function NewsLayout() { - 📝 Latest Blog Posts - - {hubspotData.length === 0 && ( - - No blog posts available at the moment. - - )} - - - diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index 4dd184f2eb..4faaf6a3fb 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -11,6 +11,7 @@ import { NEWS_URL, ORG_HOME_URL, SUBSCRIPTION_SETTING, + ENVIRONMENT_SETTING, } from "constants/routesURL"; import { getUser, isFetchingUser } from "redux/selectors/usersSelectors"; import { useDispatch, useSelector } from "react-redux"; @@ -231,6 +232,20 @@ export default function ApplicationHome() { } ] }, + { + items: [ + { + text: {trans("environments.detail_enterpriseEdition")}, + routePath: ENVIRONMENT_SETTING, + routeComp: Setting, + routePathExact: false, + icon: ({ selected, ...otherProps }) => , + mobileVisible: true, + visible: () => !isLicenseActive, + style: { color: "#ff6f3c" }, + } + ] + }, { items: [ diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index cf9e6d7929..4f083cc186 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -224,7 +224,7 @@ export default function ProfileDropdown(props: DropDownProps) { const switchOrgMenu = { key: 'switchOrg', label: trans("profile.switchOrg"), - popupOffset: [4, -12], + popupOffset: checkIsMobile(window.innerWidth) ? [-200, 36] : [4, -12], children: [ { key: 'joinedOrg', diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index c11d42e9cb..2c7f0de92e 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -498,8 +498,6 @@ function EditorView(props: EditorViewProps) { return () => { window.removeEventListener(eventType, updateSize); - savePanelStatus(panelStatus); - saveEditorModeStatus(editorModeStatus); }; }, [panelStatus, editorModeStatus]); @@ -553,6 +551,8 @@ function EditorView(props: EditorViewProps) { setShowShortcutList(false); setMenuKey(SiderKey.State); setHeight(undefined); + savePanelStatus(panelStatus); + saveEditorModeStatus(editorModeStatus); }; }, []); diff --git a/client/packages/lowcoder/src/pages/setting/appUsage/index.tsx b/client/packages/lowcoder/src/pages/setting/appUsage/index.tsx index c8e1e81e80..d17d72f417 100644 --- a/client/packages/lowcoder/src/pages/setting/appUsage/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/appUsage/index.tsx @@ -55,6 +55,7 @@ const AppUsageDoc = () => { const user = useSelector(getUser); const deploymentId = useSelector(getDeploymentId); const dispatch = useDispatch(); +const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud'; const apiUsage = useSelector(getOrgApiUsage); useEffect(() => { @@ -122,12 +123,6 @@ const apiUsage = useSelector(getOrgApiUsage); - - -

{deploymentId}

-
-
- {trans("enterprise.PricingIntro")} diff --git a/client/packages/lowcoder/src/pages/setting/audit/index.tsx b/client/packages/lowcoder/src/pages/setting/audit/index.tsx index 552b191175..edbc3840a5 100644 --- a/client/packages/lowcoder/src/pages/setting/audit/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/audit/index.tsx @@ -54,7 +54,8 @@ const Audit = () => { const user = useSelector(getUser); const deploymentId = useSelector(getDeploymentId); const dispatch = useDispatch(); - + const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud'; + const apiUsage = useSelector(getOrgApiUsage); useEffect(() => { dispatch(fetchAPIUsageAction(user.currentOrgId)); @@ -162,12 +163,6 @@ const Audit = () => { - - -

{deploymentId}

-
-
- {trans("enterprise.PricingIntro")} diff --git a/client/packages/lowcoder/src/pages/setting/branding/index.tsx b/client/packages/lowcoder/src/pages/setting/branding/index.tsx index 410842f4c9..9c1d34fcba 100644 --- a/client/packages/lowcoder/src/pages/setting/branding/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/branding/index.tsx @@ -45,6 +45,7 @@ const BrandingPromo = () => { const user = useSelector(getUser); const deploymentId = useSelector(getDeploymentId); const dispatch = useDispatch(); + const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud'; const apiUsage = useSelector(getOrgApiUsage); useEffect(() => { @@ -152,12 +153,6 @@ const BrandingPromo = () => { {trans("enterprise.BrandingWhatsNewIntro")} - - - -

{deploymentId}

-
-
diff --git a/client/packages/lowcoder/src/pages/setting/environments/index.tsx b/client/packages/lowcoder/src/pages/setting/environments/index.tsx index 1b77787084..967b8d70a1 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/index.tsx @@ -44,6 +44,7 @@ const EnvironmentsPromo = () => { const [modalOpen, setModalOpen] = useState(false); const user = useSelector(getUser); const deploymentId = useSelector(getDeploymentId); + const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud'; const dispatch = useDispatch(); @@ -129,11 +130,11 @@ const EnvironmentsPromo = () => { - + {!isLowCoderDomain &&

{deploymentId}

-
+
} diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 762bb47743..3d4504942c 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -95,8 +95,8 @@ export async function getEnvironments(): Promise { return response.data.data || []; } catch (error) { const errorMessage = - error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchEnvironments"); - messageInstance.error(errorMessage); + error && error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchEnvironments"); + console.error(errorMessage); throw error; } } @@ -150,7 +150,7 @@ export async function getEnvironmentById(id: string): Promise { } catch (error) { const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchEnvironment"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -223,7 +223,7 @@ export async function getEnvironmentWorkspaces( // Handle and transform error const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchWorkspaces"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -273,7 +273,7 @@ export async function getEnvironmentUserGroups( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchUserGroups"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -383,7 +383,7 @@ export async function getWorkspaceApps( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchWorkspaceApps"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -441,7 +441,7 @@ export async function getWorkspaceDataSources( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchWorkspaceDataSources"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -523,7 +523,7 @@ export async function getWorkspaceQueries( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchWorkspaceQueries"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -575,7 +575,7 @@ export async function getEnvironmentsWithLicenseStatus(): Promise } catch (error) { const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchEnvironments"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } @@ -618,7 +618,7 @@ export async function getEnvironmentDeploymentId( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : trans("environments.services_environments_failedToFetchDeploymentId"); - messageInstance.error(errorMessage); + console.error(errorMessage); throw error; } } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/hubspotModal.tsx b/client/packages/lowcoder/src/pages/setting/hubspotModal.tsx index 354ede1bbd..c517570d04 100644 --- a/client/packages/lowcoder/src/pages/setting/hubspotModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/hubspotModal.tsx @@ -41,6 +41,9 @@ interface Props { } export function HubspotModal({ open, onClose, orgId, deploymentIds }: Props) { + + const isLowCoderDomain = typeof window !== "undefined" && window.location.hostname === "app.lowcoder.cloud"; + useEffect(() => { if (!open) return; @@ -96,21 +99,25 @@ export function HubspotModal({ open, onClose, orgId, deploymentIds }: Props) { {orgId} - - Deployment IDs: - + + { !isLowCoderDomain && + <> + + Deployment IDs: + + + {deploymentIds.length === 0 ? ( + No deployments found. + ) : ( + deploymentIds.map((id, idx) => ( + + {id} + + )) + )} + + } - - {deploymentIds.length === 0 ? ( - No deployments found. - ) : ( - deploymentIds.map((id, idx) => ( - - {id} - - )) - )} - diff --git a/client/packages/lowcoder/src/pages/setting/settingHome.tsx b/client/packages/lowcoder/src/pages/setting/settingHome.tsx index 0017e598fc..548a72cd71 100644 --- a/client/packages/lowcoder/src/pages/setting/settingHome.tsx +++ b/client/packages/lowcoder/src/pages/setting/settingHome.tsx @@ -165,9 +165,31 @@ export function SettingHome() {
{trans("enterprise.premiumFeaturesNotice")}
- + +
+ {trans("enterprise.readMoreNotice")} +
+ + + )} diff --git a/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx b/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx index e2ae339ed3..f892755e29 100644 --- a/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx +++ b/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx @@ -5,6 +5,7 @@ import { Card, Button } from 'antd'; import { SettingOutlined, CheckCircleOutlined, LoadingOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { buildSubscriptionSettingsLink, buildSubscriptionInfoLink } from "constants/routesURL"; import history from "util/history"; +import { trans } from "i18n"; const ProductCardContainer = styled(Card)` width: 300px; @@ -87,8 +88,8 @@ export const ProductCard: React.FC = ({ ) : ( !activeSubscription && ( checkoutLinkDataLoaded ? ( - ) : ( diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index 7b3bf927d0..41fb54f3d8 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -7,7 +7,7 @@ type: application version: 2.7.0 # Lowcoder version -appVersion: "2.7.0" +appVersion: "2.7.2" # Dependencies needed for Lowcoder deployment dependencies: diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/repository/OrganizationRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/repository/OrganizationRepository.java index 7fceace3e9..d6606fde20 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/repository/OrganizationRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/repository/OrganizationRepository.java @@ -6,6 +6,8 @@ import org.lowcoder.domain.organization.model.OrganizationState; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.stereotype.Repository; +import org.springframework.data.domain.Pageable; +import java.util.List; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -31,4 +33,7 @@ public interface OrganizationRepository extends ReactiveMongoRepository findByOrganizationDomainIsNotNull(); Mono existsBySlug(String slug); + + Flux findByIdInAndNameContainingIgnoreCase(List ids, String name, Pageable pageable); + Mono countByIdInAndNameContainingIgnoreCase(List ids, String name); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java index 6b375d4d2d..fa9b0cd5e2 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java @@ -8,6 +8,7 @@ import org.lowcoder.infra.annotation.NonEmptyMono; import org.lowcoder.infra.annotation.PossibleEmptyMono; import org.springframework.http.codec.multipart.Part; +import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -52,4 +53,7 @@ public interface OrganizationService { Mono updateCommonSettings(String orgId, String key, Object value); Mono updateSlug(String organizationId, String newSlug); + + Flux findUserOrgs(String userId, String orgName, Pageable pageable); + Mono countUserOrgs(String userId, String orgName); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 781ffe257d..39c26d9906 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -18,6 +18,8 @@ import org.lowcoder.domain.user.repository.UserRepository; import org.lowcoder.domain.util.SlugUtils; import org.lowcoder.infra.annotation.PossibleEmptyMono; +import org.lowcoder.infra.birelation.BiRelationService; +import org.lowcoder.infra.birelation.BiRelation; import org.lowcoder.infra.mongo.MongoUpsertHelper; import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.config.dynamic.Conf; @@ -31,6 +33,7 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Service; +import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -41,6 +44,7 @@ import static org.lowcoder.domain.organization.model.OrganizationState.DELETED; import static org.lowcoder.domain.util.QueryDslUtils.fieldName; import static org.lowcoder.sdk.exception.BizError.UNABLE_TO_FIND_VALID_ORG; +import static org.lowcoder.infra.birelation.BiRelationBizType.ORG_MEMBER; import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; import static org.lowcoder.sdk.util.LocaleUtils.getLocale; @@ -62,6 +66,7 @@ public class OrganizationServiceImpl implements OrganizationService { private final ApplicationContext applicationContext; private final CommonConfig commonConfig; private final ConfigCenter configCenter; + private final BiRelationService biRelationService; @PostConstruct private void init() @@ -315,4 +320,31 @@ public Mono updateSlug(String organizationId, String newSlug) { }); }); } + + @Override + public Flux findUserOrgs(String userId, String orgName, Pageable pageable) { + return biRelationService.getByTargetId(ORG_MEMBER, userId) + .map(BiRelation::getSourceId) + .collectList() + .flatMapMany(orgIds -> { + if (orgIds.isEmpty()) { + return Flux.empty(); + } + return repository.findByIdInAndNameContainingIgnoreCase(orgIds, orgName, pageable); + }); + } + + @Override + public Mono countUserOrgs(String userId, String orgName) { + String filter = orgName == null ? "" : orgName; + return biRelationService.getByTargetId(ORG_MEMBER, userId) + .map(BiRelation::getSourceId) + .collectList() + .flatMap(orgIds -> { + if (orgIds.isEmpty()) { + return Mono.just(0L); + } + return repository.countByIdInAndNameContainingIgnoreCase(orgIds, filter); + }); + } } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java index 8512772fba..64e1847ab6 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java @@ -1,21 +1,29 @@ package org.lowcoder.domain.serversetting.service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.lowcoder.domain.serversetting.model.ServerSetting; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.*; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +@RequiredArgsConstructor @Slf4j @Service public class ServerSettingServiceImpl implements ServerSettingService { + + private final Environment environment; private final ServerSettingRepository repository; + private final List EXCLUDED_KEYS = List.of("LOWCODER_MONGODB_EXPOSED", "LOWCODER_PUID", "LOWCODER_PGID", @@ -33,11 +41,6 @@ public class ServerSettingServiceImpl implements ServerSettingService { "LOWCODER_NODE_SERVICE_SECRET", "LOWCODER_NODE_SERVICE_SECRET_SALT"); - @Autowired - public ServerSettingServiceImpl(ServerSettingRepository repository) { - this.repository = repository; - } - @Override public Mono> getServerSettingsMap() { return repository.findAll().collectMap(ServerSetting::getKey, ServerSetting::getValue); @@ -45,9 +48,18 @@ public Mono> getServerSettingsMap() { @PostConstruct public void saveEnvironmentVariables() { - Map envVariables = System.getenv(); - Flux.fromIterable(envVariables.keySet()) - .filter(key -> key.startsWith("LOWCODER_")) + + Map defaults = getEnvironmentVariablesDefaults(); + + Map envVariables = new TreeMap<>(System.getenv().entrySet().stream() + .filter(entry -> StringUtils.startsWith(entry.getKey(), "LOWCODER_")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + Map merged = new TreeMap<>(defaults); + merged.keySet().removeAll(envVariables.keySet()); + merged.putAll(envVariables); + + Flux.fromIterable(merged.keySet()) .map(key -> { String value = envVariables.getOrDefault(key, ""); if(EXCLUDED_KEYS.contains(key)) { @@ -61,4 +73,30 @@ public void saveEnvironmentVariables() { .flatMap(repository::save) .subscribe(); } + + + private Map getEnvironmentVariablesDefaults() { + Map defaults = new HashMap<>(); + + MutablePropertySources propertySources = ((AbstractEnvironment) environment).getPropertySources(); + StreamSupport.stream(propertySources.spliterator(), false) + .filter(EnumerablePropertySource.class::isInstance) + .map(EnumerablePropertySource.class::cast) + .forEach(propertySource -> { + String[] names = propertySource.getPropertyNames(); + if (names.length > 0) { + Arrays.stream(names).forEach(name -> { + String rawValue = Objects.toString(propertySource.getProperty(name), ""); + if (rawValue != null && StringUtils.contains(rawValue, "${LOWCODER_")) { + String defaultValue = StringUtils.substringBetween(rawValue, "${", "}"); + String[] keyValue = StringUtils.split(defaultValue, ":"); + if (keyValue.length == 2 && !defaults.containsKey(keyValue[0])) { + defaults.put(keyValue[0], keyValue[1]); + } + } + }); + } + }); + return defaults; + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java index 11f5bd9535..7c13cdc578 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java @@ -12,128 +12,111 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.ServiceLoader; +import java.util.*; @Slf4j @RequiredArgsConstructor @Component -public class PathBasedPluginLoader implements PluginLoader -{ +public class PathBasedPluginLoader implements PluginLoader { private final CommonConfig common; private final ApplicationHome applicationHome; - + + // Cache for plugin JAR paths to avoid redundant filesystem scans + private static final Map> cachedPluginJars = new HashMap<>(); + @Override - public List loadPlugins() - { + public List loadPlugins() { List plugins = new ArrayList<>(); - + + // Find plugin JARs using caching List pluginJars = findPluginsJars(); - if (pluginJars.isEmpty()) - { + if (pluginJars.isEmpty()) { + log.debug("No plugin JARs found."); return plugins; } - for (String pluginJar : pluginJars) - { + // Load plugins from JARs + pluginJars.parallelStream().forEach(pluginJar -> { log.debug("Inspecting plugin jar candidate: {}", pluginJar); List loadedPlugins = loadPluginCandidates(pluginJar); - if (loadedPlugins.isEmpty()) - { + if (loadedPlugins.isEmpty()) { log.debug(" - no plugins found in the jar file"); + } else { + synchronized (plugins) { + plugins.addAll(loadedPlugins); + } } - else - { - for (LowcoderPlugin plugin : loadedPlugins) - { - plugins.add(plugin); - } - } - } - + }); + return plugins; } - - protected List findPluginsJars() - { + + protected List findPluginsJars() { + String cacheKey = common.getPluginDirs().toString(); + + // Use cached JAR paths if available + if (cachedPluginJars.containsKey(cacheKey)) { + log.debug("Using cached plugin jar candidates for key: {}", cacheKey); + return cachedPluginJars.get(cacheKey); + } + List candidates = new ArrayList<>(); - if (CollectionUtils.isNotEmpty(common.getPluginDirs())) - { - for (String pluginDir : common.getPluginDirs()) - { + if (CollectionUtils.isNotEmpty(common.getPluginDirs())) { + for (String pluginDir : common.getPluginDirs()) { final Path pluginPath = getAbsoluteNormalizedPath(pluginDir); - if (pluginPath != null) - { + if (pluginPath != null) { candidates.addAll(findPluginCandidates(pluginPath)); } } } - + + // Cache the results + cachedPluginJars.put(cacheKey, candidates); return candidates; } - - protected List findPluginCandidates(Path pluginsDir) - { - List pluginCandidates = new ArrayList<>(); - try - { - Files.walk(pluginsDir) - .filter(Files::isRegularFile) - .filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar")) - .forEach(path -> pluginCandidates.add(path.toString())); - } - catch(IOException cause) - { + protected List findPluginCandidates(Path pluginsDir) { + try { + return Files.walk(pluginsDir) + .filter(Files::isRegularFile) + .filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar")) + .map(Path::toString) + .toList(); // Use Java 16+ `toList()` for better performance + } catch (IOException cause) { log.error("Error walking plugin folder! - {}", cause.getMessage()); + return Collections.emptyList(); } - - return pluginCandidates; } - - protected List loadPluginCandidates(String pluginJar) - { + + protected List loadPluginCandidates(String pluginJar) { List pluginCandidates = new ArrayList<>(); - try - { + try { Path pluginPath = Path.of(pluginJar); PluginClassLoader pluginClassLoader = new PluginClassLoader(pluginPath.getFileName().toString(), pluginPath); ServiceLoader pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader); - if (pluginServices != null ) - { - Iterator pluginIterator = pluginServices.iterator(); - while(pluginIterator.hasNext()) - { - LowcoderPlugin plugin = pluginIterator.next(); + if (pluginServices != null) { + for (LowcoderPlugin plugin : pluginServices) { log.debug(" - loaded plugin: {} - {}", plugin.pluginId(), plugin.description()); pluginCandidates.add(plugin); } } - } - catch(Throwable cause) - { + } catch (Throwable cause) { log.warn("Error loading plugin!", cause); } - + return pluginCandidates; } - - private Path getAbsoluteNormalizedPath(String path) - { - if (StringUtils.isNotBlank(path)) - { + + private Path getAbsoluteNormalizedPath(String path) { + if (StringUtils.isNotBlank(path)) { Path absPath = Path.of(path); - if (!absPath.isAbsolute()) - { + if (!absPath.isAbsolute()) { absPath = Path.of(applicationHome.getDir().getAbsolutePath(), absPath.toString()); } return absPath.normalize().toAbsolutePath(); } - return null; } -} +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java index ee02ce0abf..d8520a9d54 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java @@ -165,28 +165,7 @@ public Mono getUserHomePageView(ApplicationType applicationTyp .zipWith(folderApiService.getElements(null, applicationType, null, null).collectList()) .map(tuple2 -> { Organization organization = tuple2.getT1(); - List list = tuple2.getT2(); - List applicationInfoViews = list.stream() - .map(o -> { - if (o instanceof ApplicationInfoView applicationInfoView) { - return applicationInfoView; - } - return null; - }) - .filter(Objects::nonNull) - .toList(); - List folderInfoViews = list.stream() - .map(o -> { - if (o instanceof FolderInfoView folderInfoView) { - return folderInfoView; - } - return null; - }) - .filter(Objects::nonNull) - .toList(); userHomepageVO.setOrganization(organization); - userHomepageVO.setHomeApplicationViews(applicationInfoViews); - userHomepageVO.setFolderInfoViews(folderInfoViews); return userHomepageVO; }); }); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java index 2901aeb0dc..c87732d35c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java @@ -53,5 +53,7 @@ public interface OrgApiService { Mono getOrganizationConfigs(String orgId); Mono getApiUsageCount(String orgId, Boolean lastMonthOnly); + + Mono getOrganizationMembersForSearch(String orgId, String searchMemberName, String searchGroupId, Integer pageNum, Integer pageSize); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 5853bfcf25..2a5b0d0c30 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -40,6 +40,9 @@ import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +import reactor.core.publisher.Flux; +import org.lowcoder.domain.group.service.GroupMemberService; +import org.lowcoder.domain.group.model.GroupMember; import java.util.*; import java.util.stream.Collectors; @@ -49,6 +52,8 @@ import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; import static org.lowcoder.sdk.util.StreamUtils.collectSet; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; @Slf4j @Service @@ -72,9 +77,10 @@ public class OrgApiServiceImpl implements OrgApiService { private GroupService groupService; @Autowired private AuthenticationService authenticationService; - @Autowired private ServerLogService serverLogService; + @Autowired + private GroupMemberService groupMemberService; @Override public Mono getOrganizationMembers(String orgId, int page, int count) { @@ -84,6 +90,78 @@ public Mono getOrganizationMembers(String orgId, int page, in .then(getOrgMemberListView(orgId, page, count)); } +// Update getOrgMemberListViewForSearch to filter by group membership +private Mono getOrgMemberListViewForSearch(String orgId, String searchMemberName, String searchGroupId, Integer page, Integer pageSize) { + return orgMemberService.getOrganizationMembers(orgId) + .collectList() + .flatMap(orgMembers -> { + List userIds = orgMembers.stream() + .map(OrgMember::getUserId) + .collect(Collectors.toList()); + Mono> users = userService.getByIds(userIds); + + // If searchGroupId is provided, fetch group members + Mono> groupUserIdsMono = StringUtils.isBlank(searchGroupId) + ? Mono.just(Collections.emptySet()) + : groupMemberService.getGroupMembers(searchGroupId) + .map(list -> list.stream() + .map(GroupMember::getUserId) + .collect(Collectors.toSet())); + + return Mono.zip(users, groupUserIdsMono) + .map(tuple -> { + Map userMap = tuple.getT1(); + Set groupUserIds = tuple.getT2(); + + var list = orgMembers.stream() + .map(orgMember -> { + User user = userMap.get(orgMember.getUserId()); + if (user == null) { + log.warn("user {} not exist and will be removed from the result.", orgMember.getUserId()); + return null; + } + return buildOrgMemberView(user, orgMember); + }) + .filter(Objects::nonNull) + .filter(orgMemberView -> { + // Filter by name + boolean matchesName = StringUtils.isBlank(searchMemberName) || + StringUtils.containsIgnoreCase(orgMemberView.getName(), searchMemberName); + + // Filter by group + boolean matchesGroup = StringUtils.isBlank(searchGroupId) || + groupUserIds.contains(orgMemberView.getUserId()); + + return matchesName && matchesGroup; + }) + .collect(Collectors.toList()); + var pageTotal = list.size(); + list = list.subList((page - 1) * pageSize, pageSize == 0 ? pageTotal : Math.min(page * pageSize, pageTotal)); + return Pair.of(list, pageTotal); + }); + }) + .zipWith(sessionUserService.getVisitorOrgMemberCache()) + .map(tuple -> { + List memberViews = tuple.getT1().getLeft(); + var pageTotal = tuple.getT1().getRight(); + OrgMember orgMember = tuple.getT2(); + return OrgMemberListView.builder() + .members(memberViews) + .total(pageTotal) + .pageNum(page) + .pageSize(pageSize) + .visitorRole(orgMember.getRole().getValue()) + .build(); + }); + } + @Override + public Mono getOrganizationMembersForSearch(String orgId, String searchMemberName, String searchGroupId, Integer page, Integer pageSize) { + return sessionUserService.getVisitorId() + .flatMap(visitorId -> orgMemberService.getOrgMember(orgId, visitorId)) + .switchIfEmpty(deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED")) + .then(getOrgMemberListViewForSearch(orgId, searchMemberName, searchGroupId, page, pageSize)); + } + private Mono getOrgMemberListView(String orgId, int page, int count) { return orgMemberService.getOrganizationMembers(orgId) .collectList() @@ -136,6 +214,17 @@ protected OrgMemberView build(User user, OrgMember orgMember) { .rawUserInfos(findRawUserInfos(user, orgId)) .build(); } + protected OrgMemberView buildOrgMemberView(User user, OrgMember orgMember) { + String orgId = orgMember.getOrgId(); + return OrgMemberView.builder() + .name(user.getName()) + .userId(user.getId()) + .role(orgMember.getRole().getValue()) + .avatarUrl(user.getAvatarUrl()) + .joinTime(orgMember.getJoinTime()) + .rawUserInfos(findRawUserInfos(user, orgId)) + .build(); + } protected Map> findRawUserInfos(User user, String orgId) { return SetUtils.emptyIfNull(user.getConnections()) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java index 55221cd71b..f73758127d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java @@ -12,6 +12,7 @@ import org.lowcoder.api.usermanagement.view.UpdateRoleRequest; import org.lowcoder.api.util.BusinessEventPublisher; import org.lowcoder.api.util.GidService; +import org.lowcoder.domain.organization.model.OrgMember; import org.lowcoder.domain.organization.model.Organization; import org.lowcoder.domain.organization.model.Organization.OrganizationCommonSettings; import org.lowcoder.domain.organization.service.OrgMemberService; @@ -117,6 +118,16 @@ public Mono> getOrgMembers(@PathVariable String orgApiService.getOrganizationMembers(id, pageNum, pageSize) .map(ResponseView::success)); } + @Override + public Mono> getOrgMembersForSearch(@PathVariable String orgId, + @PathVariable String searchMemberName, + @PathVariable String searchGroupId, + @RequestParam(required = false, defaultValue = "1") int pageNum, + @RequestParam(required = false, defaultValue = "1000") int pageSize) { + return gidService.convertOrganizationIdToObjectId(orgId).flatMap(id -> + orgApiService.getOrganizationMembersForSearch(id, searchMemberName, searchGroupId, pageNum, pageSize) + .map(ResponseView::success)); + } @Override public Mono> updateRoleForMember(@RequestBody UpdateRoleRequest updateRoleRequest, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java index 86ed6888b2..6fee2a511f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java @@ -98,6 +98,13 @@ public Mono> getOrgMembers(@PathVariable String @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "1000") int pageSize); + @GetMapping("/{orgId}/{searchMemberName}/{searchGroupId}/members") + public Mono> getOrgMembersForSearch(@PathVariable String orgId, + @PathVariable String searchMemberName, + @PathVariable String searchGroupId, + @RequestParam(required = false, defaultValue = "1") int pageNum, + @RequestParam(required = false, defaultValue = "1000") int pageSize); + @Operation( tags = TAG_ORGANIZATION_MEMBERS, operationId = "updateOrganizationMemberRole", diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index 6cd8d99fd6..f3485477e3 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -4,13 +4,17 @@ import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.authentication.dto.OrganizationDomainCheckResult; import org.lowcoder.api.authentication.service.AuthenticationApiService; +import org.lowcoder.api.framework.view.PageResponseView; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.home.UserHomeApiService; +import org.lowcoder.api.usermanagement.view.OrgView; import org.lowcoder.api.usermanagement.view.UpdateUserRequest; import org.lowcoder.api.usermanagement.view.UserProfileView; import org.lowcoder.domain.organization.model.MemberRole; +import org.lowcoder.domain.organization.model.OrgMember; import org.lowcoder.domain.organization.service.OrgMemberService; +import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.domain.user.constant.UserStatusType; import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.model.UserDetail; @@ -23,8 +27,14 @@ import org.springframework.http.codec.multipart.Part; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ServerWebExchange; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; + +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + import static org.lowcoder.sdk.exception.BizError.INVALID_USER_STATUS; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -41,6 +51,7 @@ public class UserController implements UserEndpoints private final CommonConfig commonConfig; private final AuthenticationApiService authenticationApiService; private final OrgMemberService orgMemberService; + private final OrganizationService organizationService; @Override public Mono> createUserAndAddToOrg(@PathVariable String orgId, CreateUserRequest request) { @@ -62,6 +73,26 @@ public Mono> getUserProfile(ServerWebExchange exchange) { .switchIfEmpty(Mono.just(ResponseView.success(view)))); } + @Override + public Mono> getUserOrgs(ServerWebExchange exchange, + @RequestParam(required = false) String orgName, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "10") Integer pageSize) { + return sessionUserService.getVisitor() + .flatMap(user -> { + Pageable pageable = PageRequest.of(pageNum - 1, pageSize); + String filter = orgName == null ? "" : orgName; + return organizationService.findUserOrgs(user.getId(), filter, pageable) + .map(OrgView::new) + .collectList() + .zipWith(organizationService.countUserOrgs(user.getId(), filter)) + .map(tuple -> PageResponseView.success( + tuple.getT1(), pageNum, pageSize, tuple.getT2().intValue() + )); + }) + .map(ResponseView::success); + } + @Override public Mono> newUserGuidanceShown() { return sessionUserService.getVisitorId() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index 2de3af919f..955bb70bfa 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -48,6 +48,20 @@ public interface UserEndpoints @GetMapping("/me") public Mono> getUserProfile(ServerWebExchange exchange); + @Operation( + tags = {TAG_USER_MANAGEMENT}, + operationId = "getUserOrgs", + summary = "Get User Organizations", + description = "Retrieve a paginated list of organizations for the current user, filtered by organization name if provided." + ) + @GetMapping("/myorg") + public Mono> getUserOrgs( + ServerWebExchange exchange, + @RequestParam(required = false) String orgName, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "10") Integer pageSize + ); + @Operation( tags = TAG_USER_MANAGEMENT, operationId = "newUserGuidanceShown", @@ -218,5 +232,4 @@ public record MarkUserStatusRequest(String type, Object value) { public record CreateUserRequest(String email, String password) { } - } diff --git a/server/api-service/lowcoder-server/src/main/resources/application.yaml b/server/api-service/lowcoder-server/src/main/resources/application.yaml index 254eca6e87..2cb8c22f93 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application.yaml @@ -64,7 +64,7 @@ common: domain: default-value: lowcoder.org cloud: false - version: 2.7.0 + version: 2.7.2 apiVersion: 1.2 block-hound-enable: false encrypt: diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 50e8c157ff..972c236198 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -12,7 +12,7 @@ - 2.7.0 + 2.7.2 17 ${java.version} ${java.version} diff --git a/server/node-service/package.json b/server/node-service/package.json index 50ba90832e..db8736ac0b 100644 --- a/server/node-service/package.json +++ b/server/node-service/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-node-server", - "version": "2.7.0", + "version": "2.7.2", "private": true, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/translations/locales/en.js b/translations/locales/en.js index 0628515d6b..5b69d23f0b 100644 --- a/translations/locales/en.js +++ b/translations/locales/en.js @@ -589,6 +589,7 @@ export const en = { "chartBorderColor": "Border Color", "chartTextColor": "Text Color", "detailSize": "Detail Size", + "hideColumn": "Hide Column", "radiusTip": "Specifies the radius of the element's corners. Example: 5px, 50%, or 1em.", "gapTip": "Specifies the gap between rows and columns in a grid or flex container. Example: 10px, 1rem, or 5%.",