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/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/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.", - // }) - // ) } )} 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/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/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/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/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(); 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/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/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 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(); 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']; 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 props ) @@ -624,6 +646,10 @@ const ColumnOption = new MultiCompBuilder( preInputNode: , placeholder: '3px', })} + {children.hidden.propertyView({ + label: trans('style.hideColumn'), + preInputNode: + })} )) .build(); @@ -729,24 +755,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 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 > 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) { } - } 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%.",