From f84b565f20191797f50a9546d1b31dba46a49391 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Tue, 3 Jun 2025 19:50:01 +0500 Subject: [PATCH 01/15] Added support for grids similar to rjsf --- .../jsonSchemaFormComp/JsonFormsRenderer.tsx | 71 +++++++++++-------- .../jsonSchemaFormComp/jsonSchemaFormComp.tsx | 7 -- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/JsonFormsRenderer.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/JsonFormsRenderer.tsx index cbdab8a0f..bc1eaaf65 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 f0b83c4f5..0705a745b 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.", - // }) - // ) } )} From 6a3ce06a3005545345a959ee556947df25ed3a95 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 3 Jun 2025 22:35:45 +0500 Subject: [PATCH 02/15] [Feat]: #1585 Add handlers for ColumnTypes like buttons, select, links and dropdown --- .../columnTypeComps/columnDropdownComp.tsx | 16 ++++-- .../column/columnTypeComps/columnLinkComp.tsx | 20 +++---- .../columnTypeComps/columnLinksComp.tsx | 51 +++++++++++------- .../columnTypeComps/columnSelectComp.tsx | 52 +++++++++++++++++-- .../column/simpleColumnTypeComps.tsx | 14 ++--- 5 files changed, 108 insertions(+), 45 deletions(-) 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 d71ad03cb..9055413de 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,7 @@ 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"; const StyledButton = styled(Button100)` display: flex; @@ -28,18 +29,21 @@ 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); // Cleanup on unmount @@ -54,7 +58,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 + onEvent?.("click"); + }, [items, options, onEvent]); const handleMouseDown = useCallback((e: React.MouseEvent) => { e.stopPropagation(); @@ -78,6 +84,7 @@ const DropdownView = React.memo((props: { prefixIcon: ReactNode; suffixIcon: ReactNode; options: any[]; + onEvent?: (eventName: string) => void; }) => { const mountedRef = useRef(true); @@ -120,8 +127,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/columnLinkComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx index c82b7326a..512329ee3 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 @@ -11,12 +11,15 @@ 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 } from "comps/controls/eventHandlerControl"; export const ColumnValueTooltip = trans("table.columnValueTooltip"); +const LinkEventOptions = [clickEvent] as const; + const childrenMap = { text: StringControl, - onClick: ActionSelectorControlInContext, + onEvent: eventHandlerControl(LinkEventOptions), disabled: BoolCodeControl, style: styleControl(TableColumnLinkStyle), }; @@ -34,12 +37,12 @@ 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, onEvent }: { disabled: boolean; label: string; onEvent?: (eventName: string) => void }) => { const handleClick = useCallback(() => { - if (!disabled && onClick) { - onClick(); + if (!disabled && onEvent) { + onEvent("click"); } - }, [disabled, onClick]); + }, [disabled, onEvent]); return ( { const value = props.changeValue ?? getBaseValue(props, dispatch); - return ; + return ; }, (nodeValue) => nodeValue.text.value, getBaseValue @@ -125,10 +128,7 @@ export const LinkComp = (function () { tooltip: ColumnValueTooltip, })} {disabledPropertyView(children)} - {children.onClick.propertyView({ - label: trans("table.action"), - placement: "table", - })} + {children.onEvent.propertyView()} )) .setStylePropertyViewFn((children) => ( 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 4ecd308dd..b36f2acfc 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 @@ -10,6 +10,7 @@ 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 } from "comps/controls/eventHandlerControl"; const MenuLinkWrapper = styled.div` > a { @@ -37,33 +38,16 @@ const MenuWrapper = styled.div` } `; -// 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 ( - - - - ); -}); - -MenuItem.displayName = 'MenuItem'; +const LinksEventOptions = [clickEvent] as const; +// Update OptionItem to include event handlers const OptionItem = new MultiCompBuilder( { label: StringControl, onClick: ActionSelectorControlInContext, hidden: BoolCodeControl, disabled: BoolCodeControl, + onEvent: eventHandlerControl(LinksEventOptions), }, (props) => { return props; @@ -79,11 +63,38 @@ const OptionItem = new MultiCompBuilder( })} {hiddenPropertyView(children)} {disabledPropertyView(children)} + {children.onEvent.propertyView()} ); }) .build(); +// Memoized menu item component +const MenuItem = React.memo(({ option, index }: { option: any; index: number }) => { + const handleClick = useCallback(() => { + if (!option.disabled) { + if (option.onClick) { + option.onClick(); + } + if (option.onEvent) { + option.onEvent("click"); + } + } + }, [option.disabled, option.onClick, option.onEvent]); + + return ( + + + + ); +}); + +MenuItem.displayName = 'MenuItem'; + // Memoized menu component const LinksMenu = React.memo(({ options }: { options: any[] }) => { const mountedRef = useRef(true); 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 de76a4dd8..6162abea7 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,17 @@ 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 } from "comps/controls/eventHandlerControl"; const Wrapper = styled.div` display: inline-flex; @@ -75,9 +79,43 @@ const Wrapper = styled.div` } `; +const SelectOptionEventOptions = [clickEvent] 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, }; const getBaseValue: ColumnTypeViewFn = (props) => props.text; @@ -106,7 +144,13 @@ 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 && selectedOption.onEvent) { + selectedOption.onEvent("click"); + } + }, [props.onChange, props.options]); const handleEvent = useCallback(async (eventName: string) => { if (!mountedRef.current) return [] as unknown[]; 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 3d5096cc8..ba264c5e4 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx @@ -13,6 +13,7 @@ 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 } from "comps/controls/eventHandlerControl"; export const ColumnValueTooltip = trans("table.columnValueTooltip"); @@ -31,10 +32,12 @@ export const ButtonTypeOptions = [ }, ] as const; +const ButtonEventOptions = [clickEvent] as const; + const childrenMap = { text: StringControl, buttonType: dropdownControl(ButtonTypeOptions, "primary"), - onClick: ActionSelectorControlInContext, + onEvent: eventHandlerControl(ButtonEventOptions), loading: BoolCodeControl, disabled: BoolCodeControl, prefixIcon: IconControl, @@ -49,8 +52,8 @@ const ButtonStyled = React.memo(({ props }: { props: ToViewReturn { - props.onClick?.(); - }, [props.onClick]); + props.onEvent("click"); + }, [props.onEvent]); const buttonStyle = useMemo(() => ({ margin: 0, @@ -100,10 +103,7 @@ export const ButtonComp = (function () { })} {loadingPropertyView(children)} {disabledPropertyView(children)} - {children.onClick.propertyView({ - label: trans("table.action"), - placement: "table", - })} + {children.onEvent.propertyView()} )) .build(); From 70eddf10f2b1862412963bb870e5ba8f237efecc Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Wed, 4 Jun 2025 00:26:08 +0500 Subject: [PATCH 03/15] Added double click to the table event hanlders --- .../src/comps/comps/tableComp/selectionControl.tsx | 10 ++++++++++ .../lowcoder/src/comps/comps/tableComp/tableTypes.tsx | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx index 60e292d0d..80e8e0340 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx @@ -74,6 +74,12 @@ export const SelectionControl = (function () { onEvent("rowSelectChange"); } }, + onDoubleClick: () => { + onEvent("doubleClick"); + if (getKey(record) !== props.selectedRowKey) { + onEvent("rowSelectChange"); + } + } }; }, }; @@ -101,6 +107,10 @@ export const SelectionControl = (function () { changeSelectedRowKey(record); onEvent("rowClick"); }, + onDoubleClick: () => { + changeSelectedRowKey(record); + onEvent("doubleClick"); + } }; }, }; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx index 47db799b8..f40f18c73 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']; From 780ca306c15388a34b7b3253990e612ac36bfa27 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 4 Jun 2025 15:33:18 +0500 Subject: [PATCH 04/15] fixed input state change not updating temporary state value --- .../textInputComp/textInputConstants.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index c2ab8801b..fc25e03e7 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 Date: Wed, 4 Jun 2025 23:20:33 +0500 Subject: [PATCH 05/15] [Fix]: Workspace switch submenu overflow for Mobile Screens --- client/packages/lowcoder/src/pages/common/profileDropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index cf9e6d792..4f083cc18 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', From f23c3099da6d13ba2df63328f513d4a043615d19 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Thu, 5 Jun 2025 01:46:17 +0500 Subject: [PATCH 06/15] Added hide toggle for columns in responsive layout --- .../src/comps/comps/responsiveLayout/responsiveLayout.tsx | 2 +- .../packages/lowcoder/src/comps/controls/optionsControl.tsx | 4 ++++ translations/locales/en.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx index 0ce837560..c9c8229ed 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/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index e32c32c09..4ddb7463d 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -588,6 +588,7 @@ const ColumnOption = new MultiCompBuilder( radius: withDefault(RadiusControl, ""), margin: withDefault(StringControl, ""), padding: withDefault(StringControl, ""), + hidden: withDefault(BoolControl, false), }, (props) => props ) @@ -624,6 +625,9 @@ const ColumnOption = new MultiCompBuilder( preInputNode: , placeholder: '3px', })} + {children.hidden.propertyView({ + label: trans('style.hideColumn'), + })} )) .build(); diff --git a/translations/locales/en.js b/translations/locales/en.js index 0628515d6..5b69d23f0 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%.", From a621bdf54bd40805843014bac06ce160cc2a0997 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Thu, 5 Jun 2025 16:30:04 +0500 Subject: [PATCH 07/15] clear column type render comp to fix editing issue --- .../tableComp/column/columnTypeComps/simpleTextComp.tsx | 7 ++++--- .../src/comps/comps/tableComp/column/tableColumnComp.tsx | 2 ++ .../lowcoder/src/comps/comps/tableComp/tableComp.tsx | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) 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 36d1d7ce9..a34537987 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 @@ -55,9 +55,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(() => ( 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 b3dbe77c9..7866cb813 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx @@ -434,6 +434,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/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index fee2da523..721f64565 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 From 9187921addd3a39d437c7dee6146679cce80e2a8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 5 Jun 2025 22:46:28 +0500 Subject: [PATCH 08/15] [Feat]: Add default, custom preset for tags and margins etc --- .../column/columnTypeComps/columnTagsComp.tsx | 98 ++++++++++++++++--- .../src/comps/controls/optionsControl.tsx | 70 ++++++++++++- 2 files changed, 154 insertions(+), 14 deletions(-) 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 0f4f1e15f..b7092b67b 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 @@ -58,10 +58,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[]) { @@ -286,13 +334,32 @@ const TagEdit = React.memo((props: TagEditPropsType) => { {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 ( + + {item} + + ); + }) ) : ( - + {value} )} @@ -316,9 +383,18 @@ export const ColumnTagsComp = (function () { 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 (
- + {tagText}
diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index e32c32c09..a0c58f121 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -41,6 +41,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"), @@ -729,24 +749,68 @@ 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, ""), }, (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', + })} ); } }; 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 From a00e63495a24415b50640cd9ff620af5369fe7a8 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 6 Jun 2025 01:23:11 +0500 Subject: [PATCH 09/15] fixed modal z-index after optimisations --- .../src/components/Modal/handler.tsx | 9 ++++----- .../lowcoder/src/comps/hooks/modalComp.tsx | 15 ++++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/Modal/handler.tsx b/client/packages/lowcoder-design/src/components/Modal/handler.tsx index c51a6858f..2293236d6 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/src/comps/hooks/modalComp.tsx b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx index 2977ad4b9..5c98ddb89 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 > Date: Fri, 6 Jun 2025 01:26:34 +0500 Subject: [PATCH 10/15] Updated Boolean toggle with Boolean Code Control --- .../packages/lowcoder/src/comps/controls/optionsControl.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index 4ddb7463d..9e927331d 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"; @@ -588,7 +589,7 @@ const ColumnOption = new MultiCompBuilder( radius: withDefault(RadiusControl, ""), margin: withDefault(StringControl, ""), padding: withDefault(StringControl, ""), - hidden: withDefault(BoolControl, false), + hidden: withDefault(BoolCodeControl, false), }, (props) => props ) @@ -627,6 +628,7 @@ const ColumnOption = new MultiCompBuilder( })} {children.hidden.propertyView({ label: trans('style.hideColumn'), + preInputNode: })} )) From 1006e08e76744958f8214b50288c8319de855334 Mon Sep 17 00:00:00 2001 From: Thomasr Date: Fri, 6 Jun 2025 12:35:11 -0400 Subject: [PATCH 11/15] add endpoint "user/myorg" --- .../api/home/UserHomeApiServiceImpl.java | 21 ----------- .../api/usermanagement/UserController.java | 36 +++++++++++++++++++ .../api/usermanagement/UserEndpoints.java | 15 +++++++- 3 files changed, 50 insertions(+), 22 deletions(-) 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 ee02ce0ab..d8520a9d5 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/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index 6cd8d99fd..3592f0a86 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,6 +27,7 @@ import org.springframework.http.codec.multipart.Part; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static org.lowcoder.sdk.exception.BizError.INVALID_USER_STATUS; @@ -41,6 +46,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 +68,36 @@ 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 -> { + // Get all active organizations for the user + Flux orgMemberFlux = orgMemberService.getAllActiveOrgs(user.getId()); + + // If orgName filter is provided, filter organizations by name + if (StringUtils.isNotBlank(orgName)) { + return orgMemberFlux + .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) + .filter(org -> StringUtils.containsIgnoreCase(org.getName(), orgName)) + .map(OrgView::new) + .collectList() + .map(orgs -> PageResponseView.success(orgs, pageNum, pageSize, orgs.size())); + } + + // If no filter, return all organizations + return orgMemberFlux + .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) + .map(OrgView::new) + .collectList() + .map(orgs -> PageResponseView.success(orgs, pageNum, pageSize, orgs.size())); + }) + .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 2de3af919..955bb70bf 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) { } - } From 7339ba1b1d8bb6ac5258e5f7f1e258c9368a9542 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 6 Jun 2025 22:11:35 +0500 Subject: [PATCH 12/15] added control in autoComplete comp to filter options by input value --- .../autoCompleteComp/autoCompleteComp.tsx | 283 ++++++++++-------- .../packages/lowcoder/src/i18n/locales/en.ts | 1 + 2 files changed, 159 insertions(+), 125 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index a8211777d..f7e2eaa67 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"; @@ -78,6 +78,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, @@ -118,10 +119,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 +133,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 +164,113 @@ 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"); + }, [props.valueInItems, getTextInputValidate, props.value, props.onEvent]); + + const handleSelect = useCallback((data: string, option: any) => { + setsearchtext(option[valueOrLabel]); + props.valueInItems.onChange(true); + props.value.onChange(option[valueOrLabel]); + props.onEvent("submit"); + }, [valueOrLabel, props.valueInItems, props.value, props.onEvent]); + + 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 +280,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} > - + ), @@ -306,24 +330,33 @@ let AutoCompleteCompBase = (function () { 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.valueOrLabel.propertyView({ - label: trans('autoComplete.checkedValueFrom'), - radioButton: true, - })} + {children.filterOptionsByInput.getView() && ( + children.ignoreCase.propertyView({ + label: trans('autoComplete.ignoreCase'), + }) + )} + {children.filterOptionsByInput.getView() && ( + children.valueOrLabel.propertyView({ + label: trans('autoComplete.checkedValueFrom'), + radioButton: true, + }) + )} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index a88b9debd..c379004bc 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -4475,6 +4475,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", From 3637da2bf918e5419d62071e78ae6f24fd474338 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 6 Jun 2025 22:25:32 +0500 Subject: [PATCH 13/15] show/hide summary row's column based on dynamic columns settings in table comp --- .../src/comps/comps/tableComp/tableCompView.tsx | 2 ++ .../src/comps/comps/tableComp/tableSummaryComp.tsx | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 7d5d26e13..fa53d2f50 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 58a7871aa..bd7a5d0ff 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) { + 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(); From 9752906febb8936fb55c9980ed46dd9db3389d5c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 6 Jun 2025 22:38:22 +0500 Subject: [PATCH 14/15] [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()} ); } From 70e6b11b2a63a7a73a521b86c9d8635507756100 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Mon, 9 Jun 2025 18:46:36 +0200 Subject: [PATCH 15/15] new: add mongo-express to all-in-one image for accessing of internal mongodb --- deploy/docker/Dockerfile | 8 +++- .../all-in-one/conf/nginx_mongo-ui.conf | 9 +++++ deploy/docker/all-in-one/entrypoint.sh | 16 +++++--- .../conf-available/15-mongo-express.conf | 18 +++++++++ .../all-in-one/scripts/mongo-ui-entrypoint.sh | 39 +++++++++++++++++++ deploy/docker/default.env | 2 + deploy/docker/frontend/server.conf | 2 + 7 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 deploy/docker/all-in-one/conf/nginx_mongo-ui.conf create mode 100644 deploy/docker/all-in-one/etc/supervisord/conf-available/15-mongo-express.conf create mode 100755 deploy/docker/all-in-one/scripts/mongo-ui-entrypoint.sh diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 5ecbbd579..9b2044241 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -239,6 +239,7 @@ COPY deploy/docker/frontend/nginx-http.conf /etc/nginx/nginx-http.conf COPY deploy/docker/frontend/nginx-https.conf /etc/nginx/nginx-https.conf COPY deploy/docker/frontend/ssl-certificate.conf /etc/nginx/ssl-certificate.conf COPY deploy/docker/frontend/ssl-params.conf /etc/nginx/ssl-params.conf +COPY deploy/docker/all-in-one/conf/nginx_mongo-ui.conf /etc/nginx/mongo-ui.conf # Add lowcoder frontend @@ -259,6 +260,11 @@ RUN mkdir -p /lowcoder/plugins/ && chown lowcoder:lowcoder /lowcoder/plugins/ # Add lowcoder node-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-node-service /lowcoder/node-service /lowcoder/node-service +# Add mongo-express for managing mongodb database +RUN mkdir -p /lowcoder/mongo-express && chown lowcoder:lowcoder /lowcoder/mongo-express +COPY --chown=lowcoder:lowcoder --from=mongo-express:1-20-alpine3.19 /app /lowcoder/mongo-express/app +COPY --chown=lowcoder:lowcoder --chmod=0755 deploy/docker/all-in-one/scripts/mongo-ui-entrypoint.sh /lowcoder/mongo-express/mongo-ui-entrypoint.sh + # Add services configuration COPY --chown=lowcoder:lowcoder deploy/docker/all-in-one/etc /lowcoder/etc @@ -276,9 +282,9 @@ RUN echo \ && chown -R lowcoder:root /var/log \ && chmod -R g+rw /run /etc/nginx /var/cache/nginx /var/log -EXPOSE 27017 EXPOSE 3000 EXPOSE 3443 +EXPOSE 9999 ENTRYPOINT [ "/bin/sh" , "/lowcoder/entrypoint.sh" ] CMD ["/usr/bin/supervisord", "-n" , "-c" , "/lowcoder/etc/supervisord.conf"] diff --git a/deploy/docker/all-in-one/conf/nginx_mongo-ui.conf b/deploy/docker/all-in-one/conf/nginx_mongo-ui.conf new file mode 100644 index 000000000..a15954d90 --- /dev/null +++ b/deploy/docker/all-in-one/conf/nginx_mongo-ui.conf @@ -0,0 +1,9 @@ + + location /mongo-ui { + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://127.0.0.1:9999/mongo-ui; + } + diff --git a/deploy/docker/all-in-one/entrypoint.sh b/deploy/docker/all-in-one/entrypoint.sh index 74403a08d..727e9967f 100644 --- a/deploy/docker/all-in-one/entrypoint.sh +++ b/deploy/docker/all-in-one/entrypoint.sh @@ -18,12 +18,7 @@ if [ ! "$(id --group lowcoder)" -eq ${GROUP_ID} ]; then fi; # Update host on which mongo is supposed to listen -# If LOWCODER_MONGODB_EXPOSED is true, it will listen on all interfaces -if [ "${LOWCODER_MONGODB_EXPOSED}" = "true" ]; then - export MONGO_LISTEN_HOST="0.0.0.0" -else - export MONGO_LISTEN_HOST="127.0.0.1" -fi; +export MONGO_LISTEN_HOST="127.0.0.1" # Set the default mongodb connection string if not set explicitly if [ -z "${LOWCODER_MONGODB_URL}" ]; then @@ -36,6 +31,7 @@ CERT="/lowcoder-stacks/ssl" # Create folder for holding application logs and data mkdir -p ${LOGS}/redis \ ${LOGS}/mongodb \ + ${LOGS}/mongo-ui \ ${LOGS}/api-service \ ${LOGS}/node-service \ ${LOGS}/frontend \ @@ -78,9 +74,17 @@ if [ "${LOWCODER_NODE_SERVICE_ENABLED:=true}" = "true" ]; then ln ${SUPERVISOR_AVAILABLE}/11-node-service.conf ${SUPERVISOR_ENABLED}/11-node-service.conf fi; +# Enable mongo-express web UI for mongodb if configured to run +if [ "${LOWCODER_MONGODB_EXPOSED:=false}" = "true" ]; then + ln ${SUPERVISOR_AVAILABLE}/15-mongo-express.conf ${SUPERVISOR_ENABLED}/15-mongo-express.conf +fi; + # Enable frontend if configured to run if [ "${LOWCODER_FRONTEND_ENABLED:=true}" = "true" ]; then ln ${SUPERVISOR_AVAILABLE}/20-frontend.conf ${SUPERVISOR_ENABLED}/20-frontend.conf + sed -i 's@# include mongo-ui.conf;@include mongo-ui.conf;@' /etc/nginx/server.conf +else + sed -i 's@\(\s*\)\(include mongo-ui\.conf;\)@\1# \2@' /etc/nginx/server.conf fi; # disable user directive if image is running non-root (Openshift) diff --git a/deploy/docker/all-in-one/etc/supervisord/conf-available/15-mongo-express.conf b/deploy/docker/all-in-one/etc/supervisord/conf-available/15-mongo-express.conf new file mode 100644 index 000000000..f4fae87fe --- /dev/null +++ b/deploy/docker/all-in-one/etc/supervisord/conf-available/15-mongo-express.conf @@ -0,0 +1,18 @@ +[program:mongo-ui] +# privileges will be dropped in entrypoint +user=root +directory=/lowcoder/mongo-express +command=/lowcoder/mongo-express/mongo-ui-entrypoint.sh +priority=15 +autostart=true +autorestart=true +startsecs=10 +startretries=3 +stdout_logfile=/lowcoder-stacks/logs/%(program_name)s/%(program_name)s.log +redirect_stderr=true +stdout_logfile_maxbytes=10MB +stderr_logfile_maxbytes=10MB +stdout_logfile_backups=5 +stderr_logfile_backups=5 +stdout_events_enabled=true +stderr_events_enabled=true diff --git a/deploy/docker/all-in-one/scripts/mongo-ui-entrypoint.sh b/deploy/docker/all-in-one/scripts/mongo-ui-entrypoint.sh new file mode 100755 index 000000000..af50b6ad9 --- /dev/null +++ b/deploy/docker/all-in-one/scripts/mongo-ui-entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e + +export USER_ID=${LOWCODER_PUID:=9001} +export GROUP_ID=${LOWCODER_PGID:=9001} + +# Mongo express configuration +export ME_CONFIG_MONGODB_URL="${LOWCODER_MONGODB_URL:=mongodb://localhost:27017/lowcoder?authSource=admin}" +export ME_CONFIG_MONGODB_ENABLE_ADMIN=true +export ME_CONFIG_SITE_BASEURL="/mongo-ui" +export ME_CONFIG_REQUEST_SIZE=200mb +export ME_CONFIG_OPTIONS_NO_DELETE=true +export ME_CONFIG_OPTIONS_EDITORTHEME=rubyblue +export ME_CONFIG_SITE_GRIDFS_ENABLED=true +export ME_CONFIG_OPTIONS_FULLWIDTH_LAYOUT=false +export ME_CONFIG_BASICAUTH=${LOWCODER_MONGODB_EXPOSED:false} +export ME_CONFIG_BASICAUTH_USERNAME="${LOWCODER_MONGOUI_USERNAME}" +export ME_CONFIG_BASICAUTH_PASSWORD="${LOWCODER_MONGOUI_PASSWORD}" +export VCAP_APP_HOST="127.0.0.1" +export PORT="9999" + +export ME_CONFIG_SITE_COOKIESECRET=`cat /proc/sys/kernel/random/uuid | sed 's/[-]//g' | head -c 30` +export ME_CONFIG_SITE_SESSIONSECRET=`cat /proc/sys/kernel/random/uuid | sed 's/[-]//g' | head -c 30` + +cd /lowcoder/mongo-express/app + +echo +echo "Running Mongodb UI with:" +if [ "$(id -u)" -eq 0 ]; then + # only use su if its possible, suppress for containers running non-root + echo " user id: ${USER_ID}" + echo " group id: ${GROUP_ID}" + GOSU="gosu ${USER_ID}:${GROUP_ID}" +fi +echo + +exec $GOSU node app + diff --git a/deploy/docker/default.env b/deploy/docker/default.env index 8b4445a3d..4349ddee9 100644 --- a/deploy/docker/default.env +++ b/deploy/docker/default.env @@ -33,6 +33,8 @@ LOWCODER_FRONTEND_ENABLED="true" # (applies to all-in-one deployment) # LOWCODER_MONGODB_EXPOSED="false" +LOWCODER_MONGOUI_USERNAME="admin" +LOWCODER_MONGOUI_PASSWORD="s3cr3t!" ## ## Generic parameters diff --git a/deploy/docker/frontend/server.conf b/deploy/docker/frontend/server.conf index b06816298..a1a65170f 100644 --- a/deploy/docker/frontend/server.conf +++ b/deploy/docker/frontend/server.conf @@ -56,3 +56,5 @@ proxy_pass __LOWCODER_NODE_SERVICE_URL__; } + # include mongo-ui.conf; +