Skip to content

Commit 0d47fe8

Browse files
authored
Merge pull request #829 from newwork-software/addColumnLayout
WIP: Add column based layout
2 parents b4852b6 + f9e9008 commit 0d47fe8

File tree

5 files changed

+332
-2
lines changed

5 files changed

+332
-2
lines changed
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import { default as Row } from "antd/es/row";
2+
import { default as Col } from "antd/es/col";
3+
import { JSONObject, JSONValue } from "util/jsonTypes";
4+
import { CompAction, CompActionTypes, deleteCompAction, wrapChildAction } from "lowcoder-core";
5+
import { DispatchType, RecordConstructorToView, wrapDispatch } from "lowcoder-core";
6+
import { AutoHeightControl } from "comps/controls/autoHeightControl";
7+
import { ColumnOptionControl } from "comps/controls/optionsControl";
8+
import { styleControl } from "comps/controls/styleControl";
9+
import {
10+
ResponsiveLayoutRowStyle,
11+
ResponsiveLayoutRowStyleType,
12+
ResponsiveLayoutColStyleType,
13+
ResponsiveLayoutColStyle
14+
} from "comps/controls/styleControlConstants";
15+
import { sameTypeMap, UICompBuilder, withDefault } from "comps/generators";
16+
import { addMapChildAction } from "comps/generators/sameTypeMap";
17+
import { NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing";
18+
import { NameGenerator } from "comps/utils";
19+
import { Section, controlItem, sectionNames } from "lowcoder-design";
20+
import { HintPlaceHolder } from "lowcoder-design";
21+
import _ from "lodash";
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/src/components/GlobalInstances";
34+
import { BoolControl } from "comps/controls/boolControl";
35+
import { BoolCodeControl, NumberControl, StringControl } from "comps/controls/codeControl";
36+
37+
import { useContext } from "react";
38+
import { EditorContext } from "comps/editorState";
39+
40+
import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils";
41+
import { DisabledContext } from "comps/generators/uiCompBuilder";
42+
43+
44+
const ColWrapper = styled(Col)<{
45+
$style: ResponsiveLayoutColStyleType,
46+
$minWidth?: string,
47+
$matchColumnsHeight: boolean,
48+
}>`
49+
> div {
50+
height: ${(props) => props.$matchColumnsHeight ? '100%' : 'auto'};
51+
}
52+
`;
53+
54+
const childrenMap = {
55+
disabled: BoolCodeControl,
56+
columns: ColumnOptionControl,
57+
containers: withDefault(sameTypeMap(SimpleContainerComp), {
58+
0: { view: {}, layout: {} },
59+
1: { view: {}, layout: {} },
60+
}),
61+
autoHeight: AutoHeightControl,
62+
matchColumnsHeight: withDefault(BoolControl, true),
63+
templateRows: withDefault(StringControl, "1fr"),
64+
rowGap: withDefault(StringControl, "20px"),
65+
templateColumns: withDefault(StringControl, "1fr 1fr"),
66+
columnGap: withDefault(StringControl, "20px"),
67+
columnStyle: withDefault(styleControl(ResponsiveLayoutColStyle), {})
68+
};
69+
70+
type ViewProps = RecordConstructorToView<typeof childrenMap>;
71+
type ColumnLayoutProps = ViewProps & { dispatch: DispatchType };
72+
type ColumnContainerProps = Omit<ContainerBaseProps, 'style'> & {
73+
style: ResponsiveLayoutColStyleType,
74+
}
75+
76+
const ColumnContainer = (props: ColumnContainerProps) => {
77+
return (
78+
<InnerGrid
79+
{...props}
80+
emptyRows={15}
81+
hintPlaceholder={HintPlaceHolder}
82+
radius={"0"}
83+
style={props.style}
84+
enableGridLines={false}
85+
/>
86+
);
87+
};
88+
89+
90+
const ColumnLayout = (props: ColumnLayoutProps) => {
91+
let {
92+
columns,
93+
containers,
94+
dispatch,
95+
matchColumnsHeight,
96+
templateRows,
97+
rowGap,
98+
templateColumns,
99+
columnGap,
100+
columnStyle,
101+
} = props;
102+
103+
return (
104+
<BackgroundColorContext.Provider value={"none"}>
105+
<DisabledContext.Provider value={props.disabled}>
106+
<div style={{height: '100%', backgroundColor: "pink"}}>
107+
<div style={{display: "grid", gridTemplateColumns: templateColumns, columnGap, gridTemplateRows: templateRows, rowGap}}>
108+
{columns.map(column => {
109+
const id = String(column.id);
110+
const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id);
111+
if(!containers[id]) return null
112+
const containerProps = containers[id].children;
113+
114+
const columnCustomStyle = {
115+
margin: "0",
116+
padding: !_.isEmpty(column.padding) ? column.padding : "0",
117+
radius: "0",
118+
border: "1px dashed pink", // `${!_.isEmpty(column.border) ? column.border : columnStyle.border}`,
119+
background: !_.isEmpty(column.background) ? column.background : columnStyle.background,
120+
overflow: "hidden",
121+
backgroundImage: "linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 100%), linear-gradient(to bottom, rgba(253, 246, 199, 1) 0%, rgba(253, 246, 199, 1) 100%)",
122+
backgroundClip: "content-box, padding-box",
123+
124+
}
125+
const noOfColumns = columns.length;
126+
let backgroundStyle = columnCustomStyle.background;
127+
if(!_.isEmpty(column.backgroundImage)) {
128+
backgroundStyle = `center / cover url('${column.backgroundImage}') no-repeat, ${backgroundStyle}`;
129+
}
130+
return (
131+
<ColWrapper
132+
key={id}
133+
$style={columnCustomStyle}
134+
$minWidth={column.minWidth}
135+
$matchColumnsHeight={matchColumnsHeight}
136+
>
137+
<ColumnContainer
138+
layout={containerProps.layout.getView()}
139+
items={gridItemCompToGridItems(containerProps.items.getView())}
140+
positionParams={containerProps.positionParams.getView()}
141+
dispatch={childDispatch}
142+
autoHeight={props.autoHeight}
143+
style={{
144+
...columnCustomStyle,
145+
background: backgroundStyle,
146+
}}
147+
/>
148+
</ColWrapper>
149+
)
150+
})
151+
}
152+
</div>
153+
</div>
154+
</DisabledContext.Provider>
155+
</BackgroundColorContext.Provider>
156+
);
157+
};
158+
159+
export const ResponsiveLayoutBaseComp = (function () {
160+
return new UICompBuilder(childrenMap, (props, dispatch) => {
161+
return (
162+
<ColumnLayout {...props} dispatch={dispatch} />
163+
);
164+
})
165+
.setPropertyViewFn((children) => {
166+
return (
167+
<>
168+
<Section name={sectionNames.basic}>
169+
{children.columns.propertyView({
170+
title: trans("responsiveLayout.column"),
171+
newOptionLabel: "Column",
172+
})}
173+
{children.templateColumns.propertyView({label: "Column Definition"})}
174+
{children.templateRows.propertyView({label: "Row Definition"})}
175+
</Section>
176+
177+
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && (
178+
<Section name={sectionNames.interaction}>
179+
{disabledPropertyView(children)}
180+
{hiddenPropertyView(children)}
181+
</Section>
182+
)}
183+
184+
{["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (
185+
<>
186+
<Section name={sectionNames.layout}>
187+
{children.autoHeight.getPropertyView()}
188+
</Section>
189+
<Section name={trans("responsiveLayout.columnsLayout")}>
190+
{children.matchColumnsHeight.propertyView({
191+
label: trans("responsiveLayout.matchColumnsHeight")
192+
})}
193+
{controlItem({}, (
194+
<div style={{ marginTop: '8px' }}>
195+
{trans("responsiveLayout.columnsSpacing")}
196+
</div>
197+
))}
198+
{children.columnGap.propertyView({label: "Column Gap"})}
199+
{children.rowGap.propertyView({label: "Row Gap"})}
200+
</Section>
201+
</>
202+
)}
203+
</>
204+
);
205+
})
206+
.build();
207+
})();
208+
209+
class ColumnLayoutImplComp extends ResponsiveLayoutBaseComp implements IContainer {
210+
private syncContainers(): this {
211+
const columns = this.children.columns.getView();
212+
const ids: Set<string> = new Set(columns.map((column) => String(column.id)));
213+
let containers = this.children.containers.getView();
214+
// delete
215+
const actions: CompAction[] = [];
216+
Object.keys(containers).forEach((id) => {
217+
if (!ids.has(id)) {
218+
// log.debug("syncContainers delete. ids=", ids, " id=", id);
219+
actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction())));
220+
}
221+
});
222+
// new
223+
ids.forEach((id) => {
224+
if (!containers.hasOwnProperty(id)) {
225+
// log.debug("syncContainers new containers: ", containers, " id: ", id);
226+
actions.push(
227+
wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} }))
228+
);
229+
}
230+
});
231+
// log.debug("syncContainers. actions: ", actions);
232+
let instance = this;
233+
actions.forEach((action) => {
234+
instance = instance.reduce(action);
235+
});
236+
return instance;
237+
}
238+
239+
override reduce(action: CompAction): this {
240+
const columns = this.children.columns.getView();
241+
if (action.type === CompActionTypes.CUSTOM) {
242+
const value = action.value as JSONObject;
243+
if (value.type === "push") {
244+
const itemValue = value.value as JSONObject;
245+
if (_.isEmpty(itemValue.key)) itemValue.key = itemValue.label;
246+
action = {
247+
...action,
248+
value: {
249+
...value,
250+
value: { ...itemValue },
251+
},
252+
} as CompAction;
253+
}
254+
const { path } = action;
255+
if (value.type === "delete" && path[0] === 'columns' && columns.length <= 1) {
256+
messageInstance.warning(trans("responsiveLayout.atLeastOneColumnError"));
257+
// at least one column
258+
return this;
259+
}
260+
}
261+
// log.debug("before super reduce. action: ", action);
262+
let newInstance = super.reduce(action);
263+
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
264+
// Need eval to get the value in StringControl
265+
newInstance = newInstance.syncContainers();
266+
}
267+
// log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
268+
return newInstance;
269+
}
270+
271+
realSimpleContainer(key?: string): SimpleContainerComp | undefined {
272+
return Object.values(this.children.containers.children).find((container) =>
273+
container.realSimpleContainer(key)
274+
);
275+
}
276+
277+
getCompTree(): CompTree {
278+
const containerMap = this.children.containers.getView();
279+
const compTrees = Object.values(containerMap).map((container) => container.getCompTree());
280+
return mergeCompTrees(compTrees);
281+
}
282+
283+
findContainer(key: string): IContainer | undefined {
284+
const containerMap = this.children.containers.getView();
285+
for (const container of Object.values(containerMap)) {
286+
const foundContainer = container.findContainer(key);
287+
if (foundContainer) {
288+
return foundContainer === container ? this : foundContainer;
289+
}
290+
}
291+
return undefined;
292+
}
293+
294+
getPasteValue(nameGenerator: NameGenerator): JSONValue {
295+
const containerMap = this.children.containers.getView();
296+
const containerPasteValueMap = _.mapValues(containerMap, (container) =>
297+
container.getPasteValue(nameGenerator)
298+
);
299+
300+
return { ...this.toJsonValue(), containers: containerPasteValueMap };
301+
}
302+
303+
override autoHeight(): boolean {
304+
return this.children.autoHeight.getView();
305+
}
306+
}
307+
308+
export const ColumnLayoutComp = withExposingConfigs(
309+
ColumnLayoutImplComp,
310+
[ NameConfigHidden]
311+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ColumnLayoutComp } from "./columnLayout";

client/packages/lowcoder/src/comps/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,23 @@ export var uiCompMap: Registry = {
429429
defaultDataFnName: "defaultPageLayoutData",
430430
defaultDataFnPath: "comps/tableComp/mockTableComp",
431431
},
432+
columnLayout: {
433+
name: "Column Layout",
434+
enName: "Column Layout",
435+
description: trans("uiComp.responsiveLayoutCompDesc"),
436+
categories: ["layout"],
437+
icon: ResponsiveLayoutCompIcon,
438+
keywords: trans("uiComp.responsiveLayoutCompKeywords"),
439+
lazyLoad: true,
440+
compName: 'ColumnLayoutComp',
441+
compPath: 'comps/columnLayout/index',
442+
withoutLoading: true,
443+
layoutInfo: {
444+
w: 24,
445+
h: 25,
446+
delayCollision: true,
447+
},
448+
},
432449
floatTextContainer: {
433450
name: trans("uiComp.floatTextContainerCompName"),
434451
enName: "Container",

client/packages/lowcoder/src/comps/uiCompRegistry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export type UICompType =
160160
| "sunburstChart"
161161
| "themeriverChart"
162162
| "basicChart"
163+
| "columnLayout"
163164
;
164165

165166

client/packages/lowcoder/src/pages/editor/editorConstants.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ import {
9999
HillchartCompIconSmall,
100100
TurnstileCaptchaCompIconSmall,
101101
PivotTableCompIconSmall,
102-
GraphChartCompIconSmall
103-
102+
GraphChartCompIconSmall,
104103
} from "lowcoder-design";
105104

106105
export const CompStateIcon: {
@@ -115,6 +114,7 @@ export const CompStateIcon: {
115114
chart: <ChartCompIconSmall />,
116115
checkbox: <CheckboxCompIconSmall />,
117116
collapsibleContainer: <CollapsibleContainerCompIconSmall />,
117+
columnLayout: <IconCompIconSmall />,
118118
comment: <CommentCompIconSmall />,
119119
container: <ContainerCompIconSmall />,
120120
controlButton: <IconButtonCompIconSmall />,

0 commit comments

Comments
 (0)