Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
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
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