Skip to content

WIP: Add column based layout #829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
import { default as Row } from "antd/es/row";
import { default as Col } from "antd/es/col";
import { JSONObject, JSONValue } from "util/jsonTypes";
import { CompAction, CompActionTypes, deleteCompAction, wrapChildAction } from "lowcoder-core";
import { DispatchType, RecordConstructorToView, wrapDispatch } from "lowcoder-core";
import { AutoHeightControl } from "comps/controls/autoHeightControl";
import { ColumnOptionControl } from "comps/controls/optionsControl";
import { styleControl } from "comps/controls/styleControl";
import {
ResponsiveLayoutRowStyle,
ResponsiveLayoutRowStyleType,
ResponsiveLayoutColStyleType,
ResponsiveLayoutColStyle
} from "comps/controls/styleControlConstants";
import { sameTypeMap, UICompBuilder, withDefault } from "comps/generators";
import { addMapChildAction } from "comps/generators/sameTypeMap";
import { NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing";
import { NameGenerator } from "comps/utils";
import { Section, controlItem, sectionNames } from "lowcoder-design";
import { HintPlaceHolder } from "lowcoder-design";
import _ from "lodash";
import styled from "styled-components";
import { IContainer } from "../containerBase/iContainer";
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
import { CompTree, mergeCompTrees } from "../containerBase/utils";
import {
ContainerBaseProps,
gridItemCompToGridItems,
InnerGrid,
} from "../containerComp/containerView";
import { BackgroundColorContext } from "comps/utils/backgroundColorContext";
import { trans } from "i18n";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import { BoolControl } from "comps/controls/boolControl";
import { BoolCodeControl, NumberControl, StringControl } from "comps/controls/codeControl";

import { useContext } from "react";
import { EditorContext } from "comps/editorState";

import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils";
import { DisabledContext } from "comps/generators/uiCompBuilder";


const ColWrapper = styled(Col)<{
$style: ResponsiveLayoutColStyleType,
$minWidth?: string,
$matchColumnsHeight: boolean,
}>`
> div {
height: ${(props) => props.$matchColumnsHeight ? '100%' : 'auto'};
}
`;

const childrenMap = {
disabled: BoolCodeControl,
columns: ColumnOptionControl,
containers: withDefault(sameTypeMap(SimpleContainerComp), {
0: { view: {}, layout: {} },
1: { view: {}, layout: {} },
}),
autoHeight: AutoHeightControl,
matchColumnsHeight: withDefault(BoolControl, true),
templateRows: withDefault(StringControl, "1fr"),
rowGap: withDefault(StringControl, "20px"),
templateColumns: withDefault(StringControl, "1fr 1fr"),
columnGap: withDefault(StringControl, "20px"),
columnStyle: withDefault(styleControl(ResponsiveLayoutColStyle), {})
};

type ViewProps = RecordConstructorToView<typeof childrenMap>;
type ColumnLayoutProps = ViewProps & { dispatch: DispatchType };
type ColumnContainerProps = Omit<ContainerBaseProps, 'style'> & {
style: ResponsiveLayoutColStyleType,
}

const ColumnContainer = (props: ColumnContainerProps) => {
return (
<InnerGrid
{...props}
emptyRows={15}
hintPlaceholder={HintPlaceHolder}
radius={"0"}
style={props.style}
enableGridLines={false}
/>
);
};


const ColumnLayout = (props: ColumnLayoutProps) => {
let {
columns,
containers,
dispatch,
matchColumnsHeight,
templateRows,
rowGap,
templateColumns,
columnGap,
columnStyle,
} = props;

return (
<BackgroundColorContext.Provider value={"none"}>
<DisabledContext.Provider value={props.disabled}>
<div style={{height: '100%', backgroundColor: "pink"}}>
<div style={{display: "grid", gridTemplateColumns: templateColumns, columnGap, gridTemplateRows: templateRows, rowGap}}>
{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 columnCustomStyle = {
margin: "0",
padding: !_.isEmpty(column.padding) ? column.padding : "0",
radius: "0",
border: "1px dashed pink", // `${!_.isEmpty(column.border) ? column.border : columnStyle.border}`,
background: !_.isEmpty(column.background) ? column.background : columnStyle.background,
overflow: "hidden",
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%)",
backgroundClip: "content-box, padding-box",

}
const noOfColumns = columns.length;
let backgroundStyle = columnCustomStyle.background;
if(!_.isEmpty(column.backgroundImage)) {
backgroundStyle = `center / cover url('${column.backgroundImage}') no-repeat, ${backgroundStyle}`;
}
return (
<ColWrapper
key={id}
$style={columnCustomStyle}
$minWidth={column.minWidth}
$matchColumnsHeight={matchColumnsHeight}
>
<ColumnContainer
layout={containerProps.layout.getView()}
items={gridItemCompToGridItems(containerProps.items.getView())}
positionParams={containerProps.positionParams.getView()}
dispatch={childDispatch}
autoHeight={props.autoHeight}
style={{
...columnCustomStyle,
background: backgroundStyle,
}}
/>
</ColWrapper>
)
})
}
</div>
</div>
</DisabledContext.Provider>
</BackgroundColorContext.Provider>
);
};

export const ResponsiveLayoutBaseComp = (function () {
return new UICompBuilder(childrenMap, (props, dispatch) => {
return (
<ColumnLayout {...props} dispatch={dispatch} />
);
})
.setPropertyViewFn((children) => {
return (
<>
<Section name={sectionNames.basic}>
{children.columns.propertyView({
title: trans("responsiveLayout.column"),
newOptionLabel: "Column",
})}
{children.templateColumns.propertyView({label: "Column Definition"})}
{children.templateRows.propertyView({label: "Row Definition"})}
</Section>

{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && (
<Section name={sectionNames.interaction}>
{disabledPropertyView(children)}
{hiddenPropertyView(children)}
</Section>
)}

{["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (
<>
<Section name={sectionNames.layout}>
{children.autoHeight.getPropertyView()}
</Section>
<Section name={trans("responsiveLayout.columnsLayout")}>
{children.matchColumnsHeight.propertyView({
label: trans("responsiveLayout.matchColumnsHeight")
})}
{controlItem({}, (
<div style={{ marginTop: '8px' }}>
{trans("responsiveLayout.columnsSpacing")}
</div>
))}
{children.columnGap.propertyView({label: "Column Gap"})}
{children.rowGap.propertyView({label: "Row Gap"})}
</Section>
</>
)}
</>
);
})
.build();
})();

class ColumnLayoutImplComp extends ResponsiveLayoutBaseComp implements IContainer {
private syncContainers(): this {
const columns = this.children.columns.getView();
const ids: Set<string> = new Set(columns.map((column) => String(column.id)));
let containers = this.children.containers.getView();
// delete
const actions: CompAction[] = [];
Object.keys(containers).forEach((id) => {
if (!ids.has(id)) {
// log.debug("syncContainers delete. ids=", ids, " id=", id);
actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction())));
}
});
// new
ids.forEach((id) => {
if (!containers.hasOwnProperty(id)) {
// log.debug("syncContainers new containers: ", containers, " id: ", id);
actions.push(
wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} }))
);
}
});
// log.debug("syncContainers. actions: ", actions);
let instance = this;
actions.forEach((action) => {
instance = instance.reduce(action);
});
return instance;
}

override reduce(action: CompAction): this {
const columns = this.children.columns.getView();
if (action.type === CompActionTypes.CUSTOM) {
const value = action.value as JSONObject;
if (value.type === "push") {
const itemValue = value.value as JSONObject;
if (_.isEmpty(itemValue.key)) itemValue.key = itemValue.label;
action = {
...action,
value: {
...value,
value: { ...itemValue },
},
} as CompAction;
}
const { path } = action;
if (value.type === "delete" && path[0] === 'columns' && columns.length <= 1) {
messageInstance.warning(trans("responsiveLayout.atLeastOneColumnError"));
// at least one column
return this;
}
}
// log.debug("before super reduce. action: ", action);
let newInstance = super.reduce(action);
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
// Need eval to get the value in StringControl
newInstance = newInstance.syncContainers();
}
// log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
return newInstance;
}

realSimpleContainer(key?: string): SimpleContainerComp | undefined {
return Object.values(this.children.containers.children).find((container) =>
container.realSimpleContainer(key)
);
}

getCompTree(): CompTree {
const containerMap = this.children.containers.getView();
const compTrees = Object.values(containerMap).map((container) => container.getCompTree());
return mergeCompTrees(compTrees);
}

findContainer(key: string): IContainer | undefined {
const containerMap = this.children.containers.getView();
for (const container of Object.values(containerMap)) {
const foundContainer = container.findContainer(key);
if (foundContainer) {
return foundContainer === container ? this : foundContainer;
}
}
return undefined;
}

getPasteValue(nameGenerator: NameGenerator): JSONValue {
const containerMap = this.children.containers.getView();
const containerPasteValueMap = _.mapValues(containerMap, (container) =>
container.getPasteValue(nameGenerator)
);

return { ...this.toJsonValue(), containers: containerPasteValueMap };
}

override autoHeight(): boolean {
return this.children.autoHeight.getView();
}
}

export const ColumnLayoutComp = withExposingConfigs(
ColumnLayoutImplComp,
[ NameConfigHidden]
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ColumnLayoutComp } from "./columnLayout";
17 changes: 17 additions & 0 deletions client/packages/lowcoder/src/comps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,23 @@ export var uiCompMap: Registry = {
defaultDataFnName: "defaultPageLayoutData",
defaultDataFnPath: "comps/tableComp/mockTableComp",
},
columnLayout: {
name: "Column Layout",
enName: "Column Layout",
description: trans("uiComp.responsiveLayoutCompDesc"),
categories: ["layout"],
icon: ResponsiveLayoutCompIcon,
keywords: trans("uiComp.responsiveLayoutCompKeywords"),
lazyLoad: true,
compName: 'ColumnLayoutComp',
compPath: 'comps/columnLayout/index',
withoutLoading: true,
layoutInfo: {
w: 24,
h: 25,
delayCollision: true,
},
},
floatTextContainer: {
name: trans("uiComp.floatTextContainerCompName"),
enName: "Container",
Expand Down
1 change: 1 addition & 0 deletions client/packages/lowcoder/src/comps/uiCompRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export type UICompType =
| "sunburstChart"
| "themeriverChart"
| "basicChart"
| "columnLayout"
;


Expand Down
4 changes: 2 additions & 2 deletions client/packages/lowcoder/src/pages/editor/editorConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ import {
HillchartCompIconSmall,
TurnstileCaptchaCompIconSmall,
PivotTableCompIconSmall,
GraphChartCompIconSmall

GraphChartCompIconSmall,
} from "lowcoder-design";

export const CompStateIcon: {
Expand All @@ -115,6 +114,7 @@ export const CompStateIcon: {
chart: <ChartCompIconSmall />,
checkbox: <CheckboxCompIconSmall />,
collapsibleContainer: <CollapsibleContainerCompIconSmall />,
columnLayout: <IconCompIconSmall />,
comment: <CommentCompIconSmall />,
container: <ContainerCompIconSmall />,
controlButton: <IconButtonCompIconSmall />,
Expand Down
Loading