Skip to content

Commit f24a7f7

Browse files
authored
Merge pull request #346 from raheeliftikhar5/layout-component
Layout component
2 parents 14d26d5 + bf3bc70 commit f24a7f7

File tree

14 files changed

+523
-17
lines changed

14 files changed

+523
-17
lines changed
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

client/packages/lowcoder-design/src/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,5 @@ export { ReactComponent as TimeLineIcon } from "icons/icon-timeline-comp.svg"
291291
export { ReactComponent as LottieIcon } from "icons/icon-lottie.svg";
292292
export { ReactComponent as MentionIcon } from "icons/icon-mention-comp.svg";
293293
export { ReactComponent as AutoCompleteCompIcon } from "icons/icon-autocomplete-comp.svg";
294+
export { ReactComponent as WidthIcon } from "icons/icon-width.svg";
295+
export { ReactComponent as ResponsiveLayoutCompIcon } from "icons/icon-responsive-layout-comp.svg";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ResponsiveLayoutComp } from "./responsiveLayout";
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import { Row, Col } from "antd";
2+
import { JSONObject, JSONValue } from "util/jsonTypes";
3+
import { CompAction, CompActionTypes, deleteCompAction, wrapChildAction } from "lowcoder-core";
4+
import { DispatchType, RecordConstructorToView, wrapDispatch } from "lowcoder-core";
5+
import { AutoHeightControl } from "comps/controls/autoHeightControl";
6+
import { ColumnOptionControl } from "comps/controls/optionsControl";
7+
import { styleControl } from "comps/controls/styleControl";
8+
import {
9+
ResponsiveLayoutRowStyle,
10+
ResponsiveLayoutRowStyleType,
11+
ResponsiveLayoutColStyleType,
12+
ResponsiveLayoutColStyle
13+
} from "comps/controls/styleControlConstants";
14+
import { sameTypeMap, UICompBuilder, withDefault } from "comps/generators";
15+
import { addMapChildAction } from "comps/generators/sameTypeMap";
16+
import { NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing";
17+
import { NameGenerator } from "comps/utils";
18+
import { Section, controlItem, sectionNames } from "lowcoder-design";
19+
import { HintPlaceHolder } from "lowcoder-design";
20+
import _ from "lodash";
21+
import React from "react";
22+
import styled from "styled-components";
23+
import { IContainer } from "../containerBase/iContainer";
24+
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
25+
import { CompTree, mergeCompTrees } from "../containerBase/utils";
26+
import {
27+
ContainerBaseProps,
28+
gridItemCompToGridItems,
29+
InnerGrid,
30+
} from "../containerComp/containerView";
31+
import { BackgroundColorContext } from "comps/utils/backgroundColorContext";
32+
import { trans } from "i18n";
33+
import { messageInstance } from "lowcoder-design";
34+
import { BoolControl } from "comps/controls/boolControl";
35+
import { NumberControl } from "comps/controls/codeControl";
36+
37+
const RowWrapper = styled(Row)<{$style: ResponsiveLayoutRowStyleType}>`
38+
height: 100%;
39+
border: 1px solid ${(props) => props.$style.border};
40+
border-radius: ${(props) => props.$style.radius};
41+
padding: ${(props) => props.$style.padding};
42+
background-color: ${(props) => props.$style.background};
43+
overflow-x: auto;
44+
`;
45+
46+
const ColWrapper = styled(Col)<{
47+
$style: ResponsiveLayoutColStyleType,
48+
$minWidth?: string,
49+
$matchColumnsHeight: boolean,
50+
}>`
51+
min-width: ${(props) => props.$minWidth};
52+
display: flex;
53+
flex-direction: column;
54+
55+
> div {
56+
height: ${(props) => props.$matchColumnsHeight ? '100%' : 'auto'};
57+
}
58+
`;
59+
60+
const childrenMap = {
61+
columns: ColumnOptionControl,
62+
containers: withDefault(sameTypeMap(SimpleContainerComp), {
63+
0: { view: {}, layout: {} },
64+
1: { view: {}, layout: {} },
65+
}),
66+
autoHeight: AutoHeightControl,
67+
rowBreak: withDefault(BoolControl, false),
68+
matchColumnsHeight: withDefault(BoolControl, false),
69+
rowStyle: withDefault(styleControl(ResponsiveLayoutRowStyle), {}),
70+
columnStyle: withDefault(styleControl(ResponsiveLayoutColStyle), {}),
71+
columnPerRowLG: withDefault(NumberControl, 4),
72+
columnPerRowMD: withDefault(NumberControl, 2),
73+
columnPerRowSM: withDefault(NumberControl, 1),
74+
verticalSpacing: withDefault(NumberControl, 8),
75+
horizontalSpacing: withDefault(NumberControl, 8),
76+
};
77+
78+
type ViewProps = RecordConstructorToView<typeof childrenMap>;
79+
type ResponsiveLayoutProps = ViewProps & { dispatch: DispatchType };
80+
type ColumnContainerProps = Omit<ContainerBaseProps, 'style'> & {
81+
style: ResponsiveLayoutColStyleType,
82+
}
83+
84+
const ColumnContainer = (props: ColumnContainerProps) => {
85+
return (
86+
<InnerGrid
87+
{...props}
88+
emptyRows={15}
89+
hintPlaceholder={HintPlaceHolder}
90+
radius={props.style.radius}
91+
style={props.style}
92+
/>
93+
);
94+
};
95+
96+
97+
const ResponsiveLayout = (props: ResponsiveLayoutProps) => {
98+
let {
99+
columns,
100+
containers,
101+
dispatch,
102+
rowBreak,
103+
matchColumnsHeight,
104+
rowStyle,
105+
columnStyle,
106+
columnPerRowLG,
107+
columnPerRowMD,
108+
columnPerRowSM,
109+
verticalSpacing,
110+
horizontalSpacing,
111+
} = props;
112+
113+
return (
114+
<BackgroundColorContext.Provider value={props.rowStyle.background}>
115+
<div style={{padding: rowStyle.margin, height: '100%'}}>
116+
<RowWrapper
117+
$style={rowStyle}
118+
wrap={rowBreak}
119+
gutter={[horizontalSpacing, verticalSpacing]}
120+
>
121+
{columns.map(column => {
122+
const id = String(column.id);
123+
const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id);
124+
if(!containers[id]) return null
125+
const containerProps = containers[id].children;
126+
127+
const columnCustomStyle = {
128+
margin: !_.isEmpty(column.margin) ? column.margin : columnStyle.margin,
129+
padding: !_.isEmpty(column.padding) ? column.padding : columnStyle.padding,
130+
radius: !_.isEmpty(column.radius) ? column.radius : columnStyle.radius,
131+
border: `1px solid ${!_.isEmpty(column.border) ? column.border : columnStyle.border}`,
132+
background: !_.isEmpty(column.background) ? column.background : columnStyle.background,
133+
}
134+
const noOfColumns = columns.length;
135+
let backgroundStyle = columnCustomStyle.background;
136+
if(!_.isEmpty(column.backgroundImage)) {
137+
backgroundStyle = `center / cover url('${column.backgroundImage}') no-repeat, ${backgroundStyle}`;
138+
}
139+
return (
140+
<ColWrapper
141+
key={id}
142+
lg={24/(noOfColumns < columnPerRowLG ? noOfColumns : columnPerRowLG)}
143+
md={24/(noOfColumns < columnPerRowMD ? noOfColumns : columnPerRowMD)}
144+
sm={24/(noOfColumns < columnPerRowSM ? noOfColumns : columnPerRowSM)}
145+
xs={24/(noOfColumns < columnPerRowSM ? noOfColumns : columnPerRowSM)}
146+
$style={columnCustomStyle}
147+
$minWidth={column.minWidth}
148+
$matchColumnsHeight={matchColumnsHeight}
149+
>
150+
<ColumnContainer
151+
layout={containerProps.layout.getView()}
152+
items={gridItemCompToGridItems(containerProps.items.getView())}
153+
positionParams={containerProps.positionParams.getView()}
154+
dispatch={childDispatch}
155+
autoHeight={props.autoHeight}
156+
style={{
157+
...columnCustomStyle,
158+
background: backgroundStyle,
159+
}}
160+
/>
161+
</ColWrapper>
162+
)
163+
})
164+
}
165+
</RowWrapper>
166+
</div>
167+
</BackgroundColorContext.Provider>
168+
);
169+
};
170+
171+
export const ResponsiveLayoutBaseComp = (function () {
172+
return new UICompBuilder(childrenMap, (props, dispatch) => {
173+
return (
174+
<ResponsiveLayout {...props} dispatch={dispatch} />
175+
);
176+
})
177+
.setPropertyViewFn((children) => {
178+
return (
179+
<>
180+
<Section name={sectionNames.basic}>
181+
{children.columns.propertyView({
182+
title: trans("responsiveLayout.column"),
183+
newOptionLabel: "Column",
184+
})}
185+
{children.autoHeight.getPropertyView()}
186+
</Section>
187+
<Section name={trans("responsiveLayout.rowLayout")}>
188+
{children.rowBreak.propertyView({
189+
label: trans("responsiveLayout.rowBreak")
190+
})}
191+
{controlItem({}, (
192+
<div style={{marginTop: '8px'}}>
193+
{trans("responsiveLayout.columnsPerRow")}
194+
</div>
195+
))}
196+
{children.columnPerRowLG.propertyView({
197+
label: trans("responsiveLayout.desktop")
198+
})}
199+
{children.columnPerRowMD.propertyView({
200+
label: trans("responsiveLayout.tablet")
201+
})}
202+
{children.columnPerRowSM.propertyView({
203+
label: trans("responsiveLayout.mobile")
204+
})}
205+
</Section>
206+
<Section name={trans("responsiveLayout.columnsLayout")}>
207+
{children.matchColumnsHeight.propertyView({
208+
label: trans("responsiveLayout.matchColumnsHeight")
209+
})}
210+
{controlItem({}, (
211+
<div style={{marginTop: '8px'}}>
212+
{trans("responsiveLayout.columnsSpacing")}
213+
</div>
214+
))}
215+
{children.horizontalSpacing.propertyView({
216+
label: trans("responsiveLayout.horizontal")
217+
})}
218+
{children.verticalSpacing.propertyView({
219+
label: trans("responsiveLayout.vertical")
220+
})}
221+
</Section>
222+
<Section name={trans("responsiveLayout.rowStyle")}>
223+
{children.rowStyle.getPropertyView()}
224+
</Section>
225+
<Section name={trans("responsiveLayout.columnStyle")}>
226+
{children.columnStyle.getPropertyView()}
227+
</Section>
228+
</>
229+
);
230+
})
231+
.build();
232+
})();
233+
234+
class ResponsiveLayoutImplComp extends ResponsiveLayoutBaseComp implements IContainer {
235+
private syncContainers(): this {
236+
const columns = this.children.columns.getView();
237+
const ids: Set<string> = new Set(columns.map((column) => String(column.id)));
238+
let containers = this.children.containers.getView();
239+
// delete
240+
const actions: CompAction[] = [];
241+
Object.keys(containers).forEach((id) => {
242+
if (!ids.has(id)) {
243+
// log.debug("syncContainers delete. ids=", ids, " id=", id);
244+
actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction())));
245+
}
246+
});
247+
// new
248+
ids.forEach((id) => {
249+
if (!containers.hasOwnProperty(id)) {
250+
// log.debug("syncContainers new containers: ", containers, " id: ", id);
251+
actions.push(
252+
wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} }))
253+
);
254+
}
255+
});
256+
// log.debug("syncContainers. actions: ", actions);
257+
let instance = this;
258+
actions.forEach((action) => {
259+
instance = instance.reduce(action);
260+
});
261+
return instance;
262+
}
263+
264+
override reduce(action: CompAction): this {
265+
const columns = this.children.columns.getView();
266+
if (action.type === CompActionTypes.CUSTOM) {
267+
const value = action.value as JSONObject;
268+
if (value.type === "push") {
269+
const itemValue = value.value as JSONObject;
270+
if (_.isEmpty(itemValue.key)) itemValue.key = itemValue.label;
271+
action = {
272+
...action,
273+
value: {
274+
...value,
275+
value: { ...itemValue },
276+
},
277+
} as CompAction;
278+
}
279+
if (value.type === "delete" && columns.length <= 1) {
280+
messageInstance.warning(trans("responsiveLayout.atLeastOneColumnError"));
281+
// at least one column
282+
return this;
283+
}
284+
}
285+
// log.debug("before super reduce. action: ", action);
286+
let newInstance = super.reduce(action);
287+
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
288+
// Need eval to get the value in StringControl
289+
newInstance = newInstance.syncContainers();
290+
}
291+
// log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
292+
return newInstance;
293+
}
294+
295+
realSimpleContainer(key?: string): SimpleContainerComp | undefined {
296+
return Object.values(this.children.containers.children).find((container) =>
297+
container.realSimpleContainer(key)
298+
);
299+
}
300+
301+
getCompTree(): CompTree {
302+
const containerMap = this.children.containers.getView();
303+
const compTrees = Object.values(containerMap).map((container) => container.getCompTree());
304+
return mergeCompTrees(compTrees);
305+
}
306+
307+
findContainer(key: string): IContainer | undefined {
308+
const containerMap = this.children.containers.getView();
309+
for (const container of Object.values(containerMap)) {
310+
const foundContainer = container.findContainer(key);
311+
if (foundContainer) {
312+
return foundContainer === container ? this : foundContainer;
313+
}
314+
}
315+
return undefined;
316+
}
317+
318+
getPasteValue(nameGenerator: NameGenerator): JSONValue {
319+
const containerMap = this.children.containers.getView();
320+
const containerPasteValueMap = _.mapValues(containerMap, (container) =>
321+
container.getPasteValue(nameGenerator)
322+
);
323+
324+
return { ...this.toJsonValue(), containers: containerPasteValueMap };
325+
}
326+
327+
override autoHeight(): boolean {
328+
return this.children.autoHeight.getView();
329+
}
330+
}
331+
332+
export const ResponsiveLayoutComp = withExposingConfigs(
333+
ResponsiveLayoutImplComp,
334+
[ NameConfigHidden]
335+
);

client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -196,20 +196,6 @@ const TabbedContainer = (props: TabbedContainerProps) => {
196196
</BackgroundColorContext.Provider>
197197
)
198198
}
199-
// return (
200-
// <TabPane tab={label} key={tab.key} forceRender>
201-
// <BackgroundColorContext.Provider value={props.style.background}>
202-
// <ContainerInTab
203-
// layout={containerProps.layout.getView()}
204-
// items={gridItemCompToGridItems(containerProps.items.getView())}
205-
// positionParams={containerProps.positionParams.getView()}
206-
// dispatch={childDispatch}
207-
// autoHeight={props.autoHeight}
208-
// containerPadding={[paddingWidth, 20]}
209-
// />
210-
// </BackgroundColorContext.Provider>
211-
// </TabPane>
212-
// );
213199
})
214200

215201
return (

0 commit comments

Comments
 (0)