Skip to content

[In Progress] [Feat] Table Lite Component #1939

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

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
refactor / add virtualization
  • Loading branch information
iamfaran committed Aug 15, 2025
commit a909c645f44c791cb59dd65be59831ab632e12da
1 change: 1 addition & 0 deletions client/packages/lowcoder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"pigeon-maps": "^0.22.1",
"qrcode.react": "^3.1.0",
"rc-trigger": "^5.3.1",
"rc-virtual-list": "^3.19.1",
"react": "18.3.0",
"react-best-gradient-color-picker": "^3.0.10",
"react-colorful": "^5.5.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ export const columnChildrenMap = {
dataIndex: valueComp<string>(""),
hide: BoolControl,
sortable: BoolControl,
// header filters
filterable: withDefault(BoolControl, false),
filteredValue: stateComp<string[]>([]),
width: NumberControl,
autoWidth: dropdownControl(columnWidthOptions, "auto"),
render: RenderComp,
Expand Down Expand Up @@ -261,6 +264,9 @@ const ColumnPropertyView = React.memo(({
{comp.children.sortable.propertyView({
label: trans("table.sortable"),
})}
{comp.children.filterable.propertyView({
label: "Filterable",
})}
{comp.children.hide.propertyView({
label: trans("prop.hide"),
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
fromRecord,
isMyCustomAction,
RecordNode,
wrapChildAction,
changeChildAction,
} from "lowcoder-core";
import { shallowEqual } from "react-redux";
import { JSONObject, JSONValue } from "util/jsonTypes";
Expand All @@ -27,14 +29,20 @@ const ColumnListTmpComp = list(ColumnComp);
* rowExample is used for code prompts
*/
type RowExampleType = JSONObject | undefined;
type ActionDataType = {

type DataChangedActionType = {
type: "dataChanged";
rowExample: RowExampleType;
doGeneColumn: boolean;
dynamicColumn: boolean;
data: Array<JSONObject>;
};

type SetFilteredValuesActionType = {
type: "setFilteredValues";
values: Record<string, string[]>; // dataIndex -> filtered values
};

export function tableDataRowExample(data: Array<JSONObject>) {
if (!data?.length) {
return undefined;
Expand All @@ -58,7 +66,7 @@ export function tableDataRowExample(data: Array<JSONObject>) {

export class ColumnListComp extends ColumnListTmpComp {
override reduce(action: CompAction): this {
if (isMyCustomAction<ActionDataType>(action, "dataChanged")) {
if (isMyCustomAction<DataChangedActionType>(action, "dataChanged")) {
const rowExample = action.value.rowExample;
const { readOnly } = getReduceContext();
let comp = this;
Expand All @@ -68,9 +76,39 @@ export class ColumnListComp extends ColumnListTmpComp {
}
return comp;
}
if (isMyCustomAction<SetFilteredValuesActionType>(action, "setFilteredValues")) {
const values = action.value.values;
const columnsView = this.getView();
const actions: Array<any> = [];
columnsView.forEach((column, index) => {
const dataIndex = column.getView().dataIndex;
if (values.hasOwnProperty(dataIndex)) {
actions.push(
wrapChildAction(
index + "",
changeChildAction("filteredValue", values[dataIndex] ?? [], false)
)
);
}
});
if (actions.length > 0) {
return this.reduce(this.multiAction(actions));
}
return this;
}
return super.reduce(action);
}

setFilteredValuesAction(values: Record<string, string[]>) {
return customAction<SetFilteredValuesActionType>(
{
type: "setFilteredValues",
values,
},
false
);
}

getChangeSet(filterNewRowsChange?: boolean) {
const changeSet: Record<string, Record<string, JSONValue>> = {};
const columns = this.getView();
Expand Down Expand Up @@ -107,7 +145,7 @@ export class ColumnListComp extends ColumnListTmpComp {
dynamicColumn: boolean;
data: Array<JSONObject>;
}) {
return customAction<ActionDataType>(
return customAction<DataChangedActionType>(
{
type: "dataChanged",
...param,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from "lodash";
import { OB_ROW_ORI_INDEX } from "./tableUtils";
import { SortValue } from "./tableTypes";
import { OB_ROW_ORI_INDEX } from "../tableUtils";
import { SortValue } from "../tableTypes";

export const tableMethodExposings = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import _ from "lodash";
import { fromRecord, Node, RecordNode, ValueAndMsg, withFunction } from "lowcoder-core";
import { lastValueIfEqual, shallowEqual } from "util/objectUtils";
import { JSONObject } from "util/jsonTypes";
import { getPageSize } from "../paginationControl";
import {
getOriDisplayData,
getColumnsAggr,
OB_ROW_ORI_INDEX,
RecordType,
sortData,
tranToTableRecord,
} from "../tableUtils";
import type { SortValue } from "../tableTypes";
import type { TableImplComp } from "../tableComp";

export function buildSortedDataNode(comp: TableImplComp) {
const nodes: {
data: Node<JSONObject[]>;
sort: Node<SortValue[]>;
dataIndexes: RecordNode<Record<string, Node<string>>>;
sortables: RecordNode<Record<string, Node<ValueAndMsg<boolean>>>>;
withParams: RecordNode<_.Dictionary<any>>;
} = {
data: comp.children.data.exposingNode(),
sort: comp.children.sort.node(),
dataIndexes: comp.children.columns.getColumnsNode("dataIndex"),
sortables: comp.children.columns.getColumnsNode("sortable"),
withParams: comp.children.columns.withParamsNode(),
};

const sortedDataNode = withFunction(fromRecord(nodes), (input) => {
const { data, sort, dataIndexes, sortables } = input;
const sortColumns = _(dataIndexes)
.mapValues((dataIndex, idx) => ({ sortable: !!sortables[idx] }))
.mapKeys((sortable, idx) => dataIndexes[idx])
.value();
const dataColumns = _(dataIndexes)
.mapValues((dataIndex, idx) => ({
dataIndex,
render: input.withParams[idx] as any,
}))
.value();

const updatedData: Array<RecordType> = data.map((row, index) => ({
...row,
[OB_ROW_ORI_INDEX]: index + "",
}));

const updatedDataMap: Record<string, RecordType> = {};
updatedData.forEach((row) => {
updatedDataMap[row[OB_ROW_ORI_INDEX]] = row;
});

const originalData = getOriDisplayData(updatedData, 1000, Object.values(dataColumns));
const sorted = sortData(originalData, sortColumns, sort);

const newData = sorted.map((row) => ({
...row,
...updatedDataMap[row[OB_ROW_ORI_INDEX]],
}));
return newData;
});

return lastValueIfEqual(comp, "sortedDataNode", [sortedDataNode, nodes] as const, (a, b) =>
shallowEqual(a[1], b[1])
)[0];
}

export function buildFilteredDataNode(comp: TableImplComp) {
const nodes = {
data: buildSortedDataNode(comp),
};
const filteredDataNode = withFunction(fromRecord(nodes), ({ data }) => {
// No pre-filtering here; AntD header filters are handled internally by Table
return data.map((row) => tranToTableRecord(row, (row as any)[OB_ROW_ORI_INDEX]));
});
return lastValueIfEqual(comp, "filteredDataNode", [filteredDataNode, nodes] as const, (a, b) =>
shallowEqual(a[1], b[1])
)[0];
}

export function buildOriDisplayDataNode(comp: TableImplComp) {
const nodes = {
data: buildFilteredDataNode(comp),
showSizeChanger: comp.children.pagination.children.showSizeChanger.node(),
pageSize: comp.children.pagination.children.pageSize.node(),
pageSizeOptions: comp.children.pagination.children.pageSizeOptions.node(),
changablePageSize: comp.children.pagination.children.changeablePageSize.node(),
withParams: comp.children.columns.withParamsNode(),
dataIndexes: comp.children.columns.getColumnsNode("dataIndex"),
};
const resNode = withFunction(fromRecord(nodes), (input) => {
const columns = _(input.dataIndexes)
.mapValues((dataIndex, idx) => ({
dataIndex,
render: input.withParams[idx],
}))
.value();
const pageSize = getPageSize(
input.showSizeChanger.value,
input.pageSize.value,
input.pageSizeOptions.value,
input.changablePageSize
);
return getOriDisplayData(input.data, pageSize, Object.values(columns));
});
return lastValueIfEqual(comp, "oriDisplayDataNode", [resNode, nodes] as const, (a, b) =>
shallowEqual(a[1], b[1])
)[0];
}

export function buildColumnAggrNode(comp: TableImplComp) {
const nodes = {
oriDisplayData: buildOriDisplayDataNode(comp),
withParams: comp.children.columns.withParamsNode(),
dataIndexes: comp.children.columns.getColumnsNode("dataIndex"),
};
const resNode = withFunction(fromRecord(nodes), (input) => {
const dataIndexWithParamsDict = _(input.dataIndexes)
.mapValues((dataIndex, idx) => input.withParams[idx])
.mapKeys((withParams, idx) => input.dataIndexes[idx])
.value();
const res = getColumnsAggr(input.oriDisplayData, dataIndexWithParamsDict);
return res;
});
return lastValueIfEqual(comp, "columnAggrNode", [resNode, nodes] as const, (a, b) =>
shallowEqual(a[1], b[1])
)[0];
}
Loading
Loading