From f3eba514dd8638e2a0eb27a1bb76575c63b3f104 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:44:58 +0100 Subject: [PATCH 01/19] refactor: migrate event controllers from DI to React Context for simplified architecture --- .../datagrid-web/src/Datagrid.tsx | 57 ++++++++++++++++- .../src/components/RowsRenderer.tsx | 20 ++---- .../row-interaction/CellEventsController.ts | 61 ++++++++----------- .../datagrid-web/src/helpers/root-context.ts | 23 +++++++ .../src/helpers/useDataGridJSActions.ts | 6 +- .../model/containers/Datagrid.container.ts | 9 +-- .../src/model/containers/Root.container.ts | 2 +- .../src/model/hooks/injection-hooks.ts | 4 -- .../datagrid-web/src/model/tokens.ts | 4 +- .../datagrid-web/src/utils/test-utils.tsx | 1 + .../src/selection/select-action-handler.ts | 2 +- 11 files changed, 115 insertions(+), 74 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 3eeddd6be1..0fd98997a9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -1,22 +1,73 @@ +import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; +import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; import { ContainerProvider } from "brandi-react"; import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; import { DatagridContainerProps } from "../typings/DatagridProps"; import { Widget } from "./components/Widget"; 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 { useDataGridJSActions } from "./helpers/useDataGridJSActions"; -import { useColumnsStore, useExportProgressService } from "./model/hooks/injection-hooks"; +import { + useColumnsStore, + useDatagridConfig, + useExportProgressService, + useMainGate, + useSelectActions, + useSelectionHelper +} from "./model/hooks/injection-hooks"; import { useDatagridContainer } from "./model/hooks/useDatagridContainer"; const DatagridRoot = observer((props: DatagridContainerProps): ReactElement => { + const config = useDatagridConfig(); + const gate = useMainGate(); const columnsStore = useColumnsStore(); const exportProgress = useExportProgressService(); + const items = gate.props.datasource.items ?? []; const [abortExport] = useDataExport(props, columnsStore, exportProgress); - useDataGridJSActions(); + const selectionHelper = useSelectionHelper(); - return ; + const selectActionHelper = useSelectActions(); + + const clickActionHelper = useClickActionHelper({ + onClickTrigger: props.onClickTrigger, + onClick: props.onClick + }); + + useDataGridJSActions(selectActionHelper); + + const visibleColumnsCount = config.checkboxColumnEnabled + ? columnsStore.visibleColumns.length + 1 + : columnsStore.visibleColumns.length; + + const focusController = useFocusTargetController({ + rows: items.length, + columns: visibleColumnsCount, + pageSize: props.pageSize + }); + + const cellEventsController = useCellEventsController(selectActionHelper, clickActionHelper, focusController); + + const checkboxEventsController = useCheckboxEventsController(selectActionHelper, focusController); + + return ( + + + + ); }); DatagridRoot.displayName = "DatagridComponent"; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx index 11de6a4e44..65c620c21c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx @@ -1,15 +1,8 @@ import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context"; import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; -import { - useCellEventsHandler, - useColumnsStore, - useDatagridConfig, - useFocusService, - useRowClass, - useRows, - useSelectActions -} from "../model/hooks/injection-hooks"; +import { useLegacyContext } from "../helpers/root-context"; +import { useColumnsStore, useDatagridConfig, useRowClass, useRows } from "../model/hooks/injection-hooks"; import { Row } from "./Row"; export const RowsRenderer = observer(function RowsRenderer(): ReactElement { @@ -17,18 +10,15 @@ export const RowsRenderer = observer(function RowsRenderer(): ReactElement { const config = useDatagridConfig(); const { visibleColumns } = useColumnsStore(); const rowClass = useRowClass(); - const cellEventsController = useCellEventsHandler(); - const focusService = useFocusService(); - const selectActions = useSelectActions(); - + const { cellEventsController, focusController, selectActionHelper } = useLegacyContext(); return ( - + {rows.map((item, rowIndex) => { return ( + focusController: FocusTargetController ): CellEventsController { - // Placeholder function, actual implementation will depend on the specific context and services available. - const cellContextFactory = (item: ObjectItem): CellContext => ({ - type: "cell", - item, - pageSize: pageSize.get(), - selectionType: config.selectionType, - selectionMethod: config.selectionMethod, - selectionMode: config.selectionMode, - clickTrigger: clickHelper.clickTrigger - }); + const pageSize = 10; + return useMemo(() => { + const cellContextFactory = (item: ObjectItem): CellContext => ({ + item, + pageSize: selectHelper.pageSize, + selectionType: selectHelper.selectionType, + selectionMethod: selectHelper.selectionMethod, + selectionMode: selectHelper.selectionMode, + clickTrigger: clickHelper.clickTrigger + }); - return new CellEventsController( - cellContextFactory, - selectActions.select, - selectActions.selectPage, - selectActions.selectAdjacent, - clickHelper.onExecuteAction, - focusController.dispatch - ); + return new CellEventsController( + cellContextFactory, + selectHelper.onSelect, + selectHelper.onSelectAll, + selectHelper.onSelectAdjacent, + clickHelper.onExecuteAction, + focusController.dispatch + ); + }, [selectHelper, clickHelper, focusController]); } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts new file mode 100644 index 0000000000..95bfeca571 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts @@ -0,0 +1,23 @@ +import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; +import { SelectionHelperService } from "@mendix/widget-plugin-grid/main"; +import { createContext, useContext } from "react"; +import { SelectActionHelper } from "../model/services/GridSelectActionsProvider.service"; +import { EventsController } from "../typings/CellComponent"; + +export interface LegacyRootScope { + selectionHelper: SelectionHelperService | undefined; + selectActionHelper: SelectActionHelper; + cellEventsController: EventsController; + checkboxEventsController: EventsController; + focusController: FocusTargetController; +} + +export const LegacyContext = createContext(null); + +export const useLegacyContext = (): LegacyRootScope => { + const contextValue = useContext(LegacyContext); + if (!contextValue) { + throw new Error("useDatagridRootScope must be used within a root context provider"); + } + return contextValue; +}; diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts index 78f70a245e..2c57da66bc 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts @@ -1,9 +1,9 @@ import { useOnClearSelectionEvent, useOnResetFiltersEvent } from "@mendix/widget-plugin-external-events/hooks"; -import { useDatagridConfig, useSelectActions } from "../model/hooks/injection-hooks"; +import { SelectActionsService } from "@mendix/widget-plugin-grid/main"; +import { useDatagridConfig } from "../model/hooks/injection-hooks"; -export function useDataGridJSActions(): void { +export function useDataGridJSActions(selectActions: SelectActionsService): void { const info = useDatagridConfig(); - const selectActions = useSelectActions(); useOnResetFiltersEvent(info.name, info.filtersChannelName); useOnClearSelectionEvent({ widgetName: info.name, diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts index 2493dfe82c..ab94ffbad5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -27,7 +27,6 @@ import { PageControlService } from "../../features/pagination/PageControl.servic import { paginationConfig } from "../../features/pagination/pagination.config"; import { customPaginationAtom, dynamicPageAtom, dynamicPageSizeAtom } from "../../features/pagination/pagination.model"; import { PaginationViewModel } from "../../features/pagination/Pagination.viewModel"; -import { createCellEventsController } from "../../features/row-interaction/CellEventsController"; import { creteCheckboxEventsController } from "../../features/row-interaction/CheckboxEventsController"; import { SelectAllModule } from "../../features/select-all/SelectAllModule.container"; import { ColumnGroupStore } from "../../helpers/state/ColumnGroupStore"; @@ -47,7 +46,7 @@ injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.fil injected(DatasourceParamsController, CORE.setupService, DG.query, DG.combinedFilter, CORE.columnsStore); injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.optional); injected(GridBasicData, CORE.mainGate); -injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM); +injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.progressService); injected(GridSizeStore, CORE.atoms.hasMoreItems, DG.paginationConfig, DG.setPageAction); /** Pagination **/ @@ -96,7 +95,6 @@ injected(createFocusController, CORE.setupService, DG.virtualLayout); injected(creteCheckboxEventsController, CORE.config, DG.selectActions, DG.focusService, DG.pageSize); injected(layoutAtom, CORE.atoms.itemCount, CORE.atoms.columnCount, DG.pageSize); injected(createClickActionHelper, CORE.setupService, CORE.mainGate); -injected(createCellEventsController, CORE.config, DG.selectActions, DG.focusService, DG.clickActionHelper, DG.pageSize); // selection counter injected( @@ -168,8 +166,6 @@ export class DatagridContainer extends Container { this.bind(DG.focusService).toInstance(createFocusController).inSingletonScope(); // Checkbox events service this.bind(DG.checkboxEventsHandler).toInstance(creteCheckboxEventsController).inSingletonScope(); - // Cell events service - this.bind(DG.cellEventsHandler).toInstance(createCellEventsController).inSingletonScope(); // Click action helper this.bind(DG.clickActionHelper).toInstance(createClickActionHelper).inSingletonScope(); } @@ -231,9 +227,6 @@ export class DatagridContainer extends Container { // Bind selection counter position this.bind(DG.selectionCounterCfg).toConstant({ position: props.selectionCounterPosition }); - // Bind selection type - this.bind(DG.selectionType).toConstant(config.selectionType); - this.postInit(props, config); return this; diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts index d62d242228..db58eb57fa 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts @@ -31,7 +31,7 @@ injected(hasMoreItemsAtom, CORE.mainGate); injected(visibleColumnsCountAtom, CORE.columnsStore); injected(isAllItemsPresentAtom, CORE.atoms.offset, CORE.atoms.hasMoreItems); injected(rowsAtom, CORE.mainGate); -injected(columnCount, CORE.atoms.visibleColumnsCount, CORE.config); +injected(columnCount, CORE.atoms.columnCount, CORE.config); // selection injected( diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts index 2d3cca8eba..187a845a90 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -21,9 +21,5 @@ export const [useRowClass] = createInjectionHooks(DG.rowClass); export const [useDatagridRootVM] = createInjectionHooks(DG.datagridRootVM); export const [useRows] = createInjectionHooks(CORE.rows); export const [useSelectActions] = createInjectionHooks(DG.selectActions); -export const [useClickActionHelper] = createInjectionHooks(DG.clickActionHelper); -export const [useFocusService] = createInjectionHooks(DG.focusService); -export const [useCheckboxEventsHandler] = createInjectionHooks(DG.checkboxEventsHandler); -export const [useCellEventsHandler] = createInjectionHooks(DG.cellEventsHandler); export const [useCustomPagination] = createInjectionHooks(DG.customPagination); export const [usePaginationConfig] = createInjectionHooks(DG.paginationConfig); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index cdcf6d5911..3b4eb157be 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -31,7 +31,6 @@ import { DynamicPaginationFeature } from "../features/pagination/DynamicPaginati import { GridPageControl } from "../features/pagination/GridPageControl"; import { PaginationViewModel } from "../features/pagination/Pagination.viewModel"; import { PaginationConfig } from "../features/pagination/pagination.config"; -import { CellEventsController } from "../features/row-interaction/CellEventsController"; import { CheckboxEventsController } from "../features/row-interaction/CheckboxEventsController"; import { SelectAllBarViewModel } from "../features/select-all/SelectAllBar.viewModel"; import { SelectionProgressDialogViewModel } from "../features/select-all/SelectionProgressDialog.viewModel"; @@ -145,8 +144,7 @@ export const DG_TOKENS = { virtualLayout: token>("@computed:virtualLayout"), clickActionHelper: token("@service:ClickActionHelper"), focusService: token("@service:FocusTargetController"), - checkboxEventsHandler: token("@service:CheckboxEventsController"), - cellEventsHandler: token("@service:CellEventsController") + checkboxEventsHandler: token("@service:CheckboxEventsController") }; /** "Select all" module tokens. */ diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index 006aa771f8..a2f4d9c646 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -2,6 +2,7 @@ import { dynamic, list, listAttribute, listExpression } from "@mendix/widget-plu import { ColumnsType, DatagridContainerProps } from "../../typings/DatagridProps"; import { ColumnStore } from "../helpers/state/column/ColumnStore"; import { IColumnParentStore } from "../helpers/state/ColumnGroupStore"; +import { SelectActionHelper } from "../model/services/GridSelectActionsProvider.service"; import { ColumnId, GridColumn } from "../typings/GridColumn"; export const column = (header = "Test", patch?: (col: ColumnsType) => void): ColumnsType => { diff --git a/packages/shared/widget-plugin-grid/src/selection/select-action-handler.ts b/packages/shared/widget-plugin-grid/src/selection/select-action-handler.ts index 165f58d6cb..c5bcccad46 100644 --- a/packages/shared/widget-plugin-grid/src/selection/select-action-handler.ts +++ b/packages/shared/widget-plugin-grid/src/selection/select-action-handler.ts @@ -6,7 +6,7 @@ import { SelectAdjacentFx, SelectAllFx, SelectFx, SelectionType, WidgetSelection export class SelectActionHandler { constructor( private selection: WidgetSelectionProperty, - protected selectionHelper: SelectionHelperService | undefined + protected selectionHelper: SelectionHelperService ) {} get selectionType(): SelectionType { From 4288b0f24c7d9eb7a2813246bb0787bd4e4aeb82 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:47:40 +0100 Subject: [PATCH 02/19] refactor: migrate event handlers to container --- .../datagrid-web/src/Datagrid.tsx | 57 +---------------- .../src/components/RowsRenderer.tsx | 20 ++++-- .../row-interaction/CellEventsController.ts | 61 +++++++++++-------- .../datagrid-web/src/helpers/root-context.ts | 23 ------- .../src/helpers/useDataGridJSActions.ts | 6 +- .../model/containers/Datagrid.container.ts | 9 ++- .../src/model/containers/Root.container.ts | 2 +- .../src/model/hooks/injection-hooks.ts | 4 ++ .../datagrid-web/src/model/tokens.ts | 4 +- 9 files changed, 73 insertions(+), 113 deletions(-) delete mode 100644 packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 0fd98997a9..3eeddd6be1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -1,73 +1,22 @@ -import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; -import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; -import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; import { ContainerProvider } from "brandi-react"; import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; import { DatagridContainerProps } from "../typings/DatagridProps"; import { Widget } from "./components/Widget"; 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 { useDataGridJSActions } from "./helpers/useDataGridJSActions"; -import { - useColumnsStore, - useDatagridConfig, - useExportProgressService, - useMainGate, - useSelectActions, - useSelectionHelper -} 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 config = useDatagridConfig(); - const gate = useMainGate(); const columnsStore = useColumnsStore(); const exportProgress = useExportProgressService(); - const items = gate.props.datasource.items ?? []; const [abortExport] = useDataExport(props, columnsStore, exportProgress); - const selectionHelper = useSelectionHelper(); + useDataGridJSActions(); - const selectActionHelper = useSelectActions(); - - const clickActionHelper = useClickActionHelper({ - onClickTrigger: props.onClickTrigger, - onClick: props.onClick - }); - - useDataGridJSActions(selectActionHelper); - - const visibleColumnsCount = config.checkboxColumnEnabled - ? columnsStore.visibleColumns.length + 1 - : columnsStore.visibleColumns.length; - - const focusController = useFocusTargetController({ - rows: items.length, - columns: visibleColumnsCount, - pageSize: props.pageSize - }); - - const cellEventsController = useCellEventsController(selectActionHelper, clickActionHelper, focusController); - - const checkboxEventsController = useCheckboxEventsController(selectActionHelper, focusController); - - return ( - - - - ); + return ; }); DatagridRoot.displayName = "DatagridComponent"; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx index 65c620c21c..11de6a4e44 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/RowsRenderer.tsx @@ -1,8 +1,15 @@ import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context"; import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; -import { useLegacyContext } from "../helpers/root-context"; -import { useColumnsStore, useDatagridConfig, useRowClass, useRows } from "../model/hooks/injection-hooks"; +import { + useCellEventsHandler, + useColumnsStore, + useDatagridConfig, + useFocusService, + useRowClass, + useRows, + useSelectActions +} from "../model/hooks/injection-hooks"; import { Row } from "./Row"; export const RowsRenderer = observer(function RowsRenderer(): ReactElement { @@ -10,15 +17,18 @@ export const RowsRenderer = observer(function RowsRenderer(): ReactElement { const config = useDatagridConfig(); const { visibleColumns } = useColumnsStore(); const rowClass = useRowClass(); - const { cellEventsController, focusController, selectActionHelper } = useLegacyContext(); + const cellEventsController = useCellEventsHandler(); + const focusService = useFocusService(); + const selectActions = useSelectActions(); + return ( - + {rows.map((item, rowIndex) => { return ( ): CellEventsController { - const pageSize = 10; - return useMemo(() => { - const cellContextFactory = (item: ObjectItem): CellContext => ({ - item, - pageSize: selectHelper.pageSize, - selectionType: selectHelper.selectionType, - selectionMethod: selectHelper.selectionMethod, - selectionMode: selectHelper.selectionMode, - clickTrigger: clickHelper.clickTrigger - }); + // Placeholder function, actual implementation will depend on the specific context and services available. + const cellContextFactory = (item: ObjectItem): CellContext => ({ + type: "cell", + item, + pageSize: pageSize.get(), + selectionType: config.selectionType, + selectionMethod: config.selectionMethod, + selectionMode: config.selectionMode, + clickTrigger: clickHelper.clickTrigger + }); - return new CellEventsController( - cellContextFactory, - selectHelper.onSelect, - selectHelper.onSelectAll, - selectHelper.onSelectAdjacent, - clickHelper.onExecuteAction, - focusController.dispatch - ); - }, [selectHelper, clickHelper, focusController]); + return new CellEventsController( + cellContextFactory, + selectActions.select, + selectActions.selectPage, + selectActions.selectAdjacent, + clickHelper.onExecuteAction, + focusController.dispatch + ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts deleted file mode 100644 index 95bfeca571..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; -import { SelectionHelperService } from "@mendix/widget-plugin-grid/main"; -import { createContext, useContext } from "react"; -import { SelectActionHelper } from "../model/services/GridSelectActionsProvider.service"; -import { EventsController } from "../typings/CellComponent"; - -export interface LegacyRootScope { - selectionHelper: SelectionHelperService | undefined; - selectActionHelper: SelectActionHelper; - cellEventsController: EventsController; - checkboxEventsController: EventsController; - focusController: FocusTargetController; -} - -export const LegacyContext = createContext(null); - -export const useLegacyContext = (): LegacyRootScope => { - const contextValue = useContext(LegacyContext); - if (!contextValue) { - throw new Error("useDatagridRootScope must be used within a root context provider"); - } - return contextValue; -}; diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts index 2c57da66bc..78f70a245e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/useDataGridJSActions.ts @@ -1,9 +1,9 @@ import { useOnClearSelectionEvent, useOnResetFiltersEvent } from "@mendix/widget-plugin-external-events/hooks"; -import { SelectActionsService } from "@mendix/widget-plugin-grid/main"; -import { useDatagridConfig } from "../model/hooks/injection-hooks"; +import { useDatagridConfig, useSelectActions } from "../model/hooks/injection-hooks"; -export function useDataGridJSActions(selectActions: SelectActionsService): void { +export function useDataGridJSActions(): void { const info = useDatagridConfig(); + const selectActions = useSelectActions(); useOnResetFiltersEvent(info.name, info.filtersChannelName); useOnClearSelectionEvent({ widgetName: info.name, diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts index ab94ffbad5..90c8e5a4d1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -22,6 +22,7 @@ import { Container, injected } from "brandi"; import { MainGateProps } from "../../../typings/MainGateProps"; import { WidgetRootViewModel } from "../../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../../features/empty-message/EmptyPlaceholder.viewModel"; +import { createCellEventsController } from "../../features/row-interaction/CellEventsController"; import { DynamicPaginationFeature } from "../../features/pagination/DynamicPagination.feature"; import { PageControlService } from "../../features/pagination/PageControl.service"; import { paginationConfig } from "../../features/pagination/pagination.config"; @@ -46,7 +47,7 @@ injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.fil injected(DatasourceParamsController, CORE.setupService, DG.query, DG.combinedFilter, CORE.columnsStore); injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.optional); injected(GridBasicData, CORE.mainGate); -injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.progressService); +injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM); injected(GridSizeStore, CORE.atoms.hasMoreItems, DG.paginationConfig, DG.setPageAction); /** Pagination **/ @@ -95,6 +96,7 @@ injected(createFocusController, CORE.setupService, DG.virtualLayout); injected(creteCheckboxEventsController, CORE.config, DG.selectActions, DG.focusService, DG.pageSize); injected(layoutAtom, CORE.atoms.itemCount, CORE.atoms.columnCount, DG.pageSize); injected(createClickActionHelper, CORE.setupService, CORE.mainGate); +injected(createCellEventsController, CORE.config, DG.selectActions, DG.focusService, DG.clickActionHelper, DG.pageSize); // selection counter injected( @@ -166,6 +168,8 @@ export class DatagridContainer extends Container { this.bind(DG.focusService).toInstance(createFocusController).inSingletonScope(); // Checkbox events service this.bind(DG.checkboxEventsHandler).toInstance(creteCheckboxEventsController).inSingletonScope(); + // Cell events service + this.bind(DG.cellEventsHandler).toInstance(createCellEventsController).inSingletonScope(); // Click action helper this.bind(DG.clickActionHelper).toInstance(createClickActionHelper).inSingletonScope(); } @@ -227,6 +231,9 @@ export class DatagridContainer extends Container { // Bind selection counter position this.bind(DG.selectionCounterCfg).toConstant({ position: props.selectionCounterPosition }); + // Bind selection type + this.bind(DG.selectionType).toConstant(config.selectionType); + this.postInit(props, config); return this; diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts index db58eb57fa..d62d242228 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts @@ -31,7 +31,7 @@ injected(hasMoreItemsAtom, CORE.mainGate); injected(visibleColumnsCountAtom, CORE.columnsStore); injected(isAllItemsPresentAtom, CORE.atoms.offset, CORE.atoms.hasMoreItems); injected(rowsAtom, CORE.mainGate); -injected(columnCount, CORE.atoms.columnCount, CORE.config); +injected(columnCount, CORE.atoms.visibleColumnsCount, CORE.config); // selection injected( diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts index 187a845a90..4f48b8b10c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -23,3 +23,7 @@ export const [useRows] = createInjectionHooks(CORE.rows); export const [useSelectActions] = createInjectionHooks(DG.selectActions); export const [useCustomPagination] = createInjectionHooks(DG.customPagination); export const [usePaginationConfig] = createInjectionHooks(DG.paginationConfig); +export const [useClickActionHelper] = createInjectionHooks(DG.clickActionHelper); +export const [useFocusService] = createInjectionHooks(DG.focusService); +export const [useCheckboxEventsHandler] = createInjectionHooks(DG.checkboxEventsHandler); +export const [useCellEventsHandler] = createInjectionHooks(DG.cellEventsHandler); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index 3b4eb157be..cdcf6d5911 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -31,6 +31,7 @@ import { DynamicPaginationFeature } from "../features/pagination/DynamicPaginati import { GridPageControl } from "../features/pagination/GridPageControl"; import { PaginationViewModel } from "../features/pagination/Pagination.viewModel"; import { PaginationConfig } from "../features/pagination/pagination.config"; +import { CellEventsController } from "../features/row-interaction/CellEventsController"; import { CheckboxEventsController } from "../features/row-interaction/CheckboxEventsController"; import { SelectAllBarViewModel } from "../features/select-all/SelectAllBar.viewModel"; import { SelectionProgressDialogViewModel } from "../features/select-all/SelectionProgressDialog.viewModel"; @@ -144,7 +145,8 @@ export const DG_TOKENS = { virtualLayout: token>("@computed:virtualLayout"), clickActionHelper: token("@service:ClickActionHelper"), focusService: token("@service:FocusTargetController"), - checkboxEventsHandler: token("@service:CheckboxEventsController") + checkboxEventsHandler: token("@service:CheckboxEventsController"), + cellEventsHandler: token("@service:CellEventsController") }; /** "Select all" module tokens. */ From 0967c319bebfc303d5f889aca40acc55b615c161 Mon Sep 17 00:00:00 2001 From: Yordan Date: Thu, 20 Nov 2025 13:02:24 +0100 Subject: [PATCH 03/19] refactor: extract drag & drop state and logic to mobx --- .../features/column/ColumnHeader.viewModel.ts | 114 ++++++++++++++++++ .../features/column/HeaderDragnDrop.store.ts | 36 ++++++ .../model/containers/Datagrid.container.ts | 3 + .../src/model/hooks/injection-hooks.ts | 1 + .../datagrid-web/src/model/tokens.ts | 2 + 5 files changed, 156 insertions(+) create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts new file mode 100644 index 0000000000..27be4adede --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts @@ -0,0 +1,114 @@ +import { makeAutoObservable } from "mobx"; +import { DragEvent, DragEventHandler } from "react"; +import { HeaderDragnDropStore } from "./HeaderDragnDrop.store"; +import { ColumnId } from "../../typings/GridColumn"; +import { ColumnGroupStore } from "../../helpers/state/ColumnGroupStore"; + +/** + * View model for a single column header drag & drop interactions. + * Encapsulates previous `useDraggable` hook logic and uses MobX store for shared drag state. + */ +export class ColumnHeaderViewModel { + private readonly dndStore: HeaderDragnDropStore; + private readonly columnsStore: ColumnGroupStore; + private readonly columnsDraggable: boolean; + + constructor(params: { dndStore: HeaderDragnDropStore; columnsStore: ColumnGroupStore; columnsDraggable: boolean }) { + this.dndStore = params.dndStore; + this.columnsStore = params.columnsStore; + this.columnsDraggable = params.columnsDraggable; + makeAutoObservable(this); + } + + get dropTarget(): [ColumnId, "before" | "after"] | undefined { + return this.dndStore.dragOver; + } + + get dragging(): [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined { + return this.dndStore.isDragging; + } + + /** Handlers exposed to the component. */ + get draggableProps(): { + draggable?: boolean; + onDragStart?: DragEventHandler; + onDragOver?: DragEventHandler; + onDrop?: DragEventHandler; + onDragEnter?: DragEventHandler; + onDragEnd?: DragEventHandler; + } { + if (!this.columnsDraggable) { + return {}; + } + return { + draggable: true, + onDragStart: this.handleDragStart, + onDragOver: this.handleDragOver, + onDrop: this.handleOnDrop, + onDragEnter: this.handleDragEnter, + onDragEnd: this.handleDragEnd + }; + } + + private handleDragStart = (e: DragEvent): void => { + const elt = (e.target as HTMLDivElement).closest(".th") as HTMLDivElement; + if (!elt) { + return; + } + const columnId = (elt.dataset.columnId ?? "") as ColumnId; + const columnAtTheLeft = (elt.previousElementSibling as HTMLDivElement)?.dataset?.columnId as ColumnId; + const columnAtTheRight = (elt.nextElementSibling as HTMLDivElement)?.dataset?.columnId as ColumnId; + this.dndStore.setIsDragging([columnAtTheLeft, columnId, columnAtTheRight]); + }; + + private handleDragOver = (e: DragEvent): void => { + const dragging = this.dragging; + if (!dragging) { + return; + } + const columnId = (e.currentTarget as HTMLDivElement).dataset.columnId as ColumnId; + if (!columnId) { + return; + } + e.preventDefault(); + const [leftSiblingColumnId, draggingColumnId, rightSiblingColumnId] = dragging; + if (columnId === draggingColumnId) { + if (this.dropTarget !== undefined) { + this.dndStore.setDragOver(undefined); + } + return; + } + let isAfter: boolean; + if (columnId === leftSiblingColumnId) { + isAfter = false; + } else if (columnId === rightSiblingColumnId) { + isAfter = true; + } else { + const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect(); + isAfter = rect.width / 2 + (this.dropTarget?.[1] === "after" ? -10 : 10) < e.clientX - rect.left; + } + const newPosition: "before" | "after" = isAfter ? "after" : "before"; + if (columnId !== this.dropTarget?.[0] || newPosition !== this.dropTarget?.[1]) { + this.dndStore.setDragOver([columnId, newPosition]); + } + }; + + private handleDragEnter = (e: DragEvent): void => { + e.preventDefault(); + }; + + private handleDragEnd = (): void => { + this.dndStore.clearDragState(); + }; + + private handleOnDrop = (_e: DragEvent): void => { + const dragging = this.dragging; + const dropTarget = this.dropTarget; + this.handleDragEnd(); + if (!dragging || !dropTarget) { + return; + } + // Reorder columns using existing columns store logic + this.columnsStore.swapColumns(dragging[1], dropTarget); + }; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts new file mode 100644 index 0000000000..14090ab1ae --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts @@ -0,0 +1,36 @@ +import { action, makeAutoObservable } from "mobx"; +import { ColumnId } from "../../typings/GridColumn"; + +export class HeaderDragnDropStore { + private _dragOver: [ColumnId, "before" | "after"] | undefined = undefined; + private _isDragging: [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined = undefined; + + constructor() { + makeAutoObservable(this, { + setDragOver: action, + setIsDragging: action, + clearDragState: action + }); + } + + get dragOver(): [ColumnId, "before" | "after"] | undefined { + return this._dragOver; + } + + get isDragging(): [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined { + return this._isDragging; + } + + setDragOver(value: [ColumnId, "before" | "after"] | undefined): void { + this._dragOver = value; + } + + setIsDragging(value: [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined): void { + this._isDragging = value; + } + + clearDragState(): void { + this._dragOver = undefined; + this._isDragging = undefined; + } +} diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts index 90c8e5a4d1..3ca4e2208a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -31,6 +31,7 @@ import { PaginationViewModel } from "../../features/pagination/Pagination.viewMo import { creteCheckboxEventsController } from "../../features/row-interaction/CheckboxEventsController"; import { SelectAllModule } from "../../features/select-all/SelectAllModule.container"; import { ColumnGroupStore } from "../../helpers/state/ColumnGroupStore"; +import { HeaderDragnDropStore } from "../../features/column/HeaderDragnDrop.store"; import { GridBasicData } from "../../helpers/state/GridBasicData"; import { GridPersonalizationStore } from "../../helpers/state/GridPersonalizationStore"; import { DatagridConfig } from "../configs/Datagrid.config"; @@ -116,6 +117,8 @@ export class DatagridContainer extends Container { this.bind(DG.basicDate).toInstance(GridBasicData).inSingletonScope(); // Columns store this.bind(CORE.columnsStore).toInstance(ColumnGroupStore).inSingletonScope(); + // Drag and Drop store + this.bind(DG.headerDragDrop).toInstance(HeaderDragnDropStore).inSingletonScope(); // Query service this.bind(DG.query).toInstance(DatasourceService).inSingletonScope(); // Grid sizing and scrolling store diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts index 4f48b8b10c..e17c4432b9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -23,6 +23,7 @@ export const [useRows] = createInjectionHooks(CORE.rows); export const [useSelectActions] = createInjectionHooks(DG.selectActions); export const [useCustomPagination] = createInjectionHooks(DG.customPagination); export const [usePaginationConfig] = createInjectionHooks(DG.paginationConfig); +export const [useHeaderDragDrop] = createInjectionHooks(DG.headerDragDrop); export const [useClickActionHelper] = createInjectionHooks(DG.clickActionHelper); export const [useFocusService] = createInjectionHooks(DG.focusService); export const [useCheckboxEventsHandler] = createInjectionHooks(DG.checkboxEventsHandler); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index cdcf6d5911..3f1e877fe1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -38,6 +38,7 @@ import { SelectionProgressDialogViewModel } from "../features/select-all/Selecti import { ColumnGroupStore } from "../helpers/state/ColumnGroupStore"; import { GridBasicData } from "../helpers/state/GridBasicData"; import { GridPersonalizationStore } from "../helpers/state/GridPersonalizationStore"; +import { HeaderDragnDropStore } from "../features/column/HeaderDragnDrop.store"; import { DatasourceParamsController } from "../model/services/DatasourceParamsController"; import { GridColumn } from "../typings/GridColumn"; import { DatagridConfig } from "./configs/Datagrid.config"; @@ -146,6 +147,7 @@ export const DG_TOKENS = { clickActionHelper: token("@service:ClickActionHelper"), focusService: token("@service:FocusTargetController"), checkboxEventsHandler: token("@service:CheckboxEventsController"), + headerDragDrop: token("HeaderDragnDropStore"), cellEventsHandler: token("@service:CellEventsController") }; From eb5c962d6d4816ec88fd3cf01b26c56ce2d4de05 Mon Sep 17 00:00:00 2001 From: Yordan Date: Thu, 20 Nov 2025 13:04:03 +0100 Subject: [PATCH 04/19] refactor: update components to use new state management --- .../src/components/ColumnContainer.tsx | 127 ++++++++++ .../src/components/ColumnHeader.tsx | 29 +++ .../src/components/GridHeader.tsx | 13 +- .../datagrid-web/src/components/Header.tsx | 238 ------------------ 4 files changed, 159 insertions(+), 248 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx delete mode 100644 packages/pluggableWidgets/datagrid-web/src/components/Header.tsx diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx new file mode 100644 index 0000000000..e537707f67 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -0,0 +1,127 @@ +import classNames from "classnames"; +import { HTMLAttributes, KeyboardEvent, ReactElement, ReactNode, useMemo } from "react"; +import { FaArrowsAltV } from "./icons/FaArrowsAltV"; +import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; +import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; + +import ColumnHeader from "./ColumnHeader"; + +import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDragDrop } from "../model/hooks/injection-hooks"; +import { GridColumn } from "../typings/GridColumn"; +import { ColumnResizerProps } from "./ColumnResizer"; +import { ColumnHeaderViewModel } from "../features/column/ColumnHeader.viewModel"; +import { observer } from "mobx-react-lite"; + +export interface ColumnContainerProps { + isLast?: boolean; + resizer: ReactElement; +} + +export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement { + const { columnsFilterable, id: gridId } = useDatagridConfig(); + const columnsStore = useColumnsStore(); + const column = useColumn(); + const { canDrag, canSort } = column; + + const headerDragDropStore = useHeaderDragDrop(); + const columnHeaderVM = useMemo( + () => + new ColumnHeaderViewModel({ + dndStore: headerDragDropStore, + columnsStore, + columnsDraggable: canDrag + }), + [headerDragDropStore, columnsStore, canDrag] + ); + const draggableProps = columnHeaderVM.draggableProps; + const dropTarget = columnHeaderVM.dropTarget; + const isDragging = columnHeaderVM.dragging; + + const sortProps = canSort ? getSortProps(column) : null; + const caption = column.header.trim(); + + return ( + + + + {canSort ? : null} + + {columnsFilterable && ( + + {columnsStore.columnFilters[column.columnIndex]?.renderFilterWidgets()} + + )} + + {column.canResize ? props.resizer : null} + + ); +}); + +function SortIcon(): ReactNode { + const column = useColumn(); + switch (column.sortDir) { + case "asc": + return ; + case "desc": + return ; + default: + return ; + } +} + +function getAriaSort(canSort: boolean, column: GridColumn): "ascending" | "descending" | "none" | undefined { + if (!canSort) { + return undefined; + } + + switch (column.sortDir) { + case "asc": + return "ascending"; + case "desc": + return "descending"; + default: + return "none"; + } +} + +function getSortProps(column: GridColumn): HTMLAttributes { + return { + onClick: () => { + column.toggleSort(); + }, + onKeyDown: (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + column.toggleSort(); + } + }, + role: "button", + tabIndex: 0 + }; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx new file mode 100644 index 0000000000..a27a18e7af --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -0,0 +1,29 @@ +import classNames from "classnames"; +import { HTMLAttributes, ReactElement, ReactNode } from "react"; + +export interface ColumnHeaderProps { + children?: ReactNode; + sortProps?: HTMLAttributes | null; + canSort: boolean; + caption: string; + isDragging?: [string | undefined, string, string | undefined] | undefined; + columnAlignment?: "left" | "center" | "right"; +} + +export default function ColumnHeader(props: ColumnHeaderProps): ReactElement { + return ( + + {props.caption.length > 0 ? props.caption : "\u00a0"} + {props.children} + + ); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx index a378cd42b7..bd4abcea4d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx @@ -1,11 +1,10 @@ -import { ReactElement, useState } from "react"; +import { ReactElement } from "react"; import { useColumnsStore, useDatagridConfig, useGridSizeStore } from "../model/hooks/injection-hooks"; -import { ColumnId } from "../typings/GridColumn"; import { CheckboxColumnHeader } from "./CheckboxColumnHeader"; import { ColumnProvider } from "./ColumnProvider"; import { ColumnResizer } from "./ColumnResizer"; import { ColumnSelector } from "./ColumnSelector"; -import { Header } from "./Header"; +import { ColumnContainer } from "./ColumnContainer"; import { HeaderSkeletonLoader } from "./loader/HeaderSkeletonLoader"; export function GridHeader(): ReactElement { @@ -13,8 +12,6 @@ export function GridHeader(): ReactElement { const columnsStore = useColumnsStore(); const gridSizeStore = useGridSizeStore(); const columns = columnsStore.visibleColumns; - const [dragOver, setDragOver] = useState<[ColumnId, "before" | "after"] | undefined>(undefined); - const [isDragging, setIsDragging] = useState<[ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined>(); if (!columnsStore.loaded) { return ; @@ -26,9 +23,7 @@ export function GridHeader(): ReactElement { {columns.map(column => ( - columnsStore.setIsResizing(true)} @@ -36,8 +31,6 @@ export function GridHeader(): ReactElement { setColumnWidth={(width: number) => column.setSize(width)} /> } - setDropTarget={setDragOver} - setIsDragging={setIsDragging} /> ))} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx deleted file mode 100644 index 1c396b3b75..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import classNames from "classnames"; -import { - Dispatch, - DragEvent, - DragEventHandler, - HTMLAttributes, - KeyboardEvent, - ReactElement, - ReactNode, - SetStateAction, - useCallback -} from "react"; -import { FaArrowsAltV } from "./icons/FaArrowsAltV"; -import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; -import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; - -import { useColumn, useColumnsStore, useDatagridConfig } from "../model/hooks/injection-hooks"; -import { ColumnId, GridColumn } from "../typings/GridColumn"; -import { ColumnResizerProps } from "./ColumnResizer"; - -export interface HeaderProps { - isLast?: boolean; - resizer: ReactElement; - - dropTarget?: [ColumnId, "before" | "after"]; - isDragging?: [ColumnId | undefined, ColumnId, ColumnId | undefined]; - setDropTarget: Dispatch>; - setIsDragging: Dispatch>; -} - -export function Header(props: HeaderProps): ReactElement { - const { columnsFilterable, id: gridId, columnsDraggable, columnsResizable, columnsSortable } = useDatagridConfig(); - const columnsStore = useColumnsStore(); - const column = useColumn(); - const canDrag = columnsDraggable && column.canDrag; - const canSort = columnsSortable && column.canSort; - const canResize = columnsResizable && column.canResize; - - const draggableProps = useDraggable( - canDrag, - columnsStore.swapColumns.bind(columnsStore), - props.dropTarget, - props.setDropTarget, - props.isDragging, - props.setIsDragging - ); - - const sortIcon = canSort ? getSortIcon(column) : null; - const sortProps = canSort ? getSortProps(column) : null; - const caption = column.header.trim(); - - return ( - - - - {caption.length > 0 ? caption : "\u00a0"} - {sortIcon} - - {columnsFilterable && ( - - {columnsStore.columnFilters[column.columnIndex]?.renderFilterWidgets()} - - )} - - {canResize ? props.resizer : null} - - ); -} - -function useDraggable( - columnsDraggable: boolean, - setColumnOrder: (source: ColumnId, target: [ColumnId, "after" | "before"]) => void, - dropTarget: [ColumnId, "before" | "after"] | undefined, - setDropTarget: Dispatch>, - dragging: [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined, - setDragging: Dispatch> -): { - draggable?: boolean; - onDragStart?: DragEventHandler; - onDragOver?: DragEventHandler; - onDrop?: DragEventHandler; - onDragEnter?: DragEventHandler; - onDragEnd?: DragEventHandler; -} { - const handleDragStart = useCallback( - (e: DragEvent): void => { - const elt = (e.target as HTMLDivElement).closest(".th") as HTMLDivElement; - const columnId = elt.dataset.columnId ?? ""; - - const columnAtTheLeft = (elt.previousElementSibling as HTMLDivElement)?.dataset?.columnId as ColumnId; - const columnAtTheRight = (elt.nextElementSibling as HTMLDivElement)?.dataset?.columnId as ColumnId; - - setDragging([columnAtTheLeft, columnId as ColumnId, columnAtTheRight]); - }, - [setDragging] - ); - - const handleDragOver = useCallback( - (e: DragEvent): void => { - if (!dragging) { - return; - } - const columnId = (e.currentTarget as HTMLDivElement).dataset.columnId as ColumnId; - if (!columnId) { - return; - } - e.preventDefault(); - - const [leftSiblingColumnId, draggingColumnId, rightSiblingColumnId] = dragging; - - if (columnId === draggingColumnId) { - // hover on itself place, no highlight - if (dropTarget !== undefined) { - setDropTarget(undefined); - } - return; - } - - let isAfter: boolean; - - if (columnId === leftSiblingColumnId) { - isAfter = false; - } else if (columnId === rightSiblingColumnId) { - isAfter = true; - } else { - // check position in element - const rect = e.currentTarget.getBoundingClientRect(); - isAfter = rect.width / 2 + (dropTarget?.[1] === "after" ? -10 : 10) < e.clientX - rect.left; - } - - const newPosition = isAfter ? "after" : "before"; - - if (columnId !== dropTarget?.[0] || newPosition !== dropTarget?.[1]) { - setDropTarget([columnId, newPosition]); - } - }, - [dragging, dropTarget, setDropTarget] - ); - - const handleDragEnter = useCallback((e: DragEvent): void => { - e.preventDefault(); - }, []); - - const handleDragEnd = useCallback((): void => { - setDragging(undefined); - setDropTarget(undefined); - }, [setDropTarget, setDragging]); - - const handleOnDrop = useCallback( - (_e: DragEvent): void => { - handleDragEnd(); - if (!dragging || !dropTarget) { - return; - } - - setColumnOrder(dragging[1], dropTarget); - }, - [handleDragEnd, setColumnOrder, dragging, dropTarget] - ); - - return columnsDraggable - ? { - draggable: true, - onDragStart: handleDragStart, - onDragOver: handleDragOver, - onDrop: handleOnDrop, - onDragEnter: handleDragEnter, - onDragEnd: handleDragEnd - } - : {}; -} - -function getSortIcon(column: GridColumn): ReactNode { - switch (column.sortDir) { - case "asc": - return ; - case "desc": - return ; - default: - return ; - } -} - -function getAriaSort(canSort: boolean, column: GridColumn): "ascending" | "descending" | "none" | undefined { - if (!canSort) { - return undefined; - } - - switch (column.sortDir) { - case "asc": - return "ascending"; - case "desc": - return "descending"; - default: - return "none"; - } -} - -function getSortProps(column: GridColumn): HTMLAttributes { - return { - onClick: () => { - column.toggleSort(); - }, - onKeyDown: (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - column.toggleSort(); - } - }, - role: "button", - tabIndex: 0 - }; -} From 813d7b6cc5a61003816cd3e6929ff438436a41bc Mon Sep 17 00:00:00 2001 From: Yordan Date: Thu, 20 Nov 2025 13:05:33 +0100 Subject: [PATCH 05/19] refactor: remove obsolete tests and snapshots, create for new component --- .../src/components/__tests__/Header.spec.tsx | 177 ----------- .../__snapshots__/Header.spec.tsx.snap | 197 ------------ .../__tests__/ColumnHeader.viewModel.spec.ts | 299 ++++++++++++++++++ 3 files changed, 299 insertions(+), 374 deletions(-) delete mode 100644 packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx delete mode 100644 packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Header.spec.tsx.snap create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx deleted file mode 100644 index defbdfd369..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { dynamic } from "@mendix/widget-plugin-test-utils"; -import "@testing-library/jest-dom"; -import { render } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { ContainerProvider } from "brandi-react"; -import { createDatagridContainer } from "../../model/containers/createDatagridContainer"; -import { CORE_TOKENS } from "../../model/tokens"; -import { column, mockContainerProps } from "../../utils/test-utils"; -import { ColumnProvider } from "../ColumnProvider"; -import { ColumnResizer } from "../ColumnResizer"; -import { Header, HeaderProps } from "../Header"; - -describe("Header", () => { - it("renders the structure correctly", () => { - const props = mockContainerProps({ - columns: [column("Column 1")] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - - const component = render( - - - - - - ); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly when sortable", () => { - const columnsType = column("Column 1", col => { - col.sortable = true; - }); - const props = mockContainerProps({ - columns: [columnsType] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - - const component = render( - - - - - - ); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly when resizable", () => { - const columnsType = column("Column 1", col => { - col.resizable = true; - }); - const props = mockContainerProps({ - columns: [columnsType] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - - const component = render( - - - resizer} /> - - - ); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly when draggable", () => { - const columnsType = column("Column 1", col => { - col.draggable = true; - }); - const props = mockContainerProps({ - columns: [columnsType] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - - const component = render( - - - - - - ); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly when filterable with custom filter", () => { - const columnsType = column("Column 1", col => { - col.filter = Custom filter; - }); - const props = mockContainerProps({ - columns: [columnsType] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - - const component = render( - - - - - - ); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("calls sort function when sortable", async () => { - const user = userEvent.setup(); - const columnsType = column("Column 1", col => { - col.sortable = true; - }); - const props = mockContainerProps({ - columns: [columnsType] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - const spy = jest.spyOn(col, "toggleSort"); - - const component = render( - - - - - - ); - const button = component.getByLabelText("sort Column 1"); - - expect(button).toBeInTheDocument(); - await user.click(button); - expect(spy).toHaveBeenCalled(); - }); - - it("renders the structure correctly when value is empty", () => { - const columnsType = column("Column 1", col => { - col.header = dynamic(" "); - }); - const props = mockContainerProps({ - columns: [columnsType] - }); - const [container] = createDatagridContainer(props); - const columns = container.get(CORE_TOKENS.columnsStore); - const col = columns.visibleColumns[0]; - - const component = render( - - - - - - ); - expect(component.asFragment()).toMatchSnapshot(); - }); -}); - -function mockHeaderProps(): HeaderProps { - return { - dropTarget: undefined, - resizer: , - setDropTarget: jest.fn(), - setIsDragging: jest.fn() - }; -} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Header.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Header.spec.tsx.snap deleted file mode 100644 index 6deadade40..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Header.spec.tsx.snap +++ /dev/null @@ -1,197 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header renders the structure correctly 1`] = ` - - - - - - Column 1 - - - - - - -`; - -exports[`Header renders the structure correctly when draggable 1`] = ` - - - - - - Column 1 - - - - - - -`; - -exports[`Header renders the structure correctly when filterable with custom filter 1`] = ` - - - - - - Column 1 - - - - - Custom filter - - - - - -`; - -exports[`Header renders the structure correctly when resizable 1`] = ` - - - - - - Column 1 - - - - - - -`; - -exports[`Header renders the structure correctly when sortable 1`] = ` - - - - - - Column 1 - - - - - - - - - -`; - -exports[`Header renders the structure correctly when value is empty 1`] = ` - - - - - - Â - - - - - - -`; diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts new file mode 100644 index 0000000000..bb8b0dde39 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts @@ -0,0 +1,299 @@ +import { DragEvent } from "react"; +import { ColumnHeaderViewModel } from "../ColumnHeader.viewModel"; +import { HeaderDragnDropStore } from "../HeaderDragnDrop.store"; +import { ColumnId } from "../../../typings/GridColumn"; + +describe("ColumnHeaderViewModel", () => { + let dndStore: HeaderDragnDropStore; + let mockColumnsStore: any; + + beforeEach(() => { + dndStore = new HeaderDragnDropStore(); + mockColumnsStore = { + swapColumns: jest.fn() + }; + }); + + describe("when columnsDraggable is false", () => { + it("returns empty draggableProps", () => { + const vm = new ColumnHeaderViewModel({ + dndStore, + columnsStore: mockColumnsStore, + columnsDraggable: false + }); + + expect(vm.draggableProps).toEqual({}); + }); + }); + + describe("when columnsDraggable is true", () => { + let vm: ColumnHeaderViewModel; + + beforeEach(() => { + vm = new ColumnHeaderViewModel({ + dndStore, + columnsStore: mockColumnsStore, + columnsDraggable: true + }); + }); + + it("returns draggable props with handlers", () => { + const props = vm.draggableProps; + + expect(props.draggable).toBe(true); + expect(props.onDragStart).toBeDefined(); + expect(props.onDragOver).toBeDefined(); + expect(props.onDrop).toBeDefined(); + expect(props.onDragEnter).toBeDefined(); + expect(props.onDragEnd).toBeDefined(); + }); + + describe("dropTarget", () => { + it("returns undefined initially", () => { + expect(vm.dropTarget).toBeUndefined(); + }); + + it("returns value from dndStore", () => { + dndStore.setDragOver(["col1" as ColumnId, "after"]); + expect(vm.dropTarget).toEqual(["col1", "after"]); + }); + }); + + describe("dragging", () => { + it("returns undefined initially", () => { + expect(vm.dragging).toBeUndefined(); + }); + + it("returns value from dndStore", () => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + expect(vm.dragging).toEqual(["col0", "col1", "col2"]); + }); + }); + + describe("handleDragStart", () => { + it("sets dragging state with column siblings", () => { + const mockElement = createMockElement("col1", "col0", "col2"); + const event = createMockDragEvent(mockElement); + + vm.draggableProps.onDragStart?.(event); + + expect(dndStore.isDragging).toEqual(["col0", "col1", "col2"]); + }); + + it("handles missing previous sibling", () => { + const mockElement = createMockElement("col1", undefined, "col2"); + const event = createMockDragEvent(mockElement); + + vm.draggableProps.onDragStart?.(event); + + expect(dndStore.isDragging).toEqual([undefined, "col1", "col2"]); + }); + + it("handles missing next sibling", () => { + const mockElement = createMockElement("col1", "col0", undefined); + const event = createMockDragEvent(mockElement); + + vm.draggableProps.onDragStart?.(event); + + expect(dndStore.isDragging).toEqual(["col0", "col1", undefined]); + }); + + it("does nothing when element is not found", () => { + const event = { + target: { + closest: jest.fn().mockReturnValue(null) + } + } as any; + + vm.draggableProps.onDragStart?.(event); + + expect(dndStore.isDragging).toBeUndefined(); + }); + }); + + describe("handleDragOver", () => { + beforeEach(() => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + }); + + it("does nothing when not dragging", () => { + dndStore.clearDragState(); + const event = createMockDragOverEvent("col2", 100, 50); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toBeUndefined(); + }); + + it("does nothing when columnId is missing", () => { + const event = createMockDragOverEvent("", 100, 50); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toBeUndefined(); + }); + + it("clears dropTarget when hovering over self", () => { + dndStore.setDragOver(["col2" as ColumnId, "after"]); + const event = createMockDragOverEvent("col1", 100, 50); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toBeUndefined(); + }); + + it("sets dropTarget to before when hovering over left sibling", () => { + const event = createMockDragOverEvent("col0", 100, 50); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toEqual(["col0", "before"]); + }); + + it("sets dropTarget to after when hovering over right sibling", () => { + const event = createMockDragOverEvent("col2", 100, 50); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toEqual(["col2", "after"]); + }); + + it("sets dropTarget to before when hovering on left side of non-sibling column", () => { + const event = createMockDragOverEvent("col5", 100, 30); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toEqual(["col5", "before"]); + }); + + it("sets dropTarget to after when hovering on right side of non-sibling column", () => { + const event = createMockDragOverEvent("col5", 100, 70); + + vm.draggableProps.onDragOver?.(event); + + expect(dndStore.dragOver).toEqual(["col5", "after"]); + }); + + it("does not update dropTarget if it hasn't changed", () => { + dndStore.setDragOver(["col5" as ColumnId, "after"]); + const setDragOverSpy = jest.spyOn(dndStore, "setDragOver"); + const event = createMockDragOverEvent("col5", 100, 70); + + vm.draggableProps.onDragOver?.(event); + + expect(setDragOverSpy).not.toHaveBeenCalled(); + }); + + it("prevents default behavior", () => { + const event = createMockDragOverEvent("col2", 100, 50); + + vm.draggableProps.onDragOver?.(event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe("handleDragEnter", () => { + it("prevents default behavior", () => { + const event = { preventDefault: jest.fn() } as any; + + vm.draggableProps.onDragEnter?.(event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe("handleDragEnd", () => { + it("clears drag state", () => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + dndStore.setDragOver(["col2" as ColumnId, "after"]); + + vm.draggableProps.onDragEnd?.({} as any); + + expect(dndStore.isDragging).toBeUndefined(); + expect(dndStore.dragOver).toBeUndefined(); + }); + }); + + describe("handleOnDrop", () => { + it("calls swapColumns with correct parameters", () => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + dndStore.setDragOver(["col3" as ColumnId, "after"]); + + vm.draggableProps.onDrop?.({} as any); + + expect(mockColumnsStore.swapColumns).toHaveBeenCalledWith("col1", ["col3", "after"]); + }); + + it("clears drag state after drop", () => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + dndStore.setDragOver(["col3" as ColumnId, "after"]); + + vm.draggableProps.onDrop?.({} as any); + + expect(dndStore.isDragging).toBeUndefined(); + expect(dndStore.dragOver).toBeUndefined(); + }); + + it("does not call swapColumns when not dragging", () => { + dndStore.setDragOver(["col3" as ColumnId, "after"]); + + vm.draggableProps.onDrop?.({} as any); + + expect(mockColumnsStore.swapColumns).not.toHaveBeenCalled(); + }); + + it("does not call swapColumns when no dropTarget", () => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + + vm.draggableProps.onDrop?.({} as any); + + expect(mockColumnsStore.swapColumns).not.toHaveBeenCalled(); + }); + + it("clears drag state even when drop is invalid", () => { + dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); + + vm.draggableProps.onDrop?.({} as any); + + expect(dndStore.isDragging).toBeUndefined(); + expect(dndStore.dragOver).toBeUndefined(); + }); + }); + }); +}); + +// Helper functions to create mock DOM elements and events + +function createMockElement( + columnId: string, + prevSiblingId: string | undefined, + nextSiblingId: string | undefined +): HTMLDivElement { + const element = { + dataset: { columnId }, + previousElementSibling: prevSiblingId ? { dataset: { columnId: prevSiblingId } } : null, + nextElementSibling: nextSiblingId ? { dataset: { columnId: nextSiblingId } } : null + } as any; + + return element; +} + +function createMockDragEvent(targetElement: HTMLDivElement): DragEvent { + return { + target: { + closest: jest.fn().mockReturnValue(targetElement) + } + } as any; +} + +function createMockDragOverEvent(columnId: string, width: number, clientX: number): DragEvent { + return { + currentTarget: { + dataset: { columnId }, + getBoundingClientRect: jest.fn().mockReturnValue({ width, left: 0 }) + }, + clientX, + preventDefault: jest.fn() + } as any; +} From e280729a71f8e212aed606a9633f441837ca2f63 Mon Sep 17 00:00:00 2001 From: Yordan Date: Thu, 20 Nov 2025 14:30:27 +0100 Subject: [PATCH 06/19] refactor: rewrite columnreszier to use injection hooks --- .../src/components/ColumnResizer.tsx | 28 ++++++++----------- .../src/components/GridHeader.tsx | 10 +------ 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnResizer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnResizer.tsx index 90dc7f7449..bcbc4bcfe9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnResizer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnResizer.tsx @@ -1,24 +1,18 @@ import { useEventCallback } from "@mendix/widget-plugin-hooks/useEventCallback"; import { MouseEvent, ReactElement, TouchEvent, useCallback, useEffect, useRef, useState } from "react"; +import { useColumn, useColumnsStore } from "../model/hooks/injection-hooks"; export interface ColumnResizerProps { minWidth?: number; - setColumnWidth: (width: number) => void; - onResizeEnds?: () => void; - onResizeStart?: () => void; } -export function ColumnResizer({ - minWidth = 50, - setColumnWidth, - onResizeEnds, - onResizeStart -}: ColumnResizerProps): ReactElement { +export function ColumnResizer({ minWidth = 50 }: ColumnResizerProps): ReactElement { + const column = useColumn(); + const columnsStore = useColumnsStore(); const [isResizing, setIsResizing] = useState(false); const [startPosition, setStartPosition] = useState(0); const [currentWidth, setCurrentWidth] = useState(0); const resizerReference = useRef(null); - const onStart = useEventCallback(onResizeStart); const onStartDrag = useCallback( (e: TouchEvent & MouseEvent): void => { @@ -26,12 +20,12 @@ export function ColumnResizer({ setStartPosition(mouseX); setIsResizing(true); if (resizerReference.current) { - const column = resizerReference.current.parentElement!; - setCurrentWidth(column.offsetWidth); + const columnElement = resizerReference.current.parentElement!; + setCurrentWidth(columnElement.offsetWidth); } - onStart(); + columnsStore.setIsResizing(true); }, - [onStart] + [columnsStore] ); const onEndDrag = useCallback((): void => { if (!isResizing) { @@ -39,9 +33,9 @@ export function ColumnResizer({ } setIsResizing(false); setCurrentWidth(0); - onResizeEnds?.(); - }, [onResizeEnds, isResizing]); - const setColumnWidthStable = useEventCallback(setColumnWidth); + columnsStore.setIsResizing(false); + }, [columnsStore, isResizing]); + const setColumnWidthStable = useEventCallback((width: number) => column.setSize(width)); const onMouseMove = useCallback( (e: TouchEvent & MouseEvent & Event): void => { if (!isResizing) { diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx index bd4abcea4d..cab4ef45f2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx @@ -23,15 +23,7 @@ export function GridHeader(): ReactElement { {columns.map(column => ( - columnsStore.setIsResizing(true)} - onResizeEnds={() => columnsStore.setIsResizing(false)} - setColumnWidth={(width: number) => column.setSize(width)} - /> - } - /> + } /> ))} {columnsHidable && ( From 3e6635eff7da224d7615ddc3adb315216e2eb5b4 Mon Sep 17 00:00:00 2001 From: Yordan Date: Thu, 20 Nov 2025 14:49:41 +0100 Subject: [PATCH 07/19] refactor: enhance ColumnResizer test structure and update snapshot --- .../__tests__/ColumnResizer.spec.tsx | 18 +++++++++++++++++- .../__snapshots__/ColumnResizer.spec.tsx.snap | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnResizer.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnResizer.spec.tsx index daa0d9572b..a3aaed7e23 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnResizer.spec.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnResizer.spec.tsx @@ -1,10 +1,26 @@ import "@testing-library/jest-dom"; import { render } from "@testing-library/react"; +import { ContainerProvider } from "brandi-react"; +import { createDatagridContainer } from "../../model/containers/createDatagridContainer"; +import { CORE_TOKENS as CORE } from "../../model/tokens"; +import { mockContainerProps } from "../../utils/test-utils"; +import { ColumnProvider } from "../ColumnProvider"; import { ColumnResizer } from "../ColumnResizer"; describe("Column Resizer", () => { it("renders the structure correctly", () => { - const component = render(); + const props = mockContainerProps(); + const [container] = createDatagridContainer(props); + const columnsStore = container.get(CORE.columnsStore); + const column = columnsStore.visibleColumns[0]; + + const component = render( + + + + + + ); expect(component).toMatchSnapshot(); }); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnResizer.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnResizer.spec.tsx.snap index 35e61c875c..531e05370c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnResizer.spec.tsx.snap +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnResizer.spec.tsx.snap @@ -13,6 +13,7 @@ exports[`Column Resizer renders the structure correctly 1`] = ` class="column-resizer-bar" /> +