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
90ff8cb
refactor: extract logic to grid plugin
iobuhov Nov 7, 2025
dcb50f0
refactor: rewrite empty placeholder
iobuhov Nov 7, 2025
d7c4740
refactor: separate code in plugin
iobuhov Nov 11, 2025
1b67928
refactor: split select-all feature
iobuhov Nov 13, 2025
ce4e856
style: fix comment
iobuhov Nov 13, 2025
7edf066
style: fix comments & type
iobuhov Nov 13, 2025
2e486a0
fix: update test types
iobuhov Nov 17, 2025
278accf
refactor: rewrite dg pagination
iobuhov Nov 17, 2025
79a13e5
refactor: rewrite grid style to atom
iobuhov Nov 17, 2025
d826956
refactor: rewrite grid body
iobuhov Nov 18, 2025
20bf982
test: add new tests
iobuhov Nov 18, 2025
c27c6a3
test: add grid component test
iobuhov Nov 18, 2025
dbe3a5b
refactor: rewire grid header props
iobuhov Nov 18, 2025
668cc17
refactor: deprecate dg basic data
iobuhov Nov 18, 2025
6adf58d
refactor: rewrite dg rows renderer
iobuhov Nov 18, 2025
ddb6f5b
refactor: remove props from grid
iobuhov Nov 20, 2025
39fd254
refactor: rewrite export widget
iobuhov Nov 20, 2025
629ef25
refactor: rename Cell to DataCell
iobuhov Nov 20, 2025
5d9554e
refactor: add new select actions provider
iobuhov Nov 20, 2025
20565c6
refactor: migrate to brandi
iobuhov Nov 21, 2025
fec2fe3
fix: add observer
iobuhov Nov 21, 2025
e3a4e71
fix: change typings
iobuhov Nov 24, 2025
0656cdc
refactor: rewrite header tests
iobuhov Nov 25, 2025
81f2928
refactor: apply feedback
iobuhov Nov 25, 2025
d2de342
fix: update snapshots
iobuhov Nov 25, 2025
e430570
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