diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx index f94b17816..bde90d3ec 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx @@ -128,6 +128,7 @@ export const columnChildrenMap = { dataIndex: valueComp(""), hide: BoolControl, sortable: BoolControl, + filterable: withDefault(BoolControl, false), width: NumberControl, autoWidth: dropdownControl(columnWidthOptions, "auto"), render: RenderComp, @@ -270,6 +271,9 @@ const ColumnPropertyView = React.memo(({ {comp.children.sortable.propertyView({ label: trans("table.sortable"), })} + {comp.children.filterable.propertyView({ + label: trans("table.filterable"), + })} {comp.children.hide.propertyView({ label: trans("prop.hide"), })} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.test.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.test.tsx index 9146a812f..bb02a33ad 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.test.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.test.tsx @@ -186,6 +186,10 @@ test("test table data transform", () => { ) ); getAndExpectTableData(2, comp); + comp = evalAndReduce(comp.reduce(comp.changeChildAction("headerFilters", { name: ["gg2"] }))); + ({ transformedData } = getAndExpectTableData(1, comp)); + expect(transformedData.map((d: any) => d["name"])).toEqual(["gg2"]); + comp = evalAndReduce(comp.reduce(comp.changeChildAction("headerFilters", {}))); // filter comp = evalAndReduce( comp.reduce( diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index 52b1acf4a..ac77c38e2 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -3,6 +3,7 @@ import { getPageSize } from "comps/comps/tableComp/paginationControl"; import { EMPTY_ROW_KEY, TableCompView } from "comps/comps/tableComp/tableCompView"; import { TableFilter } from "comps/comps/tableComp/tableToolbarComp"; import { + applyHeaderFilters, columnHide, ColumnsAggrData, COLUMN_CHILDREN_KEY, @@ -357,12 +358,14 @@ export class TableImplComp extends TableInitComp implements IContainer { data: this.sortDataNode(), searchValue: this.children.searchText.node(), filter: this.children.toolbar.children.filter.node(), + headerFilters: this.children.headerFilters.node(), showFilter: this.children.toolbar.children.showFilter.node(), }; let context = this; const filteredDataNode = withFunction(fromRecord(nodes), (input) => { - const { data, searchValue, filter, showFilter } = input; - const filteredData = filterData(data, searchValue.value, filter, showFilter.value); + const { data, searchValue, filter, headerFilters, showFilter } = input; + const toolbarFilteredData = filterData(data, searchValue.value, filter, showFilter.value); + const filteredData = applyHeaderFilters(toolbarFilteredData, headerFilters); // console.info("filterNode. data: ", data, " filter: ", filter, " filteredData: ", filteredData); // if data is changed on search then trigger event if(Boolean(searchValue.value) && data.length !== filteredData.length) { @@ -1141,6 +1144,18 @@ export const TableComp = withExposingConfigs(TableTmpComp, [ }, trans("table.filterDesc") ), + new DepsConfig( + "headerFilters", + (children) => { + return { + headerFilters: children.headerFilters.node(), + }; + }, + (input) => { + return input.headerFilters; + }, + trans("table.headerFiltersDesc") + ), new DepsConfig( "selectedCell", (children) => { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 389db6952..cbe4c28e2 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -108,6 +108,7 @@ export const TableCompView = React.memo((props: { () => compChildren.dynamicColumnConfig.getView(), [compChildren.dynamicColumnConfig] ); + const headerFilters = useMemo(() => compChildren.headerFilters.getView(), [compChildren.headerFilters]); const columnsAggrData = comp.columnAggrData; const expansion = useMemo(() => compChildren.expansion.getView(), [compChildren.expansion]); const antdColumns = useMemo( @@ -122,6 +123,7 @@ export const TableCompView = React.memo((props: { columnsAggrData, editModeClicks, onEvent, + headerFilters, ), [ columnViews, @@ -132,6 +134,7 @@ export const TableCompView = React.memo((props: { dynamicColumnConfig, columnsAggrData, editModeClicks, + headerFilters, ] ); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx index 4cea24328..cfaaab1fb 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx @@ -242,6 +242,7 @@ const tableChildrenMap = { selection: SelectionControl, pagination: PaginationControl, sort: valueComp>([]), + headerFilters: stateComp>({}), toolbar: TableToolbarComp, showSummary: BoolControl, summaryRows: dropdownControl(summarRowsOptions, "1"), diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index 69948171f..ab600ef77 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -99,6 +99,31 @@ export function filterData( return resultData; } +export function applyHeaderFilters( + data: Array, + headerFilters: Record +) { + if (!headerFilters || Object.keys(headerFilters).length === 0) { + return data; + } + + return data.filter((row) => + Object.entries(headerFilters).every(([columnKey, filterValues]) => { + if (!Array.isArray(filterValues) || filterValues.length === 0) { + return true; + } + + const cellValue = row[columnKey]; + return filterValues.some((filterValue) => { + if (cellValue == null) { + return filterValue == null; + } + return String(cellValue) === String(filterValue); + }); + }) + ); +} + export function sortData( data: Array, columns: Record, // key: dataIndex @@ -291,6 +316,14 @@ export function getColumnsAggr( .uniqBy("text") .value(); } + + res.uniqueValues = _(oriDisplayData) + .map((row) => row[dataIndex]) + .filter((value): value is JSONValue => value !== undefined && value !== null && value !== "") + .uniqWith(_.isEqual) + .slice(0, 100) + .value(); + return res; }); } @@ -338,6 +371,27 @@ export type CustomColumnType = ColumnType & { columnDataTestId?: string; }; +function buildHeaderFilterProps( + dataIndex: string, + filterable: boolean, + uniqueValues: any[], + headerFilters: Record = {} +) { + if (!filterable || uniqueValues.length === 0) { + return {}; + } + + return { + filters: uniqueValues.map((value) => ({ + text: String(value), + value, + })), + filteredValue: headerFilters[dataIndex] ?? null, + filterSearch: true, + filterMultiple: true, + } as const; +} + /** * convert column in raw format into antd format */ @@ -351,6 +405,7 @@ export function columnsToAntdFormat( columnsAggrData: ColumnsAggrData, editMode: string, onTableEvent: (eventName: any) => void, + headerFilters: Record = {}, ): Array> { const customColumns = columns.filter(col => col.isCustom).map(col => col.dataIndex); const initialColumns = getInitialColumns(columnsAggrData, customColumns); @@ -393,10 +448,18 @@ export function columnsToAntdFormat( text: string; status: StatusType; }[]; + const uniqueValues = ((columnsAggrData[column.dataIndex] ?? {}).uniqueValues ?? []) as any[]; + const columnKey = column.dataIndex || `custom-${mIndex}`; const title = renderTitle({ title: column.title, tooltip: column.titleTooltip, editable: column.editable }); + const filterProps = buildHeaderFilterProps( + column.dataIndex, + column.filterable, + uniqueValues, + headerFilters + ); return { - key: `${column.dataIndex}-${mIndex}`, + key: columnKey, title: column.showTitle ? title : '', titleText: column.title, dataIndex: column.dataIndex, @@ -468,6 +531,7 @@ export function columnsToAntdFormat( showSorterTooltip: false, } : {}), + ...filterProps, }; }); } @@ -504,6 +568,16 @@ export function onTableChange( dispatch(changeChildAction("sort", sortValues, true)); onEvent("sortChange"); } + + if (extra.action === "filter") { + const headerFilters = _(filters) + .pickBy((filterValues) => Array.isArray(filterValues) && filterValues.length > 0) + .mapValues((filterValues) => filterValues as any[]) + .value(); + + dispatch(changeChildAction("headerFilters", headerFilters, true)); + onEvent("filterChange"); + } } export function calcColumnWidth(columnKey: string, data: Array) { diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 9258cb98d..7b91aaf8f 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2095,6 +2095,7 @@ export const en = { "showTitle": "Show Title", "showTitleTooltip": "Show/Hide column title in table header", "sortable": "Sortable", + "filterable": "Filterable", "align": "Alignment", "fixedColumn": "Fixed Column", "autoWidth": "Auto Width", @@ -2170,6 +2171,7 @@ export const en = { "displayDataDesc": "Data Displayed in the Current Table", "selectedIndexDesc": "Selected Index in Display Data", "filterDesc": "Table Filtering Parameters", + "headerFiltersDesc": "Header filter selections keyed by column name", "dataDesc": "The JSON Data for the Table", "saveChanges": "Save Changes", "cancelChanges": "Cancel Changes",