From 624d349a44976ce1fa2ac8419e068677799b6685 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 May 2025 20:37:53 +0500 Subject: [PATCH 1/8] [FIX]: #1626 Column Layout Component Width Issue --- .../comps/comps/columnLayout/columnLayout.tsx | 49 +++++++------------ .../packages/lowcoder/src/i18n/locales/en.ts | 1 + 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index 7b9c80a24..37191e65e 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -46,17 +46,13 @@ import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const ContainWrapper = styled.div<{ $style: ContainerStyleType & { display: string, - gridTemplateColumns: string, - columnGap: string, - gridTemplateRows: string, - rowGap: string, + flexWrap: string, + gap: string, } | undefined; }>` display: ${(props) => props.$style?.display}; - grid-template-columns: ${(props) => props.$style?.gridTemplateColumns}; - grid-template-rows: ${(props) => props.$style?.gridTemplateRows}; - column-gap: ${(props) => props.$style?.columnGap}; - row-gap: ${(props) => props.$style?.rowGap}; + flex-wrap: ${(props) => props.$style?.flexWrap}; + gap: ${(props) => props.$style?.gap}; border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}; @@ -67,11 +63,14 @@ const ContainWrapper = styled.div<{ ${props => props.$style && getBackgroundStyle(props.$style)} `; -const ColWrapper = styled(Col)<{ +const ColWrapper = styled.div<{ $style: ResponsiveLayoutColStyleType | undefined, - $minWidth?: string, + $width?: string, $matchColumnsHeight: boolean, }>` + flex: ${props => props.$width ? "0 0 " + props.$width : "1 1 0"}; + min-width: 0; /* Prevent flex items from overflowing */ + > div { height: ${(props) => props.$matchColumnsHeight ? `calc(100% - ${props.$style?.padding || 0} - ${props.$style?.padding || 0})` : 'auto'}; border-radius: ${(props) => props.$style?.radius}; @@ -94,11 +93,8 @@ const childrenMap = { horizontalGridCells: SliderControl, autoHeight: AutoHeightControl, matchColumnsHeight: withDefault(BoolControl, true), - templateRows: withDefault(StringControl, "1fr"), - rowGap: withDefault(StringControl, "20px"), - templateColumns: withDefault(StringControl, "1fr 1fr"), + gap: withDefault(StringControl, "20px"), mainScrollbar: withDefault(BoolControl, false), - columnGap: withDefault(StringControl, "20px"), style: styleControl(ContainerStyle, 'style'), columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle') }; @@ -129,10 +125,7 @@ const ColumnLayout = (props: ColumnLayoutProps) => { containers, dispatch, matchColumnsHeight, - templateRows, - rowGap, - templateColumns, - columnGap, + gap, columnStyle, horizontalGridCells, mainScrollbar @@ -145,24 +138,21 @@ const ColumnLayout = (props: ColumnLayoutProps) => { {columns.map(column => { const id = String(column.id); const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); if(!containers[id]) return null const containerProps = containers[id].children; - const noOfColumns = columns.length; + return ( - + {trans("responsiveLayout.columnsSpacing")} ))} - {children.templateColumns.propertyView({label: trans("responsiveLayout.columnDefinition"), tooltip: trans("responsiveLayout.columnsDefinitionTooltip")})} - {children.templateRows.propertyView({label: trans("responsiveLayout.rowDefinition"), tooltip: trans("responsiveLayout.rowsDefinitionTooltip")})} - {children.columnGap.propertyView({label: trans("responsiveLayout.columnGap")})} - {children.rowGap.propertyView({label: trans("responsiveLayout.rowGap")})} + {children.gap.propertyView({label: trans("responsiveLayout.gap")})} )} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index d1eff10d7..d8236a42d 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3899,6 +3899,7 @@ export const en = { "rowDefinition": "Row Definition", "columnGap": "Column Gap", "rowGap": "Row Gap", + "gap": "Gap", "atLeastOneColumnError": "Responsive Layout Keeps at Least One Column", "columnsPerRow": "Columns per Row", "columnsSpacing": "Columns Spacing (px)", From e035ee2f092223df07cfbbdda750cb08bab5d64b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 5 May 2025 18:45:12 +0500 Subject: [PATCH 2/8] [Fix]: #1626 Add Toggle between Grid/Flex Layout --- .../comps/comps/columnLayout/columnLayout.tsx | 110 ++++++++++++++---- .../src/comps/controls/optionsControl.tsx | 10 +- .../packages/lowcoder/src/i18n/locales/en.ts | 5 +- 3 files changed, 98 insertions(+), 27 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index 37191e65e..cb8c16cc3 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -43,16 +43,34 @@ import { DisabledContext } from "comps/generators/uiCompBuilder"; import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; +// Extended ContainerStyleType for our specific needs +interface ExtendedContainerStyleType extends ContainerStyleType { + display?: string; + gridTemplateColumns?: string; + gridTemplateRows?: string; + columnGap?: string; + rowGap?: string; + [key: string]: string | undefined; +} + const ContainWrapper = styled.div<{ - $style: ContainerStyleType & { - display: string, - flexWrap: string, - gap: string, - } | undefined; + $style: ExtendedContainerStyleType | undefined; + $useFlexLayout: boolean; }>` - display: ${(props) => props.$style?.display}; - flex-wrap: ${(props) => props.$style?.flexWrap}; - gap: ${(props) => props.$style?.gap}; + display: ${(props) => props.$useFlexLayout ? 'flex' : props.$style?.display}; + flex-wrap: ${(props) => props.$useFlexLayout ? 'wrap' : 'nowrap'}; + + ${(props) => !props.$useFlexLayout && ` + grid-template-columns: ${props.$style?.gridTemplateColumns}; + grid-template-rows: ${props.$style?.gridTemplateRows}; + column-gap: ${props.$style?.columnGap}; + row-gap: ${props.$style?.rowGap}; + `} + + ${(props) => props.$useFlexLayout && ` + column-gap: ${props.$style?.columnGap || '0'}; + row-gap: ${props.$style?.rowGap || '0'}; + `} border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}; @@ -65,11 +83,14 @@ const ContainWrapper = styled.div<{ const ColWrapper = styled.div<{ $style: ResponsiveLayoutColStyleType | undefined, - $width?: string, + $width: string, $matchColumnsHeight: boolean, + $useFlexLayout: boolean, }>` - flex: ${props => props.$width ? "0 0 " + props.$width : "1 1 0"}; - min-width: 0; /* Prevent flex items from overflowing */ + ${props => props.$useFlexLayout ? ` + flex: ${props.$width === '100%' ? '1 0 100%' : `0 0 ${props.$width}`}; + max-width: ${props.$width}; + ` : ''} > div { height: ${(props) => props.$matchColumnsHeight ? `calc(100% - ${props.$style?.padding || 0} - ${props.$style?.padding || 0})` : 'auto'}; @@ -93,10 +114,14 @@ const childrenMap = { horizontalGridCells: SliderControl, autoHeight: AutoHeightControl, matchColumnsHeight: withDefault(BoolControl, true), - gap: withDefault(StringControl, "20px"), + templateRows: withDefault(StringControl, "1fr"), + rowGap: withDefault(StringControl, "0"), + templateColumns: withDefault(StringControl, "1fr 1fr"), mainScrollbar: withDefault(BoolControl, false), + columnGap: withDefault(StringControl, "0"), style: styleControl(ContainerStyle, 'style'), - columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle') + columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle'), + useFlexLayout: withDefault(BoolControl, true), }; type ViewProps = RecordConstructorToView; @@ -118,6 +143,19 @@ const ColumnContainer = (props: ColumnContainerProps) => { ); }; +const getColumnWidth = (column: any): string => { + // Use explicit width if available + if (column.width) { + // For percentage values, calculate precisely to accommodate gaps + if (column.width.endsWith('%')) { + return column.width; + } + return column.width; + } + + // If minWidth is set, use it or default to equal distribution + return column.minWidth || 'auto'; +}; const ColumnLayout = (props: ColumnLayoutProps) => { let { @@ -125,10 +163,14 @@ const ColumnLayout = (props: ColumnLayoutProps) => { containers, dispatch, matchColumnsHeight, - gap, + templateRows, + rowGap, + templateColumns, + columnGap, columnStyle, horizontalGridCells, - mainScrollbar + mainScrollbar, + useFlexLayout, } = props; return ( @@ -136,24 +178,31 @@ const ColumnLayout = (props: ColumnLayoutProps) => {
- + {columns.map(column => { const id = String(column.id); const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); if(!containers[id]) return null const containerProps = containers[id].children; + const columnWidth = getColumnWidth(column); return ( - +
{children.matchColumnsHeight.propertyView({ label: trans("responsiveLayout.matchColumnsHeight") @@ -217,7 +270,16 @@ export const ResponsiveLayoutBaseComp = (function () { {controlItem({}, (
{trans("responsiveLayout.columnsSpacing")}
))} - {children.gap.propertyView({label: trans("responsiveLayout.gap")})} + {!children.useFlexLayout.getView() && children.templateColumns.propertyView({ + label: trans("responsiveLayout.columnDefinition"), + tooltip: trans("responsiveLayout.columnsDefinitionTooltip") + })} + {!children.useFlexLayout.getView() && children.templateRows.propertyView({ + label: trans("responsiveLayout.rowDefinition"), + tooltip: trans("responsiveLayout.rowsDefinitionTooltip") + })} + {children.columnGap.propertyView({label: trans("responsiveLayout.columnGap")})} + {children.rowGap.propertyView({label: trans("responsiveLayout.rowGap")})}
)} diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index e32c32c09..d6fef99e3 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -582,6 +582,7 @@ const ColumnOption = new MultiCompBuilder( label: StringControl, key: StringControl, minWidth: withDefault(RadiusControl, ""), + width: withDefault(RadiusControl, ""), background: withDefault(ColorControl, ""), backgroundImage: withDefault(StringControl, ""), border: withDefault(ColorControl, ""), @@ -598,6 +599,11 @@ const ColumnOption = new MultiCompBuilder( preInputNode: , placeholder: '3px', })} + {children.width.propertyView({ + label: trans('responsiveLayout.width'), + preInputNode: , + placeholder: '50%', + })} {children.background.propertyView({ label: trans('style.background'), })} @@ -630,8 +636,8 @@ const ColumnOption = new MultiCompBuilder( export const ColumnOptionControl = manualOptionsControl(ColumnOption, { initOptions: [ - { id: 0, key: "Column1", label: "Column1" }, - { id: 1, key: "Column2", label: "Column2" }, + { id: 0, key: "Column1", label: "Column1", width: "50%" }, + { id: 1, key: "Column2", label: "Column2", width: "50%" }, ], uniqField: "key", autoIncField: "id", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index d8236a42d..ba248eea4 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3899,7 +3899,6 @@ export const en = { "rowDefinition": "Row Definition", "columnGap": "Column Gap", "rowGap": "Row Gap", - "gap": "Gap", "atLeastOneColumnError": "Responsive Layout Keeps at Least One Column", "columnsPerRow": "Columns per Row", "columnsSpacing": "Columns Spacing (px)", @@ -3911,6 +3910,8 @@ export const en = { "rowStyle": "Row Style", "columnStyle": "Column Style", "minWidth": "Min. Width", + "width": "Width", + "widthTooltip": "Set the column width (e.g., '300px', '50%', '100%'). When set to 100%, columns will stack vertically.", "rowBreak": "Row Break", "useComponentWidth" : "Use Self Size", "useComponentWidthDesc" : "Use the container width instead the App width", @@ -3919,6 +3920,8 @@ export const en = { "columnsLayout": "Columns Layout", "columnsDefinitionTooltip": "Columns can be defined freely based on the CSS columns properties. For example, 'auto auto' will create two columns with equal width. Read more here: https://css-tricks.com/almanac/properties/g/grid-template-columns", "rowsDefinitionTooltip": "Rows can be defined freely based on the CSS rows properties. For example, 'auto auto' will create two rows with equal height. Read more here: https://css-tricks.com/almanac/properties/g/grid-template-rows", + "useFlexLayout": "Use Flexible Layout", + "useFlexLayoutTooltip": "Enable responsive behavior where columns can wrap when there's not enough space" }, "splitLayout" : { "column": "View Areas", From 55157a8fe8a29ff14ce04e1802abe973508e5fbd Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 5 May 2025 19:18:43 +0500 Subject: [PATCH 3/8] [Fix]: #1626 Hide Width in Grid Mode --- .../comps/comps/columnLayout/columnLayout.tsx | 3 +- .../src/comps/controls/optionsControl.tsx | 99 ++++++++++--------- 2 files changed, 57 insertions(+), 45 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index cb8c16cc3..5f51f6bbb 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -121,7 +121,7 @@ const childrenMap = { columnGap: withDefault(StringControl, "0"), style: styleControl(ContainerStyle, 'style'), columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle'), - useFlexLayout: withDefault(BoolControl, true), + useFlexLayout: withDefault(BoolControl, false), }; type ViewProps = RecordConstructorToView; @@ -239,6 +239,7 @@ export const ResponsiveLayoutBaseComp = (function () { {children.columns.propertyView({ title: trans("responsiveLayout.column"), newOptionLabel: trans("responsiveLayout.addColumn"), + useFlexLayout: children.useFlexLayout.getView(), })} diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index d6fef99e3..95520cc4d 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -40,6 +40,7 @@ import { ControlItemCompBuilder } from "comps/generators/controlCompBuilder"; import { ColorControl } from "./colorControl"; import { StringStateControl } from "./codeStateControl"; import { reduceInContext } from "../utils/reduceContext"; +import React from "react"; const OptionTypes = [ { @@ -65,10 +66,13 @@ type OptionControlParam = { title?: string; // The new option's label name newOptionLabel?: string; + // Whether to use flex layout (for column options) + useFlexLayout?: boolean; }; type OptionPropertyParam = { autoMap?: boolean; + useFlexLayout?: boolean; }; interface OptionCompProperty { @@ -176,7 +180,7 @@ export function manualOptionsControl( itemTitle={(comp) => comp.children.label.getView()} popoverTitle={() => trans("edit")} content={(comp) => { - return hasPropertyView(comp) ? comp.propertyView({}) : comp.getPropertyView(); + return hasPropertyView(comp) ? comp.propertyView({ useFlexLayout: param.useFlexLayout }) : comp.getPropertyView(); }} items={manualComp.getView()} onAdd={() => { @@ -576,7 +580,7 @@ const StyledContent = styled.div` } `; -const ColumnOption = new MultiCompBuilder( +let ColumnOption = new MultiCompBuilder( { id: valueComp(-1), label: StringControl, @@ -591,48 +595,55 @@ const ColumnOption = new MultiCompBuilder( padding: withDefault(StringControl, ""), }, (props) => props -) -.setPropertyViewFn((children) => ( - - {children.minWidth.propertyView({ - label: trans('responsiveLayout.minWidth'), - preInputNode: , - placeholder: '3px', - })} - {children.width.propertyView({ - label: trans('responsiveLayout.width'), - preInputNode: , - placeholder: '50%', - })} - {children.background.propertyView({ - label: trans('style.background'), - })} - {children.backgroundImage.propertyView({ - label: `Background Image`, - // preInputNode: , - placeholder: 'https://temp.im/350x400', - })} - {children.border.propertyView({ - label: trans('style.border') - })} - {children.radius.propertyView({ - label: trans('style.borderRadius'), - preInputNode: , - placeholder: '3px', - })} - {children.margin.propertyView({ - label: trans('style.margin'), - preInputNode: , - placeholder: '3px', - })} - {children.padding.propertyView({ - label: trans('style.padding'), - preInputNode: , - placeholder: '3px', - })} - -)) - .build(); +).build(); + +// Add propertyView method through class extension +ColumnOption = class extends ColumnOption implements OptionCompProperty { + propertyView(param: OptionPropertyParam) { + const useFlexLayout = param?.useFlexLayout !== undefined ? param.useFlexLayout : true; + + return ( + + {useFlexLayout && this.children.minWidth.propertyView({ + label: trans('responsiveLayout.minWidth'), + preInputNode: , + placeholder: '3px', + })} + {useFlexLayout && this.children.width.propertyView({ + label: trans('responsiveLayout.width'), + preInputNode: , + placeholder: '50%', + })} + {this.children.background.propertyView({ + label: trans('style.background'), + })} + {this.children.backgroundImage.propertyView({ + label: `Background Image`, + // preInputNode: , + placeholder: 'https://temp.im/350x400', + })} + {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 ColumnOptionControl = manualOptionsControl(ColumnOption, { initOptions: [ From 112a7f138ff3cfa2ddac6fa7700e903436c685d8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 5 May 2025 23:07:36 +0500 Subject: [PATCH 4/8] [Fix]: #1626 fill available space issue --- .../comps/comps/columnLayout/columnLayout.tsx | 32 +++++++++---------- .../src/comps/controls/optionsControl.tsx | 4 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index 5f51f6bbb..d5b4f6f52 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -81,15 +81,27 @@ const ContainWrapper = styled.div<{ ${props => props.$style && getBackgroundStyle(props.$style)} `; +const getColumnWidth = (column: any): string => { + // Use explicit width if available + if (column.width) { + return column.width; + } + + // No explicit width - return auto to let flex handle it + return 'auto'; +}; + const ColWrapper = styled.div<{ $style: ResponsiveLayoutColStyleType | undefined, $width: string, $matchColumnsHeight: boolean, $useFlexLayout: boolean, + $hasExplicitWidth: boolean, }>` ${props => props.$useFlexLayout ? ` - flex: ${props.$width === '100%' ? '1 0 100%' : `0 0 ${props.$width}`}; - max-width: ${props.$width}; + ${props.$hasExplicitWidth + ? `flex: 0 0 ${props.$width}; max-width: ${props.$width};` + : 'flex: 1 1 0%; min-width: 0;'} ` : ''} > div { @@ -143,20 +155,6 @@ const ColumnContainer = (props: ColumnContainerProps) => { ); }; -const getColumnWidth = (column: any): string => { - // Use explicit width if available - if (column.width) { - // For percentage values, calculate precisely to accommodate gaps - if (column.width.endsWith('%')) { - return column.width; - } - return column.width; - } - - // If minWidth is set, use it or default to equal distribution - return column.minWidth || 'auto'; -}; - const ColumnLayout = (props: ColumnLayoutProps) => { let { columns, @@ -195,6 +193,7 @@ const ColumnLayout = (props: ColumnLayoutProps) => { if(!containers[id]) return null const containerProps = containers[id].children; const columnWidth = getColumnWidth(column); + const hasExplicitWidth = !!column.width; return ( @@ -203,6 +202,7 @@ const ColumnLayout = (props: ColumnLayoutProps) => { $width={columnWidth} $matchColumnsHeight={matchColumnsHeight} $useFlexLayout={useFlexLayout} + $hasExplicitWidth={hasExplicitWidth} > Date: Mon, 5 May 2025 23:30:12 +0500 Subject: [PATCH 5/8] [Fix]: #1626 remove min-width from the UI --- .../packages/lowcoder/src/comps/controls/optionsControl.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index bc1dba6eb..8e25d053d 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -604,11 +604,6 @@ ColumnOption = class extends ColumnOption implements OptionCompProperty { return ( - {useFlexLayout && this.children.minWidth.propertyView({ - label: trans('responsiveLayout.minWidth'), - preInputNode: , - placeholder: '3px', - })} {useFlexLayout && this.children.width.propertyView({ label: trans('responsiveLayout.width'), preInputNode: , From 603a62d4899f65ae13208c79cc49090141667708 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 6 May 2025 14:52:17 +0500 Subject: [PATCH 6/8] revert to Grid --- .../comps/comps/columnLayout/columnLayout.tsx | 117 +++++------------- .../src/comps/controls/optionsControl.tsx | 94 ++++++-------- .../packages/lowcoder/src/i18n/locales/en.ts | 4 - 3 files changed, 75 insertions(+), 140 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index d5b4f6f52..12c2bf0a1 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -43,34 +43,20 @@ import { DisabledContext } from "comps/generators/uiCompBuilder"; import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; -// Extended ContainerStyleType for our specific needs -interface ExtendedContainerStyleType extends ContainerStyleType { - display?: string; - gridTemplateColumns?: string; - gridTemplateRows?: string; - columnGap?: string; - rowGap?: string; - [key: string]: string | undefined; -} - const ContainWrapper = styled.div<{ - $style: ExtendedContainerStyleType | undefined; - $useFlexLayout: boolean; + $style: ContainerStyleType & { + display: string, + gridTemplateColumns: string, + columnGap: string, + gridTemplateRows: string, + rowGap: string, + } | undefined; }>` - display: ${(props) => props.$useFlexLayout ? 'flex' : props.$style?.display}; - flex-wrap: ${(props) => props.$useFlexLayout ? 'wrap' : 'nowrap'}; - - ${(props) => !props.$useFlexLayout && ` - grid-template-columns: ${props.$style?.gridTemplateColumns}; - grid-template-rows: ${props.$style?.gridTemplateRows}; - column-gap: ${props.$style?.columnGap}; - row-gap: ${props.$style?.rowGap}; - `} - - ${(props) => props.$useFlexLayout && ` - column-gap: ${props.$style?.columnGap || '0'}; - row-gap: ${props.$style?.rowGap || '0'}; - `} + display: ${(props) => props.$style?.display}; + grid-template-columns: ${(props) => props.$style?.gridTemplateColumns}; + grid-template-rows: ${(props) => props.$style?.gridTemplateRows}; + column-gap: ${(props) => props.$style?.columnGap}; + row-gap: ${(props) => props.$style?.rowGap}; border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}; @@ -81,29 +67,12 @@ const ContainWrapper = styled.div<{ ${props => props.$style && getBackgroundStyle(props.$style)} `; -const getColumnWidth = (column: any): string => { - // Use explicit width if available - if (column.width) { - return column.width; - } - - // No explicit width - return auto to let flex handle it - return 'auto'; -}; - -const ColWrapper = styled.div<{ +const ColWrapper = styled(Col)<{ $style: ResponsiveLayoutColStyleType | undefined, - $width: string, + $minWidth?: string, $matchColumnsHeight: boolean, - $useFlexLayout: boolean, - $hasExplicitWidth: boolean, }>` - ${props => props.$useFlexLayout ? ` - ${props.$hasExplicitWidth - ? `flex: 0 0 ${props.$width}; max-width: ${props.$width};` - : 'flex: 1 1 0%; min-width: 0;'} - ` : ''} - + min-width: ${(props) => props.$minWidth || 'auto'}; > div { height: ${(props) => props.$matchColumnsHeight ? `calc(100% - ${props.$style?.padding || 0} - ${props.$style?.padding || 0})` : 'auto'}; border-radius: ${(props) => props.$style?.radius}; @@ -127,13 +96,12 @@ const childrenMap = { autoHeight: AutoHeightControl, matchColumnsHeight: withDefault(BoolControl, true), templateRows: withDefault(StringControl, "1fr"), - rowGap: withDefault(StringControl, "0"), + rowGap: withDefault(StringControl, "20px"), templateColumns: withDefault(StringControl, "1fr 1fr"), mainScrollbar: withDefault(BoolControl, false), - columnGap: withDefault(StringControl, "0"), + columnGap: withDefault(StringControl, "20px"), style: styleControl(ContainerStyle, 'style'), - columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle'), - useFlexLayout: withDefault(BoolControl, false), + columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle') }; type ViewProps = RecordConstructorToView; @@ -155,6 +123,7 @@ const ColumnContainer = (props: ColumnContainerProps) => { ); }; + const ColumnLayout = (props: ColumnLayoutProps) => { let { columns, @@ -167,8 +136,7 @@ const ColumnLayout = (props: ColumnLayoutProps) => { columnGap, columnStyle, horizontalGridCells, - mainScrollbar, - useFlexLayout, + mainScrollbar } = props; return ( @@ -176,33 +144,27 @@ const ColumnLayout = (props: ColumnLayoutProps) => {
- + {columns.map(column => { const id = String(column.id); const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); if(!containers[id]) return null const containerProps = containers[id].children; - const columnWidth = getColumnWidth(column); - const hasExplicitWidth = !!column.width; - + const noOfColumns = columns.length; return ( - + @@ -260,10 +221,6 @@ export const ResponsiveLayoutBaseComp = (function () { {children.horizontalGridCells.propertyView({ label: trans('prop.horizontalGridCells'), })} - {children.useFlexLayout.propertyView({ - label: trans("responsiveLayout.useFlexLayout"), - tooltip: trans("responsiveLayout.useFlexLayoutTooltip") - })}
{children.matchColumnsHeight.propertyView({ label: trans("responsiveLayout.matchColumnsHeight") @@ -271,14 +228,8 @@ export const ResponsiveLayoutBaseComp = (function () { {controlItem({}, (
{trans("responsiveLayout.columnsSpacing")}
))} - {!children.useFlexLayout.getView() && children.templateColumns.propertyView({ - label: trans("responsiveLayout.columnDefinition"), - tooltip: trans("responsiveLayout.columnsDefinitionTooltip") - })} - {!children.useFlexLayout.getView() && children.templateRows.propertyView({ - label: trans("responsiveLayout.rowDefinition"), - tooltip: trans("responsiveLayout.rowsDefinitionTooltip") - })} + {children.templateColumns.propertyView({label: trans("responsiveLayout.columnDefinition"), tooltip: trans("responsiveLayout.columnsDefinitionTooltip")})} + {children.templateRows.propertyView({label: trans("responsiveLayout.rowDefinition"), tooltip: trans("responsiveLayout.rowsDefinitionTooltip")})} {children.columnGap.propertyView({label: trans("responsiveLayout.columnGap")})} {children.rowGap.propertyView({label: trans("responsiveLayout.rowGap")})}
diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index 8e25d053d..e32c32c09 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -40,7 +40,6 @@ import { ControlItemCompBuilder } from "comps/generators/controlCompBuilder"; import { ColorControl } from "./colorControl"; import { StringStateControl } from "./codeStateControl"; import { reduceInContext } from "../utils/reduceContext"; -import React from "react"; const OptionTypes = [ { @@ -66,13 +65,10 @@ type OptionControlParam = { title?: string; // The new option's label name newOptionLabel?: string; - // Whether to use flex layout (for column options) - useFlexLayout?: boolean; }; type OptionPropertyParam = { autoMap?: boolean; - useFlexLayout?: boolean; }; interface OptionCompProperty { @@ -180,7 +176,7 @@ export function manualOptionsControl( itemTitle={(comp) => comp.children.label.getView()} popoverTitle={() => trans("edit")} content={(comp) => { - return hasPropertyView(comp) ? comp.propertyView({ useFlexLayout: param.useFlexLayout }) : comp.getPropertyView(); + return hasPropertyView(comp) ? comp.propertyView({}) : comp.getPropertyView(); }} items={manualComp.getView()} onAdd={() => { @@ -580,13 +576,12 @@ const StyledContent = styled.div` } `; -let ColumnOption = new MultiCompBuilder( +const ColumnOption = new MultiCompBuilder( { id: valueComp(-1), label: StringControl, key: StringControl, minWidth: withDefault(RadiusControl, ""), - width: withDefault(RadiusControl, ""), background: withDefault(ColorControl, ""), backgroundImage: withDefault(StringControl, ""), border: withDefault(ColorControl, ""), @@ -595,55 +590,48 @@ let ColumnOption = new MultiCompBuilder( padding: withDefault(StringControl, ""), }, (props) => props -).build(); - -// Add propertyView method through class extension -ColumnOption = class extends ColumnOption implements OptionCompProperty { - propertyView(param: OptionPropertyParam) { - const useFlexLayout = param?.useFlexLayout !== undefined ? param.useFlexLayout : true; - - return ( - - {useFlexLayout && this.children.width.propertyView({ - label: trans('responsiveLayout.width'), - preInputNode: , - placeholder: '50%', - })} - {this.children.background.propertyView({ - label: trans('style.background'), - })} - {this.children.backgroundImage.propertyView({ - label: `Background Image`, - // preInputNode: , - placeholder: 'https://temp.im/350x400', - })} - {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', - })} - - ); - } -}; +) +.setPropertyViewFn((children) => ( + + {children.minWidth.propertyView({ + label: trans('responsiveLayout.minWidth'), + preInputNode: , + placeholder: '3px', + })} + {children.background.propertyView({ + label: trans('style.background'), + })} + {children.backgroundImage.propertyView({ + label: `Background Image`, + // preInputNode: , + placeholder: 'https://temp.im/350x400', + })} + {children.border.propertyView({ + label: trans('style.border') + })} + {children.radius.propertyView({ + label: trans('style.borderRadius'), + preInputNode: , + placeholder: '3px', + })} + {children.margin.propertyView({ + label: trans('style.margin'), + preInputNode: , + placeholder: '3px', + })} + {children.padding.propertyView({ + label: trans('style.padding'), + preInputNode: , + placeholder: '3px', + })} + +)) + .build(); export const ColumnOptionControl = manualOptionsControl(ColumnOption, { initOptions: [ - { id: 0, key: "Column1", label: "Column1", width: "" }, - { id: 1, key: "Column2", label: "Column2", width: "" }, + { id: 0, key: "Column1", label: "Column1" }, + { id: 1, key: "Column2", label: "Column2" }, ], uniqField: "key", autoIncField: "id", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index ba248eea4..d1eff10d7 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3910,8 +3910,6 @@ export const en = { "rowStyle": "Row Style", "columnStyle": "Column Style", "minWidth": "Min. Width", - "width": "Width", - "widthTooltip": "Set the column width (e.g., '300px', '50%', '100%'). When set to 100%, columns will stack vertically.", "rowBreak": "Row Break", "useComponentWidth" : "Use Self Size", "useComponentWidthDesc" : "Use the container width instead the App width", @@ -3920,8 +3918,6 @@ export const en = { "columnsLayout": "Columns Layout", "columnsDefinitionTooltip": "Columns can be defined freely based on the CSS columns properties. For example, 'auto auto' will create two columns with equal width. Read more here: https://css-tricks.com/almanac/properties/g/grid-template-columns", "rowsDefinitionTooltip": "Rows can be defined freely based on the CSS rows properties. For example, 'auto auto' will create two rows with equal height. Read more here: https://css-tricks.com/almanac/properties/g/grid-template-rows", - "useFlexLayout": "Use Flexible Layout", - "useFlexLayoutTooltip": "Enable responsive behavior where columns can wrap when there's not enough space" }, "splitLayout" : { "column": "View Areas", From dab7174a90fc72210b2905882faf6950ad1a9850 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 13 May 2025 14:39:58 +0500 Subject: [PATCH 7/8] [FIX] #1626 add min-width using css grid minmax() --- .../comps/comps/columnLayout/columnLayout.tsx | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index 12c2bf0a1..f30a3f808 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -123,6 +123,53 @@ const ColumnContainer = (props: ColumnContainerProps) => { ); }; +// Function to apply min-widths to grid template columns +const applyMinWidthsToGridColumns = (columnsDef: string, minWidths: (string | null)[] = []) => { + // Handle empty case + if (!columnsDef?.trim()) return ''; + + // Handle repeat() functions with special parsing + if (columnsDef.includes('repeat(')) { + // For complex repeat patterns, we should return as-is to avoid breaking the layout + // A more complex parser would be needed to fully support repeat with minmax + return columnsDef; + } + + const columns = columnsDef.trim().split(/\s+/); + + const newColumns = columns.map((col, index) => { + const minWidth = minWidths[index]; + + // Skip if no minWidth provided for this column + if (!minWidth) { + return col; + } + + // Keywords that should never be wrapped in minmax() + const keywords = ['auto', 'min-content', 'max-content', 'fit-content', 'subgrid']; + if (keywords.some(keyword => col === keyword)) { + return col; + } + + // Functions that should never be wrapped in minmax() + if (col.includes('(') && col.includes(')')) { + // Already includes a function like calc(), minmax(), etc. + return col; + } + + // Determine if column is flexible and can be wrapped with minmax + // - fr units (e.g., "1fr", "2.5fr") + // - percentage values (e.g., "50%") + // - length values (px, em, rem, etc.) + const isFlexible = /fr$/.test(col) || + /%$/.test(col) || + /^\d+(\.\d+)?(px|em|rem|vh|vw|vmin|vmax|cm|mm|in|pt|pc)$/.test(col); + + return isFlexible ? `minmax(${minWidth}, ${col})` : col; + }); + + return newColumns.join(' '); +}; const ColumnLayout = (props: ColumnLayoutProps) => { let { @@ -139,6 +186,12 @@ const ColumnLayout = (props: ColumnLayoutProps) => { mainScrollbar } = props; + // Extract minWidths from columns + const minWidths = columns.map(column => column.minWidth || null); + + // Apply min-widths to grid template columns + const gridTemplateColumns = applyMinWidthsToGridColumns(templateColumns, minWidths); + return ( @@ -147,7 +200,7 @@ const ColumnLayout = (props: ColumnLayoutProps) => { Date: Tue, 13 May 2025 16:07:09 +0500 Subject: [PATCH 8/8] [FIX] #1503 min-width for ResponsiveLayout Component --- .../src/comps/comps/responsiveLayout/responsiveLayout.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx index 13b3b8b5c..0ce837560 100644 --- a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx @@ -87,7 +87,7 @@ const ColWrapper = styled(Col)<{ flex-basis: ${(props) => props.$rowBreak ? `calc(100% / var(--columns))` // Force exact column distribution - : `clamp(${props.$minWidth}, 100% / var(--columns), 100%)`}; // MinWidth respected + : `clamp(${props.$minWidth || "0px"}, calc(100% / var(--columns)), 100%)`}; // MinWidth respected min-width: ${(props) => props.$minWidth}; // Ensure minWidth is respected max-width: 100%; // Prevent more columns than allowed @@ -237,7 +237,8 @@ const ResponsiveLayout = (props: ResponsiveLayoutProps) => { if (!containers[id]) return null; const containerProps = containers[id].children; - const calculatedWidth = 100 / numberOfColumns; + // Use the actual minWidth from column configuration instead of calculated width + const columnMinWidth = column.minWidth || `${100 / numberOfColumns}px`; return ( { sm={rowBreak ? 24 / numberOfColumns : undefined} xs={rowBreak ? 24 / numberOfColumns : undefined} $style={props.columnStyle} - $minWidth={`${calculatedWidth}px`} + $minWidth={columnMinWidth} $matchColumnsHeight={matchColumnsHeight} $rowBreak={rowBreak} >