diff --git a/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx b/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx index 71503d0c2..826ff0e38 100644 --- a/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx +++ b/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx @@ -1,9 +1,9 @@ import { default as Pagination } from "antd/es/pagination"; import { EditorContext } from "comps/editorState"; import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; -import _ from "lodash"; +import _, { findIndex } from "lodash"; import { ConstructorToView, deferAction } from "lowcoder-core"; -import { HintPlaceHolder, ScrollBar, pageItemRender } from "lowcoder-design"; +import { DragIcon, HintPlaceHolder, ScrollBar, pageItemRender } from "lowcoder-design"; import { RefObject, useContext, createContext, useMemo, useRef, useEffect } from "react"; import ReactResizeDetector from "react-resize-detector"; import styled from "styled-components"; @@ -22,6 +22,10 @@ import { useMergeCompStyles } from "@lowcoder-ee/util/hooks"; import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; import { AnimationStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; +import { DndContext } from "@dnd-kit/core"; +import { SortableContext, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { JSONObject } from "@lowcoder-ee/index.sdk"; const ListViewWrapper = styled.div<{ $style: any; $paddingWidth: string,$animationStyle:AnimationStyleType }>` height: 100%; @@ -63,6 +67,22 @@ const ListOrientationWrapper = styled.div<{ flex-direction: ${(props) => (props.$isHorizontal ? "row" : "column")}; `; +const StyledDragIcon = styled(DragIcon)` + height: 16px; + width: 16px; + color: #8b8fa3; + + &:hover { + cursor: grab; + outline: none; + } + + &:focus { + cursor: grab; + outline: none; + } +`; + type MinHorizontalWidthContextType = { horizontalWidth: string, minHorizontalWidth?: string, @@ -73,19 +93,48 @@ const MinHorizontalWidthContext = createContext({ minHorizontalWidth: '100px', }); -const ContainerInListView = (props: ContainerBaseProps ) => { +const ContainerInListView = (props: ContainerBaseProps & {itemIdx: number, enableSorting?: boolean} ) => { const { horizontalWidth, minHorizontalWidth } = useContext(MinHorizontalWidthContext); + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ + id: String(props.itemIdx), + }); + + if (!props.enableSorting) { + return ( +
+ +
+ ) + } + return (
+ {} void; minHorizontalWidth?: string; horizontalWidth: string; + enableSorting?: boolean; }; function ListItem({ @@ -122,6 +172,7 @@ function ListItem({ scrollContainerRef, minHeight, horizontalGridCells, + enableSorting, } = props; // disable the unmount function to save user's state with pagination @@ -133,35 +184,37 @@ function ListItem({ // }, []); return ( - + - ); @@ -190,6 +243,7 @@ export function ListView(props: Props) { () => getData(children.noOfRows.getView()), [children.noOfRows] ); + const listData = useMemo(() => children.listData.getView(), [children.listData]); const horizontalGridCells = useMemo(() => children.horizontalGridCells.getView(), [children.horizontalGridCells]); const autoHeight = useMemo(() => children.autoHeight.getView(), [children.autoHeight]); const showHorizontalScrollbar = useMemo(() => children.showHorizontalScrollbar.getView(), [children.showHorizontalScrollbar]); @@ -213,6 +267,13 @@ export function ListView(props: Props) { total, }; }, [children.pagination, totalCount]); + + const enableSorting = useMemo(() => children.enableSorting.getView(), [children.enableSorting]); + + useEffect(() => { + children.listData.dispatchChangeValueAction(data); + }, [JSON.stringify(data)]); + const style = children.style.getView(); const animationStyle = children.animationStyle.getView(); @@ -229,6 +290,7 @@ export function ListView(props: Props) { // log.log("List. listHeight: ", listHeight, " minHeight: ", minHeight); const renders = _.range(0, noOfRows).map((rowIdx) => { // log.log("renders. i: ", i, "containerProps: ", containerProps, " text: ", Object.values(containerProps.items as Record)[0].children.comp.children.text); + const items = _.range(0, noOfColumns); const render = (
- {_.range(0, noOfColumns).map((colIdx) => { + {items.map((colIdx) => { const itemIdx = rowIdx * noOfColumns + colIdx + pageInfo.offset; if ( itemIdx >= pageInfo.total || @@ -250,7 +312,7 @@ export function ListView(props: Props) { const containerProps = containerFn( { [itemIndexName]: itemIdx, - [itemDataName]: getCurrentItemParams(data, itemIdx) + [itemDataName]: getCurrentItemParams(listData as JSONObject[], itemIdx) }, String(itemIdx) ).getView(); @@ -259,6 +321,7 @@ export function ListView(props: Props) { deferAction(ContextContainerComp.batchDeleteAction([String(itemIdx)])) ); }; + return ( ); })}
); + return render; }); @@ -289,6 +354,23 @@ export function ListView(props: Props) { useMergeCompStyles(childrenProps, comp.dispatch); + const handleDragEnd = (e: { active: { id: string }; over: { id: string } | null }) => { + if (!e.over) { + return; + } + const fromIndex = Number(e.active.id); + const toIndex = Number(e.over.id); + if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) { + return; + } + + const newData = [...listData]; + const [movedItem] = newData.splice(fromIndex, 1); + newData.splice(toIndex, 0, movedItem); + + children.listData.dispatchChangeValueAction(newData); + }; + // log.debug("renders: ", renders); return ( @@ -306,7 +388,20 @@ export function ListView(props: Props) { $isGrid={noOfColumns > 1} $autoHeight={autoHeight} > - {renders} + {!enableSorting + ? renders + : ( + + String(colIdx)) + } + > + {renders} + + + ) + } )} > diff --git a/client/packages/lowcoder/src/comps/comps/listViewComp/listViewComp.tsx b/client/packages/lowcoder/src/comps/comps/listViewComp/listViewComp.tsx index 47da5c6f3..00f6807cf 100644 --- a/client/packages/lowcoder/src/comps/comps/listViewComp/listViewComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/listViewComp/listViewComp.tsx @@ -29,7 +29,7 @@ import { withFunction, WrapContextNodeV2, } from "lowcoder-core"; -import { JSONValue } from "util/jsonTypes"; +import { JSONArray, JSONValue } from "util/jsonTypes"; import { depthEqual, lastValueIfEqual, shallowEqual } from "util/objectUtils"; import { CompTree, getAllCompItems, IContainer } from "../containerBase"; import { SimpleContainerComp, toSimpleContainerData } from "../containerBase/simpleContainerComp"; @@ -43,6 +43,7 @@ import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl"; const childrenMap = { noOfRows: withIsLoadingMethod(NumberOrJSONObjectArrayControl), // FIXME: migrate "noOfRows" to "data" + listData: stateComp([]), noOfColumns: withDefault(NumberControl, 1), itemIndexName: withDefault(StringControl, "i"), itemDataName: withDefault(StringControl, "currentItem"), @@ -60,6 +61,7 @@ const childrenMap = { animationStyle: styleControl(AnimationStyle, 'animationStyle'), horizontal: withDefault(BoolControl, false), minHorizontalWidth: withDefault(RadiusControl, '100px'), + enableSorting: withDefault(BoolControl, false), }; const ListViewTmpComp = new UICompBuilder(childrenMap, () => <>) @@ -116,7 +118,7 @@ export class ListViewImplComp extends ListViewTmpComp implements IContainer { const { itemCount } = getData(this.children.noOfRows.getView()); const itemIndexName = this.children.itemIndexName.getView(); const itemDataName = this.children.itemDataName.getView(); - const dataExposingNode = this.children.noOfRows.exposingNode(); + const dataExposingNode = this.children.listData.exposingNode(); const containerComp = this.children.container; // for each container expose each comps with params const exposingRecord = _(_.range(0, itemCount)) diff --git a/client/packages/lowcoder/src/comps/comps/listViewComp/listViewPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/listViewComp/listViewPropertyView.tsx index 6e9a4b865..22e288d73 100644 --- a/client/packages/lowcoder/src/comps/comps/listViewComp/listViewPropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/listViewComp/listViewPropertyView.tsx @@ -57,6 +57,9 @@ export function listPropertyView(compType: ListCompType) {
{hiddenPropertyView(children)} {showDataLoadingIndicatorsPropertyView(children)} + {children.enableSorting.propertyView({ + label: trans('listView.enableSorting'), + })}
)} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index ce47ff709..825761bc3 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2598,7 +2598,8 @@ export const en = { "itemDataNameDesc": "The Variable Name Referring to the Item's Data Object, Default as {default}", "itemsDesc": "Exposing Data of Components in List", "dataDesc": "The JSON Data Used in the Current List", - "dataTooltip": "If You just Set a Number, This Field Will Be Regarded as Row Count, and the Data Will Be Regarded as Empty." + "dataTooltip": "If You just Set a Number, This Field Will Be Regarded as Row Count, and the Data Will Be Regarded as Empty.", + "enableSorting": "Allow Sorting" }, "navigation": { "addText": "Add Submenu Item",