Skip to content

Commit 7fbe869

Browse files
authored
Merge pull request #1644 from lowcoder-org/list-comp-sortable
Added sorting feature in list/grid components
2 parents c75d935 + fd96d10 commit 7fbe869

File tree

4 files changed

+138
-37
lines changed

4 files changed

+138
-37
lines changed

Diff for: client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx

+129-34
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { default as Pagination } from "antd/es/pagination";
22
import { EditorContext } from "comps/editorState";
33
import { BackgroundColorContext } from "comps/utils/backgroundColorContext";
4-
import _ from "lodash";
4+
import _, { findIndex } from "lodash";
55
import { ConstructorToView, deferAction } from "lowcoder-core";
6-
import { HintPlaceHolder, ScrollBar, pageItemRender } from "lowcoder-design";
6+
import { DragIcon, HintPlaceHolder, ScrollBar, pageItemRender } from "lowcoder-design";
77
import { RefObject, useContext, createContext, useMemo, useRef, useEffect } from "react";
88
import ReactResizeDetector from "react-resize-detector";
99
import styled from "styled-components";
@@ -22,6 +22,10 @@ import { useMergeCompStyles } from "@lowcoder-ee/util/hooks";
2222
import { childrenToProps } from "@lowcoder-ee/comps/generators/multi";
2323
import { AnimationStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants";
2424
import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils";
25+
import { DndContext } from "@dnd-kit/core";
26+
import { SortableContext, useSortable } from "@dnd-kit/sortable";
27+
import { CSS } from "@dnd-kit/utilities";
28+
import { JSONObject } from "@lowcoder-ee/index.sdk";
2529

2630
const ListViewWrapper = styled.div<{ $style: any; $paddingWidth: string,$animationStyle:AnimationStyleType }>`
2731
height: 100%;
@@ -63,6 +67,22 @@ const ListOrientationWrapper = styled.div<{
6367
flex-direction: ${(props) => (props.$isHorizontal ? "row" : "column")};
6468
`;
6569

70+
const StyledDragIcon = styled(DragIcon)`
71+
height: 16px;
72+
width: 16px;
73+
color: #8b8fa3;
74+
75+
&:hover {
76+
cursor: grab;
77+
outline: none;
78+
}
79+
80+
&:focus {
81+
cursor: grab;
82+
outline: none;
83+
}
84+
`;
85+
6686
type MinHorizontalWidthContextType = {
6787
horizontalWidth: string,
6888
minHorizontalWidth?: string,
@@ -73,19 +93,48 @@ const MinHorizontalWidthContext = createContext<MinHorizontalWidthContextType>({
7393
minHorizontalWidth: '100px',
7494
});
7595

76-
const ContainerInListView = (props: ContainerBaseProps ) => {
96+
const ContainerInListView = (props: ContainerBaseProps & {itemIdx: number, enableSorting?: boolean} ) => {
7797
const {
7898
horizontalWidth,
7999
minHorizontalWidth
80100
} = useContext(MinHorizontalWidthContext);
81101

102+
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
103+
id: String(props.itemIdx),
104+
});
105+
106+
if (!props.enableSorting) {
107+
return (
108+
<div
109+
style={{
110+
width: horizontalWidth,
111+
minWidth: minHorizontalWidth || '0px',
112+
}}
113+
>
114+
<InnerGrid
115+
{...props}
116+
emptyRows={15}
117+
containerPadding={[4, 4]}
118+
hintPlaceholder={HintPlaceHolder}
119+
/>
120+
</div>
121+
)
122+
}
123+
82124
return (
83125
<div
126+
ref={setNodeRef}
84127
style={{
85128
width: horizontalWidth,
86129
minWidth: minHorizontalWidth || '0px',
130+
transform: CSS.Transform.toString(transform),
131+
transition,
132+
display: 'flex',
133+
flexWrap: 'nowrap',
134+
alignItems: 'center',
87135
}}
88136
>
137+
{<StyledDragIcon {...attributes} {...listeners} />}
89138
<InnerGrid
90139
{...props}
91140
emptyRows={15}
@@ -107,6 +156,7 @@ type ListItemProps = {
107156
unMountFn?: () => void;
108157
minHorizontalWidth?: string;
109158
horizontalWidth: string;
159+
enableSorting?: boolean;
110160
};
111161

112162
function ListItem({
@@ -122,6 +172,7 @@ function ListItem({
122172
scrollContainerRef,
123173
minHeight,
124174
horizontalGridCells,
175+
enableSorting,
125176
} = props;
126177

127178
// disable the unmount function to save user's state with pagination
@@ -133,35 +184,37 @@ function ListItem({
133184
// }, []);
134185

135186
return (
136-
<MinHorizontalWidthContext.Provider
137-
value={{
138-
horizontalWidth,
139-
minHorizontalWidth
187+
<MinHorizontalWidthContext.Provider
188+
value={{
189+
horizontalWidth,
190+
minHorizontalWidth
191+
}}
192+
>
193+
<ContainerInListView
194+
itemIdx={itemIdx}
195+
layout={containerProps.layout}
196+
items={gridItemCompToGridItems(containerProps.items)}
197+
horizontalGridCells={horizontalGridCells}
198+
positionParams={containerProps.positionParams}
199+
// all layout changes should only reflect on the commonContainer
200+
dispatch={itemIdx === offset ? containerProps.dispatch : _.noop}
201+
style={{
202+
height: "100%",
203+
// in case of horizontal mode, minHorizontalWidth is 0px
204+
width: minHorizontalWidth || '100%',
205+
backgroundColor: "transparent",
206+
// flex: "auto",
140207
}}
141-
>
142-
<ContainerInListView
143-
layout={containerProps.layout}
144-
items={gridItemCompToGridItems(containerProps.items)}
145-
horizontalGridCells={horizontalGridCells}
146-
positionParams={containerProps.positionParams}
147-
// all layout changes should only reflect on the commonContainer
148-
dispatch={itemIdx === offset ? containerProps.dispatch : _.noop}
149-
style={{
150-
height: "100%",
151-
// in case of horizontal mode, minHorizontalWidth is 0px
152-
width: minHorizontalWidth || '100%',
153-
backgroundColor: "transparent",
154-
// flex: "auto",
155-
}}
156-
autoHeight={autoHeight}
157-
isDroppable={itemIdx === offset}
158-
isDraggable={itemIdx === offset}
159-
isResizable={itemIdx === offset}
160-
isSelectable={itemIdx === offset}
161-
scrollContainerRef={scrollContainerRef}
162-
overflow={"hidden"}
163-
minHeight={minHeight}
164-
enableGridLines={true}
208+
autoHeight={autoHeight}
209+
isDroppable={itemIdx === offset}
210+
isDraggable={itemIdx === offset}
211+
isResizable={itemIdx === offset}
212+
isSelectable={itemIdx === offset}
213+
scrollContainerRef={scrollContainerRef}
214+
overflow={"hidden"}
215+
minHeight={minHeight}
216+
enableGridLines={true}
217+
enableSorting={enableSorting}
165218
/>
166219
</MinHorizontalWidthContext.Provider>
167220
);
@@ -190,6 +243,7 @@ export function ListView(props: Props) {
190243
() => getData(children.noOfRows.getView()),
191244
[children.noOfRows]
192245
);
246+
const listData = useMemo(() => children.listData.getView(), [children.listData]);
193247
const horizontalGridCells = useMemo(() => children.horizontalGridCells.getView(), [children.horizontalGridCells]);
194248
const autoHeight = useMemo(() => children.autoHeight.getView(), [children.autoHeight]);
195249
const showHorizontalScrollbar = useMemo(() => children.showHorizontalScrollbar.getView(), [children.showHorizontalScrollbar]);
@@ -213,6 +267,13 @@ export function ListView(props: Props) {
213267
total,
214268
};
215269
}, [children.pagination, totalCount]);
270+
271+
const enableSorting = useMemo(() => children.enableSorting.getView(), [children.enableSorting]);
272+
273+
useEffect(() => {
274+
children.listData.dispatchChangeValueAction(data);
275+
}, [JSON.stringify(data)]);
276+
216277
const style = children.style.getView();
217278
const animationStyle = children.animationStyle.getView();
218279

@@ -229,6 +290,7 @@ export function ListView(props: Props) {
229290
// log.log("List. listHeight: ", listHeight, " minHeight: ", minHeight);
230291
const renders = _.range(0, noOfRows).map((rowIdx) => {
231292
// log.log("renders. i: ", i, "containerProps: ", containerProps, " text: ", Object.values(containerProps.items as Record<string, any>)[0].children.comp.children.text);
293+
const items = _.range(0, noOfColumns);
232294
const render = (
233295
<div
234296
key={rowIdx}
@@ -238,7 +300,7 @@ export function ListView(props: Props) {
238300
}}
239301
>
240302
<FlexWrapper>
241-
{_.range(0, noOfColumns).map((colIdx) => {
303+
{items.map((colIdx) => {
242304
const itemIdx = rowIdx * noOfColumns + colIdx + pageInfo.offset;
243305
if (
244306
itemIdx >= pageInfo.total ||
@@ -250,7 +312,7 @@ export function ListView(props: Props) {
250312
const containerProps = containerFn(
251313
{
252314
[itemIndexName]: itemIdx,
253-
[itemDataName]: getCurrentItemParams(data, itemIdx)
315+
[itemDataName]: getCurrentItemParams(listData as JSONObject[], itemIdx)
254316
},
255317
String(itemIdx)
256318
).getView();
@@ -259,6 +321,7 @@ export function ListView(props: Props) {
259321
deferAction(ContextContainerComp.batchDeleteAction([String(itemIdx)]))
260322
);
261323
};
324+
262325
return (
263326
<ListItem
264327
key={itemIdx}
@@ -272,12 +335,14 @@ export function ListView(props: Props) {
272335
unMountFn={unMountFn}
273336
horizontalWidth={`${100 / noOfColumns}%`}
274337
minHorizontalWidth={horizontal ? minHorizontalWidth : undefined}
338+
enableSorting={enableSorting}
275339
/>
276340
);
277341
})}
278342
</FlexWrapper>
279343
</div>
280344
);
345+
281346
return render;
282347
});
283348

@@ -289,6 +354,23 @@ export function ListView(props: Props) {
289354

290355
useMergeCompStyles(childrenProps, comp.dispatch);
291356

357+
const handleDragEnd = (e: { active: { id: string }; over: { id: string } | null }) => {
358+
if (!e.over) {
359+
return;
360+
}
361+
const fromIndex = Number(e.active.id);
362+
const toIndex = Number(e.over.id);
363+
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
364+
return;
365+
}
366+
367+
const newData = [...listData];
368+
const [movedItem] = newData.splice(fromIndex, 1);
369+
newData.splice(toIndex, 0, movedItem);
370+
371+
children.listData.dispatchChangeValueAction(newData);
372+
};
373+
292374
// log.debug("renders: ", renders);
293375
return (
294376
<BackgroundColorContext.Provider value={style.background}>
@@ -306,7 +388,20 @@ export function ListView(props: Props) {
306388
$isGrid={noOfColumns > 1}
307389
$autoHeight={autoHeight}
308390
>
309-
{renders}
391+
{!enableSorting
392+
? renders
393+
: (
394+
<DndContext onDragEnd={handleDragEnd}>
395+
<SortableContext
396+
items={
397+
_.range(0, totalCount).map((colIdx) => String(colIdx))
398+
}
399+
>
400+
{renders}
401+
</SortableContext>
402+
</DndContext>
403+
)
404+
}
310405
</ListOrientationWrapper>
311406
)}
312407
>

Diff for: client/packages/lowcoder/src/comps/comps/listViewComp/listViewComp.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
withFunction,
3030
WrapContextNodeV2,
3131
} from "lowcoder-core";
32-
import { JSONValue } from "util/jsonTypes";
32+
import { JSONArray, JSONValue } from "util/jsonTypes";
3333
import { depthEqual, lastValueIfEqual, shallowEqual } from "util/objectUtils";
3434
import { CompTree, getAllCompItems, IContainer } from "../containerBase";
3535
import { SimpleContainerComp, toSimpleContainerData } from "../containerBase/simpleContainerComp";
@@ -43,6 +43,7 @@ import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl";
4343

4444
const childrenMap = {
4545
noOfRows: withIsLoadingMethod(NumberOrJSONObjectArrayControl), // FIXME: migrate "noOfRows" to "data"
46+
listData: stateComp<JSONArray>([]),
4647
noOfColumns: withDefault(NumberControl, 1),
4748
itemIndexName: withDefault(StringControl, "i"),
4849
itemDataName: withDefault(StringControl, "currentItem"),
@@ -60,6 +61,7 @@ const childrenMap = {
6061
animationStyle: styleControl(AnimationStyle, 'animationStyle'),
6162
horizontal: withDefault(BoolControl, false),
6263
minHorizontalWidth: withDefault(RadiusControl, '100px'),
64+
enableSorting: withDefault(BoolControl, false),
6365
};
6466

6567
const ListViewTmpComp = new UICompBuilder(childrenMap, () => <></>)
@@ -116,7 +118,7 @@ export class ListViewImplComp extends ListViewTmpComp implements IContainer {
116118
const { itemCount } = getData(this.children.noOfRows.getView());
117119
const itemIndexName = this.children.itemIndexName.getView();
118120
const itemDataName = this.children.itemDataName.getView();
119-
const dataExposingNode = this.children.noOfRows.exposingNode();
121+
const dataExposingNode = this.children.listData.exposingNode();
120122
const containerComp = this.children.container;
121123
// for each container expose each comps with params
122124
const exposingRecord = _(_.range(0, itemCount))

Diff for: client/packages/lowcoder/src/comps/comps/listViewComp/listViewPropertyView.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export function listPropertyView(compType: ListCompType) {
5757
<Section name={sectionNames.interaction}>
5858
{hiddenPropertyView(children)}
5959
{showDataLoadingIndicatorsPropertyView(children)}
60+
{children.enableSorting.propertyView({
61+
label: trans('listView.enableSorting'),
62+
})}
6063
</Section>
6164
)}
6265

Diff for: client/packages/lowcoder/src/i18n/locales/en.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2598,7 +2598,8 @@ export const en = {
25982598
"itemDataNameDesc": "The Variable Name Referring to the Item's Data Object, Default as {default}",
25992599
"itemsDesc": "Exposing Data of Components in List",
26002600
"dataDesc": "The JSON Data Used in the Current List",
2601-
"dataTooltip": "If You just Set a Number, This Field Will Be Regarded as Row Count, and the Data Will Be Regarded as Empty."
2601+
"dataTooltip": "If You just Set a Number, This Field Will Be Regarded as Row Count, and the Data Will Be Regarded as Empty.",
2602+
"enableSorting": "Allow Sorting"
26022603
},
26032604
"navigation": {
26042605
"addText": "Add Submenu Item",

0 commit comments

Comments
 (0)