Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6fe117e
refactor: extract logic to grid plugin
iobuhov Nov 7, 2025
bee7c4d
refactor: rewrite empty placeholder
iobuhov Nov 7, 2025
f5a90cd
refactor: separate code in plugin
iobuhov Nov 11, 2025
80a096e
refactor: split select-all feature
iobuhov Nov 13, 2025
fdc18e4
style: fix comment
iobuhov Nov 13, 2025
dc149a5
style: fix comments & type
iobuhov Nov 13, 2025
b5be8c7
fix: update test types
iobuhov Nov 17, 2025
7111c90
refactor: rewrite dg pagination
iobuhov Nov 17, 2025
ede46ec
refactor: rewrite grid style to atom
iobuhov Nov 17, 2025
dac5199
refactor: rewrite grid body
iobuhov Nov 18, 2025
4fff9c5
test: add new tests
iobuhov Nov 18, 2025
7c92670
test: add grid component test
iobuhov Nov 18, 2025
487a931
refactor: rewire grid header props
iobuhov Nov 18, 2025
6303449
refactor: deprecate dg basic data
iobuhov Nov 18, 2025
510b7c4
refactor: rewrite dg rows renderer
iobuhov Nov 18, 2025
84af1b8
refactor: remove props from grid
iobuhov Nov 20, 2025
f8e6e56
refactor: rewrite export widget
iobuhov Nov 20, 2025
7dd0fa2
refactor: rename Cell to DataCell
iobuhov Nov 20, 2025
7816b42
refactor: add new select actions provider
iobuhov Nov 20, 2025
47bbef0
refactor: migrate to brandi
iobuhov Nov 21, 2025
5bf5622
fix: add observer
iobuhov Nov 21, 2025
55cb868
fix: change typings
iobuhov Nov 24, 2025
11eef55
refactor: rewrite header tests
iobuhov Nov 25, 2025
16db8bc
refactor: apply feedback
iobuhov Nov 25, 2025
03aac61
fix: update snapshots
iobuhov Nov 25, 2025
e3a73dd
fix: prevent mobx warnings
iobuhov Nov 25, 2025
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.1",
"@mendix/pluggable-widgets-tools": "10.21.2",
"@testing-library/react": ">=15.0.6",
"@types/big.js": "^6.2.2",
"@types/node": "~22.14.0",
"@types/react": ">=18.2.36",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ exports[`Dropdown Filter with single instance with single attribute renders corr
<div
class="widget-dropdown-filter-popover"
hidden=""
style="position: fixed; left: 0px; top: 0px; transform: translate(0px, 0px);"
style="position: fixed; left: 0px; top: 0px; transform: translate(0px, 0px); width: 0px;"
>
<div
class="widget-dropdown-filter-menu-slot"
Expand Down
128 changes: 4 additions & 124 deletions packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
Original file line number Diff line number Diff line change
@@ -1,142 +1,22 @@
import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper";
import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController";
import { useSelectionHelper } from "@mendix/widget-plugin-grid/selection";
import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
import { ContainerProvider } from "brandi-react";
import { observer } from "mobx-react-lite";
import { ReactElement, ReactNode, useCallback, useMemo } from "react";
import { ReactElement } from "react";
import { DatagridContainerProps } from "../typings/DatagridProps";
import { Cell } from "./components/Cell";
import { Widget } from "./components/Widget";
import { WidgetHeaderContext } from "./components/WidgetHeaderContext";
import { useDataExport } from "./features/data-export/useDataExport";
import { useCellEventsController } from "./features/row-interaction/CellEventsController";
import { useCheckboxEventsController } from "./features/row-interaction/CheckboxEventsController";
import { LegacyContext } from "./helpers/root-context";
import { useSelectActionHelper } from "./helpers/SelectActionHelper";
import { useDataGridJSActions } from "./helpers/useDataGridJSActions";
import {
useColumnsStore,
useExportProgressService,
useLoaderViewModel,
useMainGate,
usePaginationService
} from "./model/hooks/injection-hooks";
import { useColumnsStore, useExportProgressService } from "./model/hooks/injection-hooks";
import { useDatagridContainer } from "./model/hooks/useDatagridContainer";

const DatagridRoot = observer((props: DatagridContainerProps): ReactElement => {
const gate = useMainGate();
const columnsStore = useColumnsStore();
const paginationService = usePaginationService();
const exportProgress = useExportProgressService();
const loaderVM = useLoaderViewModel();
const items = gate.props.datasource.items ?? [];

const [abortExport] = useDataExport(props, columnsStore, exportProgress);

const selectionHelper = useSelectionHelper(
gate.props.itemSelection,
gate.props.datasource,
props.onSelectionChange,
props.keepSelection ? "always keep" : "always clear"
);

const selectActionHelper = useSelectActionHelper(props, selectionHelper);

const clickActionHelper = useClickActionHelper({
onClickTrigger: props.onClickTrigger,
onClick: props.onClick
});

useDataGridJSActions(selectActionHelper);

const visibleColumnsCount = selectActionHelper.showCheckboxColumn
? columnsStore.visibleColumns.length + 1
: columnsStore.visibleColumns.length;
useDataGridJSActions();

const focusController = useFocusTargetController({
rows: items.length,
columns: visibleColumnsCount,
pageSize: props.pageSize
});

const cellEventsController = useCellEventsController(selectActionHelper, clickActionHelper, focusController);

const checkboxEventsController = useCheckboxEventsController(selectActionHelper, focusController);

return (
<LegacyContext.Provider
value={useConst({
selectionHelper,
selectActionHelper,
cellEventsController,
checkboxEventsController,
focusController
})}
>
<Widget
className={props.class}
CellComponent={Cell}
columnsDraggable={props.columnsDraggable}
columnsFilterable={props.columnsFilterable}
columnsHidable={props.columnsHidable}
columnsResizable={props.columnsResizable}
columnsSortable={props.columnsSortable}
data={items}
emptyPlaceholderRenderer={useCallback(
(renderWrapper: (children: ReactNode) => ReactElement) =>
props.showEmptyPlaceholder === "custom" ? renderWrapper(props.emptyPlaceholder) : <div />,
[props.emptyPlaceholder, props.showEmptyPlaceholder]
)}
filterRenderer={useCallback(
(renderWrapper, columnIndex) => {
const columnFilter = columnsStore.columnFilters[columnIndex];
return renderWrapper(columnFilter.renderFilterWidgets());
},
[columnsStore.columnFilters]
)}
headerTitle={props.filterSectionTitle?.value}
headerContent={
props.filtersPlaceholder && (
<WidgetHeaderContext selectionHelper={selectionHelper}>
{props.filtersPlaceholder}
</WidgetHeaderContext>
)
}
hasMoreItems={props.datasource.hasMoreItems ?? false}
headerWrapperRenderer={useCallback((_columnIndex: number, header: ReactElement) => header, [])}
id={useMemo(() => `DataGrid${generateUUID()}`, [])}
numberOfItems={props.datasource.totalCount}
onExportCancel={abortExport}
page={paginationService.currentPage}
pageSize={props.pageSize}
paginationType={props.pagination}
loadMoreButtonCaption={props.loadMoreButtonCaption?.value}
paging={paginationService.showPagination}
pagingPosition={props.pagingPosition}
showPagingButtons={props.showPagingButtons}
rowClass={useCallback((value: any) => props.rowClass?.get(value)?.value ?? "", [props.rowClass])}
setPage={paginationService.setPage}
styles={props.style}
exporting={exportProgress.inProgress}
processedRows={exportProgress.loaded}
visibleColumns={columnsStore.visibleColumns}
availableColumns={columnsStore.availableColumns}
setIsResizing={(status: boolean) => columnsStore.setIsResizing(status)}
columnsSwap={(moved, [target, placement]) => columnsStore.swapColumns(moved, [target, placement])}
selectActionHelper={selectActionHelper}
cellEventsController={cellEventsController}
checkboxEventsController={checkboxEventsController}
focusController={focusController}
isFirstLoad={loaderVM.isFirstLoad}
isFetchingNextBatch={loaderVM.isFetchingNextBatch}
showRefreshIndicator={loaderVM.showRefreshIndicator}
loadingType={props.loadingType}
columnsLoading={!columnsStore.loaded}
/>
</LegacyContext.Provider>
);
return <Widget onExportCancel={abortExport} />;
});

DatagridRoot.displayName = "DatagridComponent";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps";
import { ObjectItem } from "mendix";
import { FocusEvent, ReactElement } from "react";
import { useLegacyContext } from "../helpers/root-context";
import { useBasicData } from "../model/hooks/injection-hooks";
import { FocusEvent, ReactElement, useMemo } from "react";
import {
useCheckboxEventsHandler,
useDatagridConfig,
useSelectActions,
useTexts
} from "../model/hooks/injection-hooks";
import { CellElement, CellElementProps } from "./CellElement";

export type CheckboxCellProps = CellElementProps & {
Expand All @@ -12,25 +16,27 @@ export type CheckboxCellProps = CellElementProps & {
};

export function CheckboxCell({ item, rowIndex, lastRow, ...rest }: CheckboxCellProps): ReactElement {
const config = useDatagridConfig();
const selectActions = useSelectActions();
const checkboxEventsHandler = useCheckboxEventsHandler();
const { selectRowLabel } = useTexts();
const keyNavProps = useFocusTargetProps<HTMLInputElement>({
columnIndex: 0,
rowIndex
});

const { selectActionHelper, checkboxEventsController } = useLegacyContext();
const { selectRowLabel, gridInteractive } = useBasicData();
return (
<CellElement {...rest} clickable={gridInteractive} className="widget-datagrid-col-select" tabIndex={-1}>
<CellElement {...rest} clickable={config.isInteractive} className="widget-datagrid-col-select" tabIndex={-1}>
<input
checked={selectActionHelper.isSelected(item)}
checked={selectActions.isSelected(item)}
type="checkbox"
tabIndex={keyNavProps.tabIndex}
data-position={keyNavProps["data-position"]}
onChange={stub}
onFocus={lastRow ? scrollParentOnFocus : undefined}
ref={keyNavProps.ref}
aria-label={`${selectRowLabel ?? "Select row"} ${rowIndex + 1}`}
{...checkboxEventsController.getProps(item)}
{...useMemo(() => checkboxEventsHandler.getProps(item), [item, checkboxEventsHandler])}
/>
</CellElement>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
import { If } from "@mendix/widget-plugin-component-kit/If";
import { ThreeStateCheckBox } from "@mendix/widget-plugin-component-kit/ThreeStateCheckBox";
import { SelectionStatus } from "@mendix/widget-plugin-grid/selection";
import { observer } from "mobx-react-lite";
import { Fragment, ReactElement, ReactNode } from "react";
import { useLegacyContext } from "../helpers/root-context";
import { useBasicData } from "../model/hooks/injection-hooks";
import { useDatagridConfig, useSelectActions, useSelectionHelper, useTexts } from "../model/hooks/injection-hooks";

export function CheckboxColumnHeader(): ReactElement {
const { selectActionHelper, selectionHelper } = useLegacyContext();
const { showCheckboxColumn, showSelectAllToggle, onSelectAll } = selectActionHelper;
const { selectAllRowsLabel } = useBasicData();
const { selectAllCheckboxEnabled, checkboxColumnEnabled } = useDatagridConfig();

if (showCheckboxColumn === false) {
if (checkboxColumnEnabled === false) {
return <Fragment />;
}

return (
<div className="th widget-datagrid-col-select" role="columnheader">
{showSelectAllToggle && (
<Checkbox
status={selectionHelper?.type === "Multi" ? selectionHelper.selectionStatus : "none"}
onChange={onSelectAll}
aria-label={selectAllRowsLabel}
/>
)}
<If condition={selectAllCheckboxEnabled}>
<Checkbox />
</If>
</div>
);
}

function Checkbox(props: { status: SelectionStatus; onChange: () => void; "aria-label"?: string }): ReactNode {
if (props.status === "unknown") {
console.error("Data grid 2: don't know how to render column checkbox with selectionStatus=unknown");
const Checkbox = observer(function Checkbox(): ReactNode {
const { selectAllRowsLabel } = useTexts();
const selectionHelper = useSelectionHelper();
const selectActions = useSelectActions();

if (!selectionHelper || selectionHelper.type !== "Multi") {
return null;
}
return (
<ThreeStateCheckBox
value={props.status}
onChange={props.onChange}
aria-label={props["aria-label"] ?? "Select all rows"}
value={selectionHelper.selectionStatus}
onChange={() => selectActions.selectPage()}
aria-label={selectAllRowsLabel || "Select all rows"}
/>
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
import { Container } from "brandi";
import { ContainerProvider } from "brandi-react";
import { PropsWithChildren, ReactNode } from "react";
import { CORE_TOKENS as CORE } from "../model/tokens";
import { GridColumn } from "../typings/GridColumn";

/** Provider to bind & provider column store for children at runtime. */
export function ColumnProvider(props: PropsWithChildren<{ column: GridColumn }>): ReactNode {
const ct = useConst(() => {
const container = new Container();
container.bind(CORE.column).toConstant(props.column);
return container;
});

return <ContainerProvider container={ct}>{props.children}</ContainerProvider>;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps";
import { ObjectItem } from "mendix";
import { computed } from "mobx";
import { observer } from "mobx-react-lite";
import { ReactElement, useMemo } from "react";
import { CellComponentProps } from "../typings/CellComponent";
import { ReactElement, ReactNode, useMemo } from "react";
import { EventsController } from "../typings/CellComponent";
import { GridColumn } from "../typings/GridColumn";
import { CellElement } from "./CellElement";

const component = observer(function Cell(props: CellComponentProps<GridColumn>): ReactElement {
interface DataCellProps {
children?: ReactNode;
className?: string;
column: GridColumn;
item: ObjectItem;
key?: string | number;
rowIndex: number;
columnIndex?: number;
clickable?: boolean;
preview?: boolean;
eventsController: EventsController;
}

export const DataCell = observer(function DataCell(props: DataCellProps): ReactElement {
const keyNavProps = useFocusTargetProps<HTMLDivElement>({
columnIndex: props.columnIndex ?? -1,
rowIndex: props.rowIndex
Expand Down Expand Up @@ -36,6 +50,3 @@ const component = observer(function Cell(props: CellComponentProps<GridColumn>):
</CellElement>
);
});

// Override NamedExoticComponent type
export const Cell = component as (props: CellComponentProps<GridColumn>) => ReactElement;

This file was deleted.

28 changes: 14 additions & 14 deletions packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import classNames from "classnames";
import { ComponentPropsWithoutRef, ReactElement } from "react";

type P = Omit<ComponentPropsWithoutRef<"div">, "role">;

export interface GridProps extends P {
className?: string;
}

export function Grid(props: GridProps): ReactElement {
const { className, style, children, ...rest } = props;
import { observer } from "mobx-react-lite";
import { PropsWithChildren, ReactElement } from "react";
import { useDatagridConfig, useGridStyle } from "../model/hooks/injection-hooks";

export const Grid = observer(function Grid(props: PropsWithChildren): ReactElement {
const config = useDatagridConfig();
const style = useGridStyle().get();
return (
<div className={classNames("widget-datagrid-grid table", className)} role="grid" style={style} {...rest}>
{children}
<div
aria-multiselectable={config.multiselectable}
className={"widget-datagrid-grid table"}
role="grid"
style={style}
>
{props.children}
</div>
);
}
});
Loading
Loading