From 9752906febb8936fb55c9980ed46dd9db3389d5c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 6 Jun 2025 22:38:22 +0500 Subject: [PATCH] [Feat]: Add event handlers on more column types --- .../columnTypeComps/ColumnNumberComp.tsx | 23 ++++++++++- .../columnTypeComps/columnAvatarsComp.tsx | 21 ++++++++-- .../column/columnTypeComps/columnImgComp.tsx | 24 ++++++++++-- .../columnTypeComps/columnLinksComp.tsx | 18 ++++++--- .../columnTypeComps/columnMarkdownComp.tsx | 18 +++++++-- .../columnTypeComps/columnSelectComp.tsx | 11 +++++- .../column/columnTypeComps/columnTagsComp.tsx | 35 ++++++++++++++++- .../column/columnTypeComps/simpleTextComp.tsx | 39 +++++++++++++++---- .../src/comps/controls/optionsControl.tsx | 2 + 9 files changed, 163 insertions(+), 28 deletions(-) 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 f221b547d..78bba9380 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,7 @@ import { withDefault } from "comps/generators"; import styled from "styled-components"; import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; const InputNumberWrapper = styled.div` .ant-input-number { @@ -25,6 +26,15 @@ const InputNumberWrapper = styled.div` } `; +const NumberViewWrapper = styled.div` + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; +`; + +const NumberEventOptions = [clickEvent] as const; + const childrenMap = { text: NumberControl, step: withDefault(NumberControl, 1), @@ -34,6 +44,7 @@ const childrenMap = { prefixIcon: IconControl, suffixIcon: IconControl, suffix: StringControl, + onEvent: eventHandlerControl(NumberEventOptions), }; const getBaseValue: ColumnTypeViewFn = (props) => props.text; @@ -46,6 +57,7 @@ type NumberViewProps = { suffixIcon: ReactNode; float: boolean; precision: number; + onEvent?: (eventName: string) => void; }; type NumberEditProps = { @@ -66,8 +78,14 @@ const ColumnNumberView = React.memo((props: NumberViewProps) => { return result; }, [props.value, props.float, props.precision]); + const handleClick = useCallback(() => { + if (props.onEvent) { + props.onEvent("click"); + } + }, [props.onEvent]); + return ( - <> + {hasIcon(props.prefixIcon) && ( {props.prefixIcon} )} @@ -75,7 +93,7 @@ const ColumnNumberView = React.memo((props: NumberViewProps) => { {hasIcon(props.suffixIcon) && ( {props.suffixIcon} )} - + ); }); @@ -197,6 +215,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 c34b6dfbb..a62704ff6 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 @@ -38,6 +38,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 +48,7 @@ const DropdownOption = new MultiCompBuilder( color: ColorControl, backgroundColor: ColorControl, Tooltip: StringControl, + onEvent: eventHandlerControl(AvatarEventOptions), }, (props) => props ) @@ -63,6 +66,7 @@ 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()} ); }) @@ -83,14 +87,16 @@ 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); @@ -103,8 +109,15 @@ const MemoizedAvatar = React.memo(({ const handleClick = useCallback(() => { if (!mountedRef.current) return; + + // Trigger individual avatar event first + if (onItemEvent) { + onItemEvent("click"); + } + + // Then trigger main component event onEvent("click"); - }, [onEvent]); + }, [onEvent, onItemEvent]); return ( @@ -114,6 +127,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 +176,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/columnImgComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnImgComp.tsx index b062f8fc4..d3d204101 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/columnLinksComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx index b36f2acfc..3d35aa31d 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 @@ -70,7 +70,7 @@ const OptionItem = new MultiCompBuilder( .build(); // Memoized menu item component -const MenuItem = React.memo(({ option, index }: { option: any; index: number }) => { +const MenuItem = React.memo(({ option, index, onMainEvent }: { option: any; index: number; onMainEvent?: (eventName: string) => void }) => { const handleClick = useCallback(() => { if (!option.disabled) { if (option.onClick) { @@ -79,8 +79,12 @@ const MenuItem = React.memo(({ option, index }: { option: any; index: number }) if (option.onEvent) { option.onEvent("click"); } + // Trigger the main component's event handler + if (onMainEvent) { + onMainEvent("click"); + } } - }, [option.disabled, option.onClick, option.onEvent]); + }, [option.disabled, option.onClick, option.onEvent, onMainEvent]); return ( @@ -96,7 +100,7 @@ const MenuItem = React.memo(({ option, index }: { option: any; index: number }) MenuItem.displayName = 'MenuItem'; // Memoized menu component -const LinksMenu = React.memo(({ options }: { options: any[] }) => { +const LinksMenu = React.memo(({ options, onEvent }: { options: any[]; onEvent?: (eventName: string) => void }) => { const mountedRef = useRef(true); // Cleanup on unmount @@ -111,9 +115,9 @@ const LinksMenu = React.memo(({ options }: { options: any[] }) => { .filter((o) => !o.hidden) .map((option, index) => ({ key: index, - label: + label: })), - [options] + [options, onEvent] ); return ( @@ -130,11 +134,12 @@ export const ColumnLinksComp = (function () { options: manualOptionsControl(OptionItem, { initOptions: [{ label: trans("table.option1") }], }), + onEvent: eventHandlerControl(LinksEventOptions), }; return new ColumnTypeCompBuilder( childrenMap, (props) => { - return ; + return ; }, () => "" ) @@ -144,6 +149,7 @@ export const ColumnLinksComp = (function () { newOptionLabel: trans("table.option"), title: trans("table.optionList"), })} + {children.onEvent.propertyView()} )) .build(); 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 e8fcd9a4b..17ad78efd 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 6162abea7..ee15dda64 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 @@ -116,6 +116,7 @@ const SelectOptionWithEventsControl = optionsControl(SelectOptionWithEvents, { const childrenMap = { text: StringControl, options: SelectOptionWithEventsControl, + onEvent: eventHandlerControl(SelectOptionEventOptions), }; const getBaseValue: ColumnTypeViewFn = (props) => props.text; @@ -125,6 +126,7 @@ type SelectEditProps = { onChange: (value: string) => void; onChangeEnd: () => void; options: any[]; + onMainEvent?: (eventName: string) => void; }; const SelectEdit = React.memo((props: SelectEditProps) => { @@ -150,7 +152,12 @@ const SelectEdit = React.memo((props: SelectEditProps) => { if (selectedOption && selectedOption.onEvent) { selectedOption.onEvent("click"); } - }, [props.onChange, props.options]); + + // 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[]; @@ -203,6 +210,7 @@ export const ColumnSelectComp = (function () { options={props.otherProps?.options || []} onChange={props.onChange} onChangeEnd={props.onChangeEnd} + onMainEvent={props.otherProps?.onEvent} /> ) @@ -217,6 +225,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 b7092b67b..3bdbbed9d 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; @@ -120,6 +121,7 @@ function getTagIcon(tagText: any, tagOptions: any[]) { const childrenMap = { text: TagsControl, tagColors: ColoredTagOptionControl, + onEvent: eventHandlerControl([clickEvent]), }; const getBaseValue: ColumnTypeViewFn = ( @@ -229,6 +231,7 @@ export const DropdownStyled = styled.div` export const TagStyled = styled(Tag)` margin-right: 8px; + cursor: pointer; svg { margin-right: 4px; } @@ -298,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 ( { key={i} style={{ marginRight: tagStyle.margin ? undefined : "8px", + cursor: "pointer", ...tagStyle }} + onClick={(e) => handleTagClick(item, e)} > {item} @@ -358,7 +371,11 @@ const TagEdit = React.memo((props: TagEditPropsType) => { color={getTagColor(value, memoizedTagOptions)} icon={getTagIcon(value, memoizedTagOptions)} key={index} - style={getTagStyle(value, memoizedTagOptions)} + style={{ + cursor: "pointer", + ...getTagStyle(value, memoizedTagOptions) + }} + onClick={(e) => handleTagClick(value, e)} > {value} @@ -380,6 +397,18 @@ 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); @@ -394,6 +423,7 @@ export const ColumnTagsComp = (function () { icon={tagIcon} key={index} style={tagStyle} + onClick={() => handleTagClick(tagText)} > {tagText} @@ -425,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 a34537987..aba505252 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,23 @@ import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; import React, { useCallback, useMemo } from "react"; import { RecordConstructorToComp } from "lowcoder-core"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; +import styled from "styled-components"; + +const TextEventOptions = [clickEvent] 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 +39,7 @@ interface SimpleTextContentProps { value: string | number; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; + onEvent?: (eventName: string) => void; } interface SimpleTextEditViewProps { @@ -35,13 +48,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 handleClick = useCallback(() => { + if (onEvent) { + onEvent("click"); + } + }, [onEvent]); + + return ( + + {hasIcon(prefixIcon) && } + {value} + {hasIcon(suffixIcon) && } + + ); +}); const SimpleTextEditView = React.memo(({ value, onChange, onChangeEnd }: SimpleTextEditViewProps) => { const handleChange = useCallback((e: React.ChangeEvent) => { @@ -73,8 +94,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( @@ -86,6 +108,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/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index a0c58f121..0105c7685 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -760,6 +760,7 @@ let ColoredTagOption = new MultiCompBuilder( radius: withDefault(RadiusControl, ""), margin: withDefault(StringControl, ""), padding: withDefault(StringControl, ""), + onEvent: ButtonEventHandlerControl, }, (props) => props ).build(); @@ -802,6 +803,7 @@ ColoredTagOption = class extends ColoredTagOption implements OptionCompProperty preInputNode: , placeholder: '3px', })} + {this.children.onEvent.propertyView()} ); }